Reenable displaying sub-messages.
Geary currently displays RFC 822 attachments inline, below the email's
primary message body, using the same HTML chrome for the headers and
email body as for the primary body. Taking the same approach but using
GTK+ widgets meant splitting ConversationMessage up into a
ConversationEmail class that manages the UI for displaying an email in
its entirety, and a ConversationMessage to manage the only header widgets
and webview for displaying an individual RFC 822 message, usable for both
the primary body and any sub-messages. Thus, this is a big change.
One behavioural change is that each sub-message with remote images now
requires individual approval, rather than being dependant on the
containing message's sender and/or approval. This prevents some attacks
e.g. a trusted sender forwarding a spam/malware message, but does not
prevent it if the message is forwarded inline, obviosuly.
* src/client/conversation-viewer/conversation-email.vala (ConversationEmail):
New class for managing the UI for an overall email message. This
replaces the old ConversationMessage and contains much of it's code and
widgets - anything from that class which does not directly support
displaying headers or a message body.
* src/client/conversation-viewer/conversation-message.vala:
(ConversationMessage): Same class as before, but now with its scope
narrowed to only display message headers and body. The draft infobar
remains here rather than being put ConversationEmail where it belongs
since it's bit of a pain to insert in the right place and doesn't
really hurt.
(::email): Moved this property and any code that depends on it to
ConversationEmail.
(::always_load_remote_images): New property passed in via the ctor,
allowing one dependency on the old ::email property to be removed.
(::inlined_content_ids): Moved to ConversationEmail, since that is the
class that keeps track of attachments to display. Add the signal
attachment_displayed_inline to allow ConversationEmail to be notified
of inlined attachments instead.
(::flag_remote_images, ::remember_remote_images): New signals to notify
ConversationEmail that the user has flagged this message or the
message's sender for loading remote images. This is passed through
since in the former's case we may need to set flags on the email
itself, the latter because it is one less use of the contact_store
property, which should be removed from this class at some point.
* src/client/conversation-viewer/conversation-viewer.vala: Chase API
changes from the above. In general, replace use of the term "message"
with "email" since this class is now mostly dealing with
ConversationEmail instances, rather than ConversationMessage instances.
(ConversationViewer::check_mark_read): Only consider the
ConversationEmail's primary message body when checking for visibility
rather than that and any submessages to keep things simple.
(ConversationViewer::show_message, ::hide_message): Renamed to
expand_email/collapse_email respectively since we don't ever actually
hide it. Carry that change on to same methods on ConversationEmail.
* src/engine/rfc822/rfc822-message.vala (Geary.RFC822.Message): Add
get_primary_originator(), almost vermatim from Geary.Email, to support
determining the sender for remembering remote message loading for
senders of sub-emails.
* src/client/components/main-window.vala (MainWindow::set_styling): Fix
background transition for collapsed emails.
* src/client/application/geary-controller.vala: Chase API name changes.
* src/CMakeLists.txt: Include new ConversationEmail source file.
* ui/conversation-email.ui: New UI for ConversationEmail, move the email
action box, attachments box amd sub-messages box here from
conversation-message.ui.
* ui/CMakeLists.txt: Include new UI in compiled resources.
* po/POTFILES.in: Add new UI for transation.
2016-04-19 16:52:34 +10:00
/ *
* Copyright 2016 Software Freedom Conservancy Inc .
2018-01-27 16:22:45 +10:30
* Copyright 2016 - 2018 Michael Gratton < mike @ vee . net >
2011-11-22 18:55:07 -08:00
*
* This software is licensed under the GNU Lesser General Public License
Reenable displaying sub-messages.
Geary currently displays RFC 822 attachments inline, below the email's
primary message body, using the same HTML chrome for the headers and
email body as for the primary body. Taking the same approach but using
GTK+ widgets meant splitting ConversationMessage up into a
ConversationEmail class that manages the UI for displaying an email in
its entirety, and a ConversationMessage to manage the only header widgets
and webview for displaying an individual RFC 822 message, usable for both
the primary body and any sub-messages. Thus, this is a big change.
One behavioural change is that each sub-message with remote images now
requires individual approval, rather than being dependant on the
containing message's sender and/or approval. This prevents some attacks
e.g. a trusted sender forwarding a spam/malware message, but does not
prevent it if the message is forwarded inline, obviosuly.
* src/client/conversation-viewer/conversation-email.vala (ConversationEmail):
New class for managing the UI for an overall email message. This
replaces the old ConversationMessage and contains much of it's code and
widgets - anything from that class which does not directly support
displaying headers or a message body.
* src/client/conversation-viewer/conversation-message.vala:
(ConversationMessage): Same class as before, but now with its scope
narrowed to only display message headers and body. The draft infobar
remains here rather than being put ConversationEmail where it belongs
since it's bit of a pain to insert in the right place and doesn't
really hurt.
(::email): Moved this property and any code that depends on it to
ConversationEmail.
(::always_load_remote_images): New property passed in via the ctor,
allowing one dependency on the old ::email property to be removed.
(::inlined_content_ids): Moved to ConversationEmail, since that is the
class that keeps track of attachments to display. Add the signal
attachment_displayed_inline to allow ConversationEmail to be notified
of inlined attachments instead.
(::flag_remote_images, ::remember_remote_images): New signals to notify
ConversationEmail that the user has flagged this message or the
message's sender for loading remote images. This is passed through
since in the former's case we may need to set flags on the email
itself, the latter because it is one less use of the contact_store
property, which should be removed from this class at some point.
* src/client/conversation-viewer/conversation-viewer.vala: Chase API
changes from the above. In general, replace use of the term "message"
with "email" since this class is now mostly dealing with
ConversationEmail instances, rather than ConversationMessage instances.
(ConversationViewer::check_mark_read): Only consider the
ConversationEmail's primary message body when checking for visibility
rather than that and any submessages to keep things simple.
(ConversationViewer::show_message, ::hide_message): Renamed to
expand_email/collapse_email respectively since we don't ever actually
hide it. Carry that change on to same methods on ConversationEmail.
* src/engine/rfc822/rfc822-message.vala (Geary.RFC822.Message): Add
get_primary_originator(), almost vermatim from Geary.Email, to support
determining the sender for remembering remote message loading for
senders of sub-emails.
* src/client/components/main-window.vala (MainWindow::set_styling): Fix
background transition for collapsed emails.
* src/client/application/geary-controller.vala: Chase API name changes.
* src/CMakeLists.txt: Include new ConversationEmail source file.
* ui/conversation-email.ui: New UI for ConversationEmail, move the email
action box, attachments box amd sub-messages box here from
conversation-message.ui.
* ui/CMakeLists.txt: Include new UI in compiled resources.
* po/POTFILES.in: Add new UI for transation.
2016-04-19 16:52:34 +10:00
* ( version 2.1 or later ) . See the COPYING file in this distribution .
2011-11-22 18:55:07 -08:00
* /
2014-08-28 17:27:50 -07:00
2016-04-22 15:57:07 +10:00
/ * *
* Primary controller for a Geary application instance .
* /
2013-07-23 15:43:56 -07:00
public class GearyController : Geary . BaseObject {
Reimplement in-conversation find.
* src/client/application/geary-controller.vala (GearyController): Remove
ACTION_FIND_NEXT_IN_CONVERSATION and
ACTION_FIND_PREVIOUS_IN_CONVERSATION arctions and callbacks since they
will be taken care of by the search entry & search bar buttons, and
remove from accelerators.ui. Add ACTION_TOGGLE_FIND action to handle
toggling find bar in the same way as the search bar.
* src/client/components/main-toolbar.vala (MainToolbar): Add new button
and infrastrcuture for toggling the find bar.
* src/client/conversation-viewer/conversation-viewer.vala
(ConversationViewer): Convert ::conversation_page to be grid, add new
::conversation_scroller property for the scrollbar, update call
sites. Add props for accessing find widgets, remove old find methods
and add callbacks for handling find start, change, etc.
* src/client/conversation-viewer/conversation-email.vala,
src/client/conversation-viewer/conversation-message.vala: Add methods
for accessing selected text for find.
* src/client/conversation-viewer/conversation-listbox.vala
(ConversationListBox::highlight_search_terms): Updated to return a flag
specifiying whether any search results were found, and to
expand/collapse messsages depending on whether they have any.
* src/client/conversation-viewer/conversation-message.vala
(ConversationMessage::highlight_search_terms): Keep track of how many
results were found, and return that.
* ui/conversation-viewer.ui: Convert conversation_page to be a grid, add
a search bar and search widgets to it, and move conversation
ScrolledWindow to it.
2016-08-22 11:42:54 +10:00
2011-11-22 18:55:07 -08:00
// Named actions.
2017-05-15 23:21:32 +02:00
public const string ACTION_REPLY_TO_MESSAGE = " reply-to-message " ;
public const string ACTION_REPLY_ALL_MESSAGE = " reply-all-message " ;
public const string ACTION_FORWARD_MESSAGE = " forward-message " ;
public const string ACTION_ARCHIVE_CONVERSATION = " archive-conv " ;
public const string ACTION_TRASH_CONVERSATION = " trash-conv " ;
public const string ACTION_DELETE_CONVERSATION = " delete-conv " ;
public const string ACTION_EMPTY_SPAM = " empty-spam " ;
public const string ACTION_EMPTY_TRASH = " empty-trash " ;
public const string ACTION_FIND_IN_CONVERSATION = " conv-find " ;
public const string ACTION_ZOOM = " zoom " ;
public const string ACTION_SHOW_MARK_MENU = " mark-message-menu " ;
public const string ACTION_MARK_AS_READ = " mark-message-read " ;
public const string ACTION_MARK_AS_UNREAD = " mark-message-unread " ;
public const string ACTION_MARK_AS_STARRED = " mark-message-starred " ;
public const string ACTION_MARK_AS_UNSTARRED = " mark-message-unstarred " ;
public const string ACTION_MARK_AS_SPAM = " mark-message-spam " ;
public const string ACTION_MARK_AS_NOT_SPAM = " mark-message-not-spam " ;
public const string ACTION_COPY_MENU = " show-copy-menu " ;
public const string ACTION_MOVE_MENU = " show-move-menu " ;
public const string ACTION_SEARCH = " search-conv " ;
public const string ACTION_CONVERSATION_LIST = " focus-conv-list " ;
public const string ACTION_TOGGLE_SEARCH = " toggle-search " ;
public const string ACTION_TOGGLE_FIND = " toggle-find " ;
// Properties
2013-07-23 15:43:56 -07:00
public const string PROP_CURRENT_CONVERSATION = " current-conversations " ;
2017-05-15 23:21:32 +02:00
public const string PROP_SELECTED_CONVERSATIONS = " selected-conversations " ;
2013-05-20 15:50:15 -07:00
public const int MIN_CONVERSATION_COUNT = 50 ;
2017-05-15 23:21:32 +02:00
2014-01-17 16:34:19 -08:00
private const int SELECT_FOLDER_TIMEOUT_USEC = 100 * 1000 ;
2017-05-15 23:21:32 +02:00
2013-07-08 14:22:23 -07:00
private const string PROP_ATTEMPT_OPEN_ACCOUNT = " attempt-open-account " ;
2016-10-11 01:40:12 +11:00
2019-01-01 22:19:24 +11:00
private const uint MAX_AUTH_ATTEMPTS = 3 ;
2018-02-21 16:43:56 +11:00
2018-10-24 02:14:19 +11:00
private static string untitled_file_name ;
static construct {
// Translators: File name used in save chooser when saving
// attachments that do not otherwise have a name.
GearyController . untitled_file_name = _ ( " Untitled " ) ;
}
2018-02-21 16:43:56 +11:00
internal class AccountContext : Geary . BaseObject {
public Geary . Account account { get ; private set ; }
public Geary . Folder ? inbox = null ;
public Geary . App . EmailStore store { get ; private set ; }
2019-01-01 22:19:24 +11:00
public bool authentication_failed = false ;
public bool authentication_prompting = false ;
public uint authentication_attempts = 0 ;
2019-01-08 23:34:45 +11:00
public bool tls_validation_failed = false ;
public bool tls_validation_prompting = false ;
2018-02-21 16:43:56 +11:00
public Cancellable cancellable { get ; private set ; default = new Cancellable ( ) ; }
public AccountContext ( Geary . Account account ) {
this . account = account ;
this . store = new Geary . App . EmailStore ( account ) ;
}
2018-12-30 22:40:04 +11:00
public Geary . Account . Status get_effective_status ( ) {
Geary . Account . Status current = this . account . current_status ;
Geary . Account . Status effective = 0 ;
if ( current . is_online ( ) ) {
effective | = ONLINE ;
}
if ( current . has_service_problem ( ) ) {
// Only retain this flag if the problem isn't auth or
// cert related, that is handled elsewhere.
Geary . ClientService . Status incoming =
account . incoming . current_status ;
Geary . ClientService . Status outgoing =
account . outgoing . current_status ;
if ( incoming ! = AUTHENTICATION_FAILED & &
incoming ! = TLS_VALIDATION_FAILED & &
outgoing ! = AUTHENTICATION_FAILED & &
outgoing ! = TLS_VALIDATION_FAILED ) {
effective | = SERVICE_PROBLEM ;
}
}
return effective ;
}
2018-02-21 16:43:56 +11:00
}
2016-10-11 01:40:12 +11:00
public weak GearyApplication application { get ; private set ; } // circular ref
2018-07-23 14:38:57 +10:00
public Accounts . Manager ? account_manager { get ; private set ; default = null ; }
2018-05-24 11:14:53 +10:00
2019-01-08 23:38:22 +11:00
/** Application-wide {@link Application.CertificateManager} instance. */
public Application . CertificateManager ? certificate_manager {
get ; private set ; default = null ;
}
2018-02-21 16:04:19 +11:00
public MainWindow ? main_window { get ; private set ; default = null ; }
2013-07-23 15:43:56 -07:00
public Geary . App . ConversationMonitor ? current_conversations { get ; private set ; default = null ; }
2018-12-08 16:10:16 +11:00
2014-06-26 13:31:43 -07:00
public AutostartManager ? autostart_manager { get ; private set ; default = null ; }
2016-04-22 14:55:40 +10:00
2018-10-07 13:18:23 +11:00
public Application . AvatarStore ? avatar_store {
get ; private set ; default = null ;
}
2016-04-22 14:55:40 +10:00
2013-01-31 15:17:44 -08:00
private Geary . Account ? current_account = null ;
2018-02-21 16:43:56 +11:00
private Gee . Map < Geary . AccountInformation , AccountContext > accounts =
new Gee . HashMap < Geary . AccountInformation , AccountContext > ( ) ;
2018-06-17 18:03:09 +10:00
// Created when controller is opened, cancelled and nulled out
// when closed.
private GLib . Cancellable ? open_cancellable = null ;
2013-01-31 15:17:44 -08:00
private Geary . Folder ? current_folder = null ;
2011-11-22 18:55:07 -08:00
private Cancellable cancellable_folder = new Cancellable ( ) ;
2013-06-03 14:20:01 -07:00
private Cancellable cancellable_search = new Cancellable ( ) ;
2013-07-08 14:22:23 -07:00
private Cancellable cancellable_open_account = new Cancellable ( ) ;
2013-08-29 12:37:31 -07:00
private Cancellable cancellable_context_dependent_buttons = new Cancellable ( ) ;
2016-12-11 14:30:59 +01:00
private ContactListStoreCache contact_list_store_cache = new ContactListStoreCache ( ) ;
2013-08-19 16:39:53 -07:00
private Gee . Set < Geary . App . Conversation > selected_conversations = new Gee . HashSet < Geary . App . Conversation > ( ) ;
private Geary . App . Conversation ? last_deleted_conversation = null ;
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Basic inline composition
Many of the details don't work, or don't work well, but the basics are
in place.
Allow only a single inline composition at a time
With this, we introduce a dialog when you would try to add another. We
also use this when changing the selected conversation with a composer
open.
Compose new messages inline, with no conversation selected
Hook up composer accelerators only when focus is in composer editor
It would be nice to only activate these accelerators when the composer
has focus generally, but that doesn't seem to be easy to detect.
Only disconnect accelerators if they're connected
Maintain focus when composer is popped out
The selection isn't, though.
Fix Tab focus for embedded composer
There are two things that needed to be fixed: The tab key doesn't
usually advance focus for embedded widgets (huh?), so we handle tab
presses by hand for ComposerWidgets. Also, the EmailEntrys do their own
tab handling, which needs to know about the composer widget, not the
toplevel widget in the embedded case.
Remove close() from ComposerContainer interface
I don't think it was actually doing anything, and it conflicts with the new close() method of Gtk.Window.
2014-02-11 00:26:14 -05:00
private Gee . LinkedList < ComposerWidget > composer_widgets = new Gee . LinkedList < ComposerWidget > ( ) ;
2012-08-16 18:33:28 -07:00
private NewMessagesMonitor ? new_messages_monitor = null ;
private NewMessagesIndicator ? new_messages_indicator = null ;
private UnityLauncher ? unity_launcher = null ;
2013-09-13 17:18:37 -07:00
private Libnotify ? libnotify = null ;
2013-02-07 12:28:43 -08:00
private uint select_folder_timeout_id = 0 ;
2014-01-17 16:34:19 -08:00
private int64 next_folder_select_allowed_usec = 0 ;
2013-04-26 17:58:09 -07:00
private Geary . Nonblocking . Mutex select_folder_mutex = new Geary . Nonblocking . Mutex ( ) ;
2013-05-14 11:52:02 -07:00
private Geary . Folder ? previous_non_search_folder = null ;
2013-07-08 14:22:23 -07:00
private UpgradeDialog upgrade_dialog ;
2013-09-30 18:29:14 -07:00
private Gee . List < string > pending_mailtos = new Gee . ArrayList < string > ( ) ;
2018-07-02 23:51:13 +10:00
private uint operation_count = 0 ;
2015-02-05 17:57:27 -08:00
private Geary . Revokable ? revokable = null ;
2017-07-14 16:00:45 +02:00
2013-08-08 18:47:23 -07:00
// List of windows we're waiting to close before Geary closes.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Basic inline composition
Many of the details don't work, or don't work well, but the basics are
in place.
Allow only a single inline composition at a time
With this, we introduce a dialog when you would try to add another. We
also use this when changing the selected conversation with a composer
open.
Compose new messages inline, with no conversation selected
Hook up composer accelerators only when focus is in composer editor
It would be nice to only activate these accelerators when the composer
has focus generally, but that doesn't seem to be easy to detect.
Only disconnect accelerators if they're connected
Maintain focus when composer is popped out
The selection isn't, though.
Fix Tab focus for embedded composer
There are two things that needed to be fixed: The tab key doesn't
usually advance focus for embedded widgets (huh?), so we handle tab
presses by hand for ComposerWidgets. Also, the EmailEntrys do their own
tab handling, which needs to know about the composer widget, not the
toplevel widget in the embedded case.
Remove close() from ComposerContainer interface
I don't think it was actually doing anything, and it conflicts with the new close() method of Gtk.Window.
2014-02-11 00:26:14 -05:00
private Gee . List < ComposerWidget > waiting_to_close = new Gee . ArrayList < ComposerWidget > ( ) ;
2017-05-15 23:21:32 +02:00
private const ActionEntry [ ] win_action_entries = {
2019-02-24 21:44:03 +11:00
{ GearyApplication . ACTION_CLOSE , on_close } ,
{ GearyApplication . ACTION_UNDO , on_revoke } ,
2017-05-15 23:21:32 +02:00
{ ACTION_CONVERSATION_LIST , on_conversation_list } ,
{ ACTION_FIND_IN_CONVERSATION , on_find_in_conversation_action } ,
{ ACTION_SEARCH , on_search_activated } ,
{ ACTION_EMPTY_SPAM , on_empty_spam } ,
{ ACTION_EMPTY_TRASH , on_empty_trash } ,
// Message actions
{ ACTION_REPLY_TO_MESSAGE , on_reply_to_message_action } ,
{ ACTION_REPLY_ALL_MESSAGE , on_reply_all_message_action } ,
{ ACTION_FORWARD_MESSAGE , on_forward_message_action } ,
{ ACTION_ARCHIVE_CONVERSATION , on_archive_conversation } ,
{ ACTION_TRASH_CONVERSATION , on_trash_conversation } ,
{ ACTION_DELETE_CONVERSATION , on_delete_conversation } ,
{ ACTION_COPY_MENU , on_show_copy_menu } ,
{ ACTION_MOVE_MENU , on_show_move_menu } ,
// Message marking actions
{ ACTION_SHOW_MARK_MENU , on_show_mark_menu } ,
{ ACTION_MARK_AS_READ , on_mark_as_read } ,
{ ACTION_MARK_AS_UNREAD , on_mark_as_unread } ,
{ ACTION_MARK_AS_STARRED , on_mark_as_starred } ,
{ ACTION_MARK_AS_UNSTARRED , on_mark_as_unstarred } ,
{ ACTION_MARK_AS_SPAM , on_mark_as_spam_toggle } ,
{ ACTION_MARK_AS_NOT_SPAM , on_mark_as_spam_toggle } ,
// Message viewer
{ ACTION_ZOOM , on_zoom , " s " } ,
} ;
2013-06-07 16:23:45 -07:00
/ * *
* Fired when the currently selected account has changed .
* /
public signal void account_selected ( Geary . Account ? account ) ;
/ * *
* Fired when the currently selected folder has changed .
* /
public signal void folder_selected ( Geary . Folder ? folder ) ;
2013-09-06 11:32:58 -07:00
/ * *
* Fired when the number of conversations changes .
* /
public signal void conversation_count_changed ( int count ) ;
2013-06-18 11:26:44 -07:00
/ * *
* Fired when the search text is changed according to the controller . This accounts
* for a brief typmatic delay .
* /
public signal void search_text_changed ( string keywords ) ;
2016-10-11 01:40:12 +11:00
/ * *
* Constructs a new instance of the controller .
* /
public GearyController ( GearyApplication application ) {
this . application = application ;
2013-06-07 16:23:45 -07:00
}
2016-10-11 01:40:12 +11:00
2013-06-07 16:23:45 -07:00
~ GearyController ( ) {
assert ( current_account = = null ) ;
}
2016-10-11 01:40:12 +11:00
2013-06-07 16:23:45 -07:00
/ * *
* Starts the controller and brings up Geary .
* /
2017-10-27 16:19:35 +02:00
public async void open_async ( GLib . Cancellable ? cancellable ) {
2018-05-24 11:14:53 +10:00
Geary . Engine engine = this . application . engine ;
2016-04-27 11:56:25 +10:00
// This initializes the IconFactory, important to do before
// the actions are created (as they refer to some of Geary's
// custom icons)
2012-09-27 10:57:19 -07:00
IconFactory . instance . init ( ) ;
2016-04-27 11:56:25 +10:00
2016-10-09 19:06:48 +02:00
apply_app_menu_fix ( ) ;
2018-06-17 18:03:09 +10:00
this . open_cancellable = new GLib . Cancellable ( ) ;
2012-05-22 14:04:24 -07:00
// Listen for attempts to close the application.
2016-10-11 01:40:12 +11:00
this . application . exiting . connect ( on_application_exiting ) ;
2016-12-31 14:43:35 +11:00
2013-07-08 14:22:23 -07:00
// Create DB upgrade dialog.
upgrade_dialog = new UpgradeDialog ( ) ;
upgrade_dialog . notify [ UpgradeDialog . PROP_VISIBLE_NAME ] . connect ( display_main_window_if_ready ) ;
2016-04-22 14:55:40 +10:00
2016-12-31 14:43:35 +11:00
// Initialise WebKit and WebViews
ClientWebView . init_web_context (
2017-01-04 03:32:50 +11:00
this . application . config ,
2016-12-31 14:43:35 +11:00
this . application . get_web_extensions_dir ( ) ,
2017-01-27 14:40:55 +11:00
this . application . get_user_cache_directory ( ) . get_child ( " web-resources " ) ,
2016-12-31 14:43:35 +11:00
Args . log_debug
) ;
2016-10-16 21:42:48 +11:00
try {
2019-02-28 18:34:28 +11:00
ClientWebView . load_resources (
2016-12-31 14:43:35 +11:00
this . application . get_user_config_directory ( )
) ;
2019-02-28 18:34:28 +11:00
ComposerWebView . load_resources ( ) ;
ConversationWebView . load_resources ( ) ;
2016-10-16 21:42:48 +11:00
} catch ( Error err ) {
2016-11-23 23:48:47 +11:00
error ( " Error loading web resources: %s " , err . message ) ;
2016-10-16 21:42:48 +11:00
}
2018-10-07 13:18:23 +11:00
this . avatar_store = new Application . AvatarStore (
this . application . config ,
this . application . get_user_cache_directory ( )
2016-04-22 14:55:40 +10:00
) ;
2011-11-22 18:55:07 -08:00
// Create the main window (must be done after creating actions.)
2016-10-11 01:40:12 +11:00
main_window = new MainWindow ( this . application ) ;
2019-01-01 21:26:42 +11:00
main_window . retry_service_problem . connect ( on_retry_service_problem ) ;
2014-01-07 15:04:49 -08:00
main_window . on_shift_key . connect ( on_shift_key ) ;
2012-09-27 19:22:34 -07:00
main_window . notify [ " has-toplevel-focus " ] . connect ( on_has_toplevel_focus ) ;
2017-05-15 23:21:32 +02:00
setup_actions ( ) ;
2012-01-30 12:20:25 -08:00
enable_message_buttons ( false ) ;
2017-05-15 23:21:32 +02:00
2018-05-24 11:14:53 +10:00
engine . account_available . connect ( on_account_available ) ;
2012-05-22 13:44:57 -07:00
// Connect to various UI signals.
2012-09-07 20:01:12 -07:00
main_window . conversation_list_view . conversations_selected . connect ( on_conversations_selected ) ;
2013-07-19 15:44:20 -07:00
main_window . conversation_list_view . conversation_activated . connect ( on_conversation_activated ) ;
2012-09-07 20:01:12 -07:00
main_window . conversation_list_view . load_more . connect ( on_load_more ) ;
2013-08-26 14:56:46 -07:00
main_window . conversation_list_view . mark_conversations . connect ( on_mark_conversations ) ;
2012-09-27 19:22:34 -07:00
main_window . conversation_list_view . visible_conversations_changed . connect ( on_visible_conversations_changed ) ;
2012-02-01 12:32:07 -08:00
main_window . folder_list . folder_selected . connect ( on_folder_selected ) ;
2012-05-22 13:44:57 -07:00
main_window . folder_list . copy_conversation . connect ( on_copy_conversation ) ;
main_window . folder_list . move_conversation . connect ( on_move_conversation ) ;
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 ) ;
2016-10-09 18:57:25 +11:00
main_window . search_bar . search_text_changed . connect ( ( text ) = > { do_search ( text ) ; } ) ;
2016-07-25 10:33:42 +10:00
main_window . conversation_viewer . conversation_added . connect (
on_conversation_view_added
) ;
2013-02-12 16:17:16 -08:00
new_messages_monitor = new NewMessagesMonitor ( should_notify_new_messages ) ;
2013-02-12 18:29:51 -08:00
main_window . folder_list . set_new_messages_monitor ( new_messages_monitor ) ;
2016-07-25 10:33:42 +10:00
2013-02-12 16:17:16 -08:00
// New messages indicator (Ubuntuism)
new_messages_indicator = NewMessagesIndicator . create ( new_messages_monitor ) ;
new_messages_indicator . application_activated . connect ( on_indicator_activated_application ) ;
new_messages_indicator . composer_activated . connect ( on_indicator_activated_composer ) ;
new_messages_indicator . inbox_activated . connect ( on_indicator_activated_inbox ) ;
2018-10-07 13:18:23 +11:00
2013-02-12 16:17:16 -08:00
unity_launcher = new UnityLauncher ( new_messages_monitor ) ;
2018-10-07 13:18:23 +11:00
this . libnotify = new Libnotify (
this . new_messages_monitor , this . avatar_store
) ;
this . libnotify . invoked . connect ( on_libnotify_invoked ) ;
2016-10-20 16:58:48 +11:00
this . main_window . conversation_list_view . grab_focus ( ) ;
2014-06-26 13:31:43 -07:00
// instantiate here to ensure that Config is initialized and ready
2017-10-04 17:44:41 +11:00
this . autostart_manager = new AutostartManager ( this . application ) ;
2015-02-05 17:57:27 -08:00
// initialize revokable
save_revokable ( null , null ) ;
2016-07-08 03:34:00 +02:00
// Migrate configuration if necessary.
try {
2016-10-11 01:40:12 +11:00
Migrate . xdg_config_dir ( this . application . get_user_data_directory ( ) ,
this . application . get_user_config_directory ( ) ) ;
2016-07-08 03:34:00 +02:00
} catch ( Error e ) {
error ( " Error migrating configuration directories: %s " , e . message ) ;
}
2019-01-08 23:38:22 +11:00
// Hook up cert, accounts and credentials machinery
2019-01-10 17:33:37 +11:00
this . certificate_manager = yield new Application . CertificateManager (
this . application . get_user_data_directory ( ) . get_child ( " pinned-certs " ) ,
cancellable
2019-01-10 13:58:56 +11:00
) ;
2019-01-08 23:38:22 +11:00
2018-12-26 15:27:47 +10:30
SecretMediator ? libsecret = null ;
try {
libsecret = yield new SecretMediator ( this . application , cancellable ) ;
} catch ( GLib . Error err ) {
error ( " Error opening libsecret: %s " , err . message ) ;
}
2018-07-23 14:38:57 +10:00
this . account_manager = new Accounts . Manager (
2018-12-26 15:27:47 +10:30
libsecret ,
2018-05-24 15:31:21 +10:00
this . application . get_user_config_directory ( ) ,
this . application . get_user_data_directory ( )
) ;
2018-06-02 01:04:59 +02:00
this . account_manager . account_added . connect (
on_account_added
2018-05-28 00:54:43 +10:00
) ;
2018-06-02 01:04:59 +02:00
this . account_manager . account_status_changed . connect (
on_account_status_changed
2018-05-28 00:54:43 +10:00
) ;
2018-06-17 18:03:09 +10:00
this . account_manager . account_removed . connect (
on_account_removed
) ;
2018-12-09 16:46:00 +11:00
this . account_manager . report_problem . connect (
on_report_problem
) ;
2017-10-27 16:19:35 +02:00
try {
yield this . account_manager . connect_goa ( cancellable ) ;
} catch ( GLib . Error err ) {
warning ( " Error opening GOA: %s " , err . message ) ;
}
2018-07-23 17:46:33 +10:00
// Start the engine and load our accounts
2017-10-27 16:19:35 +02:00
try {
yield engine . open_async (
this . application . get_resource_directory ( ) , cancellable
) ;
yield this . account_manager . load_accounts ( cancellable ) ;
2018-05-24 11:14:53 +10:00
if ( engine . get_accounts ( ) . size = = 0 ) {
2018-07-23 17:46:33 +10:00
this . application . show_accounts ( ) ;
if ( engine . get_accounts ( ) . size = = 0 ) {
// User cancelled without creating an account, so
// nothing else to do but exit.
this . application . quit ( ) ;
}
2013-06-07 17:49:53 -07:00
}
} catch ( Error e ) {
2018-05-24 11:22:29 +10:00
warning ( " Error opening Geary.Engine instance: %s " , e . message ) ;
2013-06-07 17:49:53 -07:00
}
2018-06-17 18:03:09 +10:00
// Expunge any deleted accounts in the background, so we're
// not blocking the app continuing to open.
this . expunge_accounts . begin ( ) ;
2011-11-22 18:55:07 -08:00
}
2018-05-24 11:22:29 +10:00
2013-06-07 16:23:45 -07:00
/ * *
2015-02-05 17:57:27 -08:00
* At the moment , this is non - reversible , i . e . once closed a GearyController cannot be
* re - opened .
2013-06-07 16:23:45 -07:00
* /
2015-02-05 17:57:27 -08:00
public async void close_async ( ) {
2018-06-17 18:03:09 +10:00
// Cancel internal processes early so they don't block
// shutdown
this . open_cancellable . cancel ( ) ;
this . open_cancellable = null ;
2015-03-13 15:37:31 -07:00
Geary . Engine . instance . account_available . disconnect ( on_account_available ) ;
2016-07-25 10:33:42 +10:00
2018-02-21 16:04:19 +11:00
// Release folder and conversations in the main window
on_conversations_selected ( new Gee . HashSet < Geary . App . Conversation > ( ) ) ;
on_folder_selected ( null ) ;
2016-07-25 10:33:42 +10:00
// Disconnect from various UI signals.
2015-03-13 15:37:31 -07:00
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 ) ;
main_window . conversation_list_view . mark_conversations . disconnect ( on_mark_conversations ) ;
main_window . conversation_list_view . visible_conversations_changed . disconnect ( on_visible_conversations_changed ) ;
main_window . folder_list . folder_selected . disconnect ( on_folder_selected ) ;
main_window . folder_list . copy_conversation . disconnect ( on_copy_conversation ) ;
main_window . folder_list . move_conversation . disconnect ( on_move_conversation ) ;
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 ) ;
2016-07-25 10:33:42 +10:00
main_window . conversation_viewer . conversation_added . disconnect (
on_conversation_view_added
) ;
2016-05-13 11:19:10 +10:00
2015-02-05 17:57:27 -08:00
// hide window while shutting down, as this can take a few seconds under certain conditions
main_window . hide ( ) ;
2018-02-21 16:04:19 +11:00
// Release monitoring early so held resources can be freed up
this . libnotify = null ;
this . new_messages_indicator = null ;
this . unity_launcher = null ;
this . new_messages_monitor . clear_folders ( ) ;
this . new_messages_monitor = null ;
2015-02-05 17:57:27 -08:00
// drop the Revokable, which will commit it if necessary
save_revokable ( null , null ) ;
2017-10-04 17:44:41 +11:00
2018-02-21 16:04:19 +11:00
this . cancellable_open_account . cancel ( ) ;
2017-10-04 17:44:41 +11:00
2017-11-21 01:13:21 +11:00
// Close the ConversationMonitor
if ( current_conversations ! = null ) {
debug ( " Stopping conversation monitor for %s... " ,
2017-12-07 15:49:35 +11:00
this . current_conversations . base_folder . to_string ( ) ) ;
2015-02-05 17:57:27 -08:00
try {
2017-11-21 01:13:21 +11:00
yield this . current_conversations . stop_monitoring_async ( null ) ;
2015-02-05 17:57:27 -08:00
} catch ( Error err ) {
2017-11-21 01:13:21 +11:00
debug (
" Error closing conversation monitor %s at shutdown: %s " ,
2017-12-07 15:49:35 +11:00
this . current_conversations . base_folder . to_string ( ) ,
2017-11-21 01:13:21 +11:00
err . message
) ;
2015-02-05 17:57:27 -08:00
}
2018-02-21 16:04:19 +11:00
this . current_conversations = null ;
2015-02-05 17:57:27 -08:00
}
2017-11-21 01:13:21 +11:00
2018-02-21 16:43:56 +11:00
// Create an array of known accounts so the loops below do not
// explode if accounts are removed while iterating.
AccountContext [ ] accounts = this . accounts . values . to_array ( ) ;
// Close all inboxes. Launch these in parallel first so we're
// not wasting time waiting for each one to close. The account
2017-11-21 01:13:21 +11:00
// will wait around for them to actually close.
2018-02-21 16:43:56 +11:00
foreach ( AccountContext context in accounts ) {
Geary . Folder ? inbox = context . inbox ;
if ( inbox ! = null ) {
2018-10-07 13:18:23 +11:00
debug ( " Closing inbox: %s... " , inbox . to_string ( ) ) ;
2018-02-21 16:43:56 +11:00
inbox . close_async . begin ( null , ( obj , ret ) = > {
try {
inbox . close_async . end ( ret ) ;
} catch ( Error err ) {
debug (
" Error closing Inbox %s at shutdown: %s " ,
inbox . to_string ( ) , err . message
) ;
}
} ) ;
context . inbox = null ;
2015-02-05 17:57:27 -08:00
}
}
2016-04-22 14:55:40 +10:00
2018-02-21 16:43:56 +11:00
// Close all Accounts. Again, this is done in parallel to
// minimise time taken to close, but here use a barrier to
// wait for all to actually finish closing.
2017-11-21 01:13:21 +11:00
Geary . Nonblocking . CountingSemaphore close_barrier =
new Geary . Nonblocking . CountingSemaphore ( null ) ;
2018-02-21 16:43:56 +11:00
foreach ( AccountContext context in accounts ) {
2017-11-21 01:13:21 +11:00
close_barrier . acquire ( ) ;
2018-05-24 15:49:52 +10:00
this . close_account . begin (
context . account . information ,
( obj , ret ) = > {
this . close_account . end ( ret ) ;
close_barrier . blind_notify ( ) ;
}
) ;
2017-11-21 01:13:21 +11:00
}
try {
yield close_barrier . wait_async ( ) ;
} catch ( Error err ) {
debug ( " Error waiting at shutdown barrier: %s " , err . message ) ;
}
2016-04-22 14:55:40 +10:00
2015-02-05 17:57:27 -08:00
// Turn off the lights and lock the door behind you
try {
2015-03-05 15:55:05 -08:00
debug ( " Closing Engine... " ) ;
2015-02-05 17:57:27 -08:00
yield Geary . Engine . instance . close_async ( null ) ;
2015-03-05 15:55:05 -08:00
debug ( " Closed Engine " ) ;
2015-02-05 17:57:27 -08:00
} catch ( Error err ) {
message ( " Error closing Geary Engine instance: %s " , err . message ) ;
}
2018-02-21 16:04:19 +11:00
2018-06-02 01:04:59 +02:00
this . account_manager . account_added . disconnect (
on_account_added
2018-05-28 00:54:43 +10:00
) ;
2018-06-02 01:04:59 +02:00
this . account_manager . account_status_changed . disconnect (
on_account_status_changed
2018-05-28 00:54:43 +10:00
) ;
2018-06-17 18:03:09 +10:00
this . account_manager . account_removed . disconnect (
on_account_removed
) ;
2018-05-24 11:14:53 +10:00
this . account_manager = null ;
2018-02-21 16:04:19 +11:00
this . application . remove_window ( this . main_window ) ;
this . main_window . destroy ( ) ;
this . main_window = null ;
this . upgrade_dialog = null ;
this . current_account = null ;
this . current_folder = null ;
this . previous_non_search_folder = null ;
this . selected_conversations = new Gee . HashSet < Geary . App . Conversation > ( ) ;
this . last_deleted_conversation = null ;
this . pending_mailtos . clear ( ) ;
this . composer_widgets . clear ( ) ;
this . waiting_to_close . clear ( ) ;
this . autostart_manager = null ;
2018-10-07 13:18:23 +11:00
this . avatar_store . close ( ) ;
this . avatar_store = null ;
2018-02-21 16:04:19 +11:00
debug ( " Closed GearyController " ) ;
2011-11-22 18:55:07 -08:00
}
2016-09-25 01:10:07 +10:00
/ * *
* Opens a new , blank composer .
* /
public void compose ( ) {
create_compose_widget ( ComposerWidget . ComposeType . NEW_MESSAGE ) ;
}
/ * *
* Opens or queues a new composer addressed to a specific email address .
* /
public void compose_mailto ( string mailto ) {
if ( current_account = = null ) {
// Schedule the send for after we have an account open.
pending_mailtos . add ( mailto ) ;
} else {
create_compose_widget ( ComposerWidget . ComposeType . NEW_MESSAGE , null , null , mailto ) ;
}
}
2018-06-17 18:03:09 +10:00
/** Expunges removed accounts while the controller remains open. */
internal async void expunge_accounts ( ) {
2018-02-21 16:43:56 +11:00
try {
2018-06-17 18:03:09 +10:00
yield this . account_manager . expunge_accounts ( this . open_cancellable ) ;
} catch ( GLib . Error err ) {
2018-05-24 15:49:52 +10:00
report_problem (
new Geary . ProblemReport ( Geary . ProblemType . GENERIC_ERROR , err )
) ;
2018-02-21 16:43:56 +11:00
}
2012-04-17 13:15:46 -07:00
}
2016-10-09 19:06:48 +02:00
// Fix for clients having both:
// * disabled Gtk/ShellShowsAppMenu setting
// * no 'menu' setting in Gtk/DecorationLayout
// See https://bugzilla.gnome.org/show_bug.cgi?id=770617
private void apply_app_menu_fix ( ) {
Gtk . Settings ? settings = Gtk . Settings . get_default ( ) ;
if ( settings = = null ) {
warning ( " Couldn't fetch Gtk default settings " ) ;
2017-05-15 23:21:32 +02:00
return ;
2016-10-09 19:06:48 +02:00
}
2017-05-15 23:21:32 +02:00
string decoration_layout = settings . gtk_decoration_layout ? ? " " ;
2016-10-09 19:06:48 +02:00
if ( ! decoration_layout . contains ( " menu " ) ) {
string prefix = " menu: " ;
if ( decoration_layout . contains ( " : " ) ) {
prefix = ( decoration_layout . has_prefix ( " : " ) ) ? " menu " : " menu, " ;
}
settings . gtk_decoration_layout = prefix + settings . gtk_decoration_layout ;
}
}
2013-12-12 11:32:13 -08:00
private void setup_actions ( ) {
2017-05-15 23:21:32 +02:00
this . main_window . add_action_entries ( win_action_entries , this ) ;
add_window_accelerators ( ACTION_MARK_AS_READ , { " <Ctrl>I " , " <Shift>I " } ) ;
add_window_accelerators ( ACTION_MARK_AS_UNREAD , { " <Ctrl>U " , " <Shift>U " } ) ;
add_window_accelerators ( ACTION_MARK_AS_STARRED , { " S " } ) ;
add_window_accelerators ( ACTION_MARK_AS_UNSTARRED , { " D " } ) ;
add_window_accelerators ( ACTION_MARK_AS_SPAM , { " <Ctrl>J " , " exclam " } ) ; // Exclamation mark (!)
add_window_accelerators ( ACTION_MARK_AS_NOT_SPAM , { " <Ctrl>J " , " exclam " } ) ;
add_window_accelerators ( ACTION_COPY_MENU , { " L " } ) ;
add_window_accelerators ( ACTION_MOVE_MENU , { " M " } ) ;
add_window_accelerators ( ACTION_REPLY_TO_MESSAGE , { " <Ctrl>R " , " R " } ) ;
add_window_accelerators ( ACTION_REPLY_ALL_MESSAGE , { " <Ctrl><Shift>R " , " <Shift>R " } ) ;
add_window_accelerators ( ACTION_FORWARD_MESSAGE , { " <Ctrl>L " , " F " } ) ;
add_window_accelerators ( ACTION_FIND_IN_CONVERSATION , { " <Ctrl>F " , " slash " } ) ;
add_window_accelerators ( ACTION_ARCHIVE_CONVERSATION , { " A " } ) ;
add_window_accelerators ( ACTION_TRASH_CONVERSATION , { " Delete " , " BackSpace " } ) ;
add_window_accelerators ( ACTION_DELETE_CONVERSATION , { " <Shift>Delete " , " <Shift>BackSpace " } ) ;
add_window_accelerators ( ACTION_ZOOM + ( " ('in') " ) , { " <Ctrl>equal " , " equal " } ) ;
add_window_accelerators ( ACTION_ZOOM + ( " ('out') " ) , { " <Ctrl>minus " , " minus " } ) ;
add_window_accelerators ( ACTION_ZOOM + ( " ('normal') " ) , { " <Ctrl>0 " , " 0 " } ) ;
add_window_accelerators ( ACTION_SEARCH , { " <Ctrl>S " } ) ;
add_window_accelerators ( ACTION_CONVERSATION_LIST , { " <Ctrl>B " } ) ;
}
private void add_window_accelerators ( string action , string [ ] accelerators , Variant ? param = null ) {
this . application . set_accels_for_action ( " win. " + action , accelerators ) ;
2012-09-18 15:38:56 -07:00
}
2016-10-11 01:40:12 +11:00
2013-06-07 16:23:45 -07:00
private void open_account ( Geary . Account account ) {
2019-01-01 22:19:24 +11:00
account . information . authentication_failure . connect (
on_authentication_failure
) ;
2018-12-27 16:56:11 +11:00
account . information . untrusted_host . connect ( on_untrusted_host ) ;
2018-12-30 22:40:04 +11:00
account . notify [ " current-status " ] . connect (
on_account_status_notify
) ;
2013-06-07 16:23:45 -07:00
account . report_problem . connect ( on_report_problem ) ;
2013-07-08 14:22:23 -07:00
connect_account_async . begin ( account , cancellable_open_account ) ;
2017-07-14 16:00:45 +02:00
2016-12-11 14:30:59 +01:00
ContactListStore list_store = this . contact_list_store_cache . create ( account . get_contact_store ( ) ) ;
account . contacts_loaded . connect ( list_store . set_sort_function ) ;
2013-06-07 16:23:45 -07:00
}
2016-12-11 14:30:59 +01:00
2018-12-30 22:40:04 +11:00
private async void close_account ( Geary . AccountInformation config ) {
AccountContext ? context = this . accounts . get ( config ) ;
2018-02-21 16:43:56 +11:00
if ( context ! = null ) {
2018-12-30 22:40:04 +11:00
Geary . Account account = context . account ;
Geary . ContactStore contact_store = account . get_contact_store ( ) ;
ContactListStore list_store =
this . contact_list_store_cache . get ( contact_store ) ;
2016-12-11 14:30:59 +01:00
2018-12-30 22:40:04 +11:00
account . contacts_loaded . disconnect ( list_store . set_sort_function ) ;
2018-02-21 16:43:56 +11:00
this . contact_list_store_cache . unset ( contact_store ) ;
2018-12-30 22:40:04 +11:00
if ( this . current_account = = account ) {
2018-02-21 16:43:56 +11:00
this . current_account = null ;
previous_non_search_folder = null ;
main_window . search_bar . set_search_text ( " " ) ; // Reset search.
cancel_folder ( ) ;
}
2018-12-30 22:40:04 +11:00
// Stop updating status and showing errors when closing
// the account - the user doesn't care any more
account . report_problem . disconnect ( on_report_problem ) ;
2019-01-01 22:19:24 +11:00
account . information . authentication_failure . disconnect (
on_authentication_failure
) ;
2018-12-27 16:56:11 +11:00
account . information . untrusted_host . disconnect ( on_untrusted_host ) ;
2018-12-30 22:40:04 +11:00
account . notify [ " current-status " ] . disconnect (
on_account_status_notify
) ;
2018-02-21 16:43:56 +11:00
2018-05-24 15:49:52 +10:00
yield disconnect_account_async ( context ) ;
2013-06-07 16:23:45 -07:00
}
}
2018-02-21 16:43:56 +11:00
2017-11-13 23:34:29 +11:00
private void report_problem ( Geary . ProblemReport report ) {
debug ( " Problem reported: %s " , report . to_string ( ) ) ;
2017-11-07 11:15:15 +11:00
2019-01-07 23:13:30 +11:00
if ( report . error = = null | |
! ( report . error . thrown is IOError . CANCELLED ) ) {
2019-01-01 23:22:54 +11:00
MainWindowInfoBar info_bar = new MainWindowInfoBar . for_problem ( report ) ;
info_bar . retry . connect ( on_retry_problem ) ;
this . main_window . show_infobar ( info_bar ) ;
}
}
private void update_account_status ( ) {
2019-01-24 18:48:30 +11:00
// Start off assuming all accounts are online and error free
// (i.e. no status issues to indicate) and proceed until
// proven incorrect.
Geary . Account . Status effective_status = ONLINE ;
2019-01-07 23:13:30 +11:00
bool has_auth_error = false ;
2019-01-08 23:34:45 +11:00
bool has_cert_error = false ;
2019-01-07 23:13:30 +11:00
Geary . Account ? service_problem_source = null ;
2019-01-01 23:22:54 +11:00
foreach ( AccountContext context in this . accounts . values ) {
2019-01-24 18:48:30 +11:00
Geary . Account . Status status = context . get_effective_status ( ) ;
if ( ! status . is_online ( ) ) {
effective_status & = ~ Geary . Account . Status . ONLINE ;
}
if ( status . has_service_problem ( ) ) {
effective_status | = SERVICE_PROBLEM ;
if ( service_problem_source = = null ) {
service_problem_source = context . account ;
}
2019-01-07 23:13:30 +11:00
}
has_auth_error | = context . authentication_failed ;
2019-01-08 23:34:45 +11:00
has_cert_error | = context . tls_validation_failed ;
2019-01-01 23:22:54 +11:00
}
foreach ( Gtk . Window window in this . application . get_windows ( ) ) {
MainWindow ? main = window as MainWindow ;
if ( main ! = null ) {
2019-01-07 23:13:30 +11:00
main . update_account_status (
2019-01-08 23:34:45 +11:00
effective_status ,
has_auth_error ,
has_cert_error ,
service_problem_source
2019-01-07 23:13:30 +11:00
) ;
2017-11-13 23:34:29 +11:00
}
2017-11-08 18:13:51 +11:00
}
}
2016-10-26 10:27:02 +02:00
2019-01-01 22:19:24 +11:00
private bool is_currently_prompting ( ) {
return this . accounts . values . fold < bool > (
2019-01-08 23:34:45 +11:00
( ctx , seed ) = > (
ctx . authentication_prompting |
ctx . tls_validation_prompting |
seed
) ,
2019-01-01 22:19:24 +11:00
false
) ;
}
private async void prompt_for_password ( AccountContext context ,
Geary . ServiceInformation service ) {
Geary . AccountInformation account = context . account . information ;
bool is_incoming = ( service = = account . incoming ) ;
Geary . Credentials credentials = is_incoming
? account . incoming . credentials
: account . get_outgoing_credentials ( ) ;
bool handled = true ;
2019-02-16 16:33:44 +11:00
if ( context . authentication_attempts > MAX_AUTH_ATTEMPTS | |
2019-01-01 22:19:24 +11:00
credentials = = null ) {
2019-02-16 16:33:44 +11:00
// We have run out of authentication attempts or have
// been asked for creds but don't even have a login. So
// just bail out immediately and flag the account as
// needing attention.
2019-01-01 22:19:24 +11:00
handled = false ;
2019-02-16 16:33:44 +11:00
} else if ( this . account_manager . is_goa_account ( account ) ) {
context . authentication_prompting = true ;
try {
2019-02-18 12:06:06 +11:00
yield account . load_incoming_credentials ( context . cancellable ) ;
yield account . load_outgoing_credentials ( context . cancellable ) ;
2019-02-16 16:33:44 +11:00
} catch ( GLib . Error err ) {
// Bail out right away, but probably should be opening
// the GOA control panel.
handled = false ;
report_problem (
new Geary . AccountProblemReport (
Geary . ProblemType . GENERIC_ERROR ,
account ,
err
)
) ;
}
context . authentication_prompting = false ;
2019-01-01 22:19:24 +11:00
} else {
context . authentication_prompting = true ;
this . application . present ( ) ;
PasswordDialog password_dialog = new PasswordDialog (
this . application . get_active_window ( ) ,
account ,
service
2018-12-30 22:40:04 +11:00
) ;
2019-01-01 22:19:24 +11:00
if ( password_dialog . run ( ) ) {
service . credentials = service . credentials . copy_with_token (
password_dialog . password
) ;
service . remember_password = password_dialog . remember_password ;
// The update the credentials for the service that the
// credentials actually came from
Geary . ServiceInformation creds_service =
credentials = = account . incoming . credentials
? account . incoming
: account . outgoing ;
SecretMediator libsecret = ( SecretMediator ) account . mediator ;
try {
yield libsecret . update_token (
account , creds_service , context . cancellable
) ;
// Update the actual service in the engine though
yield this . application . engine . update_account_service (
account , service , context . cancellable
) ;
} catch ( GLib . IOError . CANCELLED err ) {
// all good
} catch ( GLib . Error err ) {
report_problem (
new Geary . ServiceProblemReport (
Geary . ProblemType . GENERIC_ERROR ,
account ,
service ,
err
)
) ;
}
context . authentication_attempts + + ;
} else {
// User cancelled, bail out unconditionally
handled = false ;
}
context . authentication_prompting = false ;
}
if ( ! handled ) {
context . authentication_attempts = 0 ;
context . authentication_failed = true ;
update_account_status ( ) ;
}
}
2019-01-08 23:34:45 +11:00
private async void prompt_untrusted_host ( AccountContext context ,
Geary . ServiceInformation service ,
Geary . Endpoint endpoint ,
GLib . TlsConnection cx ) {
if ( Args . revoke_certs ) {
// XXX
}
context . tls_validation_prompting = true ;
2019-01-08 23:38:22 +11:00
try {
yield this . certificate_manager . prompt_pin_certificate (
this . main_window ,
context . account . information ,
service ,
endpoint ,
2019-01-09 13:02:06 +11:00
false ,
2019-01-08 23:38:22 +11:00
context . cancellable
) ;
context . tls_validation_failed = false ;
} catch ( Application . CertificateManagerError . UNTRUSTED err ) {
// Don't report an error here, the user simply declined.
context . tls_validation_failed = true ;
} catch ( Application . CertificateManagerError err ) {
// Assume validation is now good, but report the error
// since the cert may not have been saved
context . tls_validation_failed = false ;
report_problem (
new Geary . ServiceProblemReport (
Geary . ProblemType . UNTRUSTED ,
context . account . information ,
service ,
err
)
) ;
}
2019-01-08 23:34:45 +11:00
context . tls_validation_prompting = false ;
update_account_status ( ) ;
}
2013-09-17 16:55:03 -07:00
private void on_account_email_removed ( Geary . Folder folder , Gee . Collection < Geary . EmailIdentifier > ids ) {
if ( folder . special_folder_type = = Geary . SpecialFolderType . OUTBOX ) {
main_window . status_bar . deactivate_message ( StatusBar . Message . OUTBOX_SEND_FAILURE ) ;
2014-01-29 18:18:31 -08:00
main_window . status_bar . deactivate_message ( StatusBar . Message . OUTBOX_SAVE_SENT_MAIL_FAILED ) ;
2013-09-17 16:55:03 -07:00
libnotify . clear_error_notification ( ) ;
}
}
private void on_sending_started ( ) {
main_window . status_bar . activate_message ( StatusBar . Message . OUTBOX_SENDING ) ;
}
private void on_sending_finished ( ) {
main_window . status_bar . deactivate_message ( StatusBar . Message . OUTBOX_SENDING ) ;
}
2018-02-21 16:43:56 +11:00
private async void connect_account_async ( Geary . Account account , Cancellable ? cancellable = null ) {
AccountContext context = new AccountContext ( account ) ;
// XXX Need to set this early since
// on_folders_available_unavailable expects it to be there
this . accounts . set ( account . information , context ) ;
account . email_sent . connect ( on_sent ) ;
account . email_removed . connect ( on_account_email_removed ) ;
2013-01-31 15:17:44 -08:00
account . folders_available_unavailable . connect ( on_folders_available_unavailable ) ;
2013-09-17 16:55:03 -07:00
account . sending_monitor . start . connect ( on_sending_started ) ;
account . sending_monitor . finish . connect ( on_sending_finished ) ;
2018-02-21 16:43:56 +11:00
2013-09-09 14:44:36 -07:00
bool retry = false ;
do {
try {
account . set_data ( PROP_ATTEMPT_OPEN_ACCOUNT , true ) ;
yield account . open_async ( cancellable ) ;
retry = false ;
} catch ( Error open_err ) {
debug ( " Unable to open account %s: %s " , account . to_string ( ) , open_err . message ) ;
2018-02-21 16:43:56 +11:00
2019-01-02 00:28:48 +11:00
if ( open_err is Geary . EngineError . CORRUPT ) {
2013-09-09 14:44:36 -07:00
retry = yield account_database_error_async ( account ) ;
2019-01-02 00:28:48 +11:00
}
2018-02-21 16:43:56 +11:00
if ( ! retry ) {
2019-01-02 00:28:48 +11:00
report_problem (
new Geary . AccountProblemReport (
Geary . ProblemType . GENERIC_ERROR ,
account . information ,
open_err
)
) ;
this . account_manager . disable_account ( account . information ) ;
2018-02-21 16:43:56 +11:00
this . accounts . unset ( account . information ) ;
}
2013-09-09 14:44:36 -07:00
}
} while ( retry ) ;
2018-02-21 16:43:56 +11:00
2013-01-31 15:17:44 -08:00
main_window . folder_list . set_user_folders_root_name ( account , _ ( " Labels " ) ) ;
2013-07-08 14:22:23 -07:00
display_main_window_if_ready ( ) ;
2019-01-24 18:48:30 +11:00
update_account_status ( ) ;
2013-01-31 15:17:44 -08:00
}
2019-01-02 00:28:48 +11:00
2013-09-09 14:44:36 -07:00
// Returns true if the caller should try opening the account again
private async bool account_database_error_async ( Geary . Account account ) {
bool retry = true ;
// give the user two options: reset the Account local store, or exit Geary. A third
// could be done to leave the Account in an unopened state, but we don't currently
// have provisions for that.
2016-10-07 15:03:12 +02:00
QuestionDialog dialog = new QuestionDialog ( main_window ,
2016-07-14 02:22:09 +10:00
_ ( " Unable to open the database for %s " ) . printf ( account . information . id ) ,
2013-09-09 14:44:36 -07:00
_ ( " There was an error opening the local mail database for this account. This is possibly due to corruption of the database file in this directory: \n \n %s \n \n Geary can rebuild the database and re-synchronize with the server or exit. \n \n Rebuilding the database will destroy all local email and its attachments. <b>The mail on the your server will not be affected.</b> " )
2016-07-12 15:55:27 +10:00
. printf ( account . information . data_dir . get_path ( ) ) ,
2013-09-09 14:44:36 -07:00
_ ( " _Rebuild " ) , _ ( " E_xit " ) ) ;
dialog . use_secondary_markup ( true ) ;
switch ( dialog . run ( ) ) {
case Gtk . ResponseType . OK :
// don't use Cancellable because we don't want to interrupt this process
try {
yield account . rebuild_async ( ) ;
} catch ( Error err ) {
2016-10-07 15:03:12 +02:00
ErrorDialog errdialog = new ErrorDialog ( main_window ,
2016-12-15 17:09:24 +01:00
_ ( " Unable to rebuild database for “%s” " ) . printf ( account . information . id ) ,
2013-09-09 14:44:36 -07:00
_ ( " Error during rebuild: \n \n %s " ) . printf ( err . message ) ) ;
2016-10-07 15:03:12 +02:00
errdialog . run ( ) ;
2013-09-09 14:44:36 -07:00
retry = false ;
}
break ;
default :
retry = false ;
break ;
}
2016-10-11 01:40:12 +11:00
2013-09-09 14:44:36 -07:00
return retry ;
}
2016-10-11 01:40:12 +11:00
2018-02-21 16:43:56 +11:00
private async void disconnect_account_async ( AccountContext context , Cancellable ? cancellable = null ) {
debug ( " Disconnecting account: %s " , context . account . information . id ) ;
Geary . Account account = context . account ;
// Guard against trying to disconnect the account twice
this . accounts . unset ( account . information ) ;
2019-01-08 10:00:00 +11:00
// Now the account is not in the accounts map, reset any
// status notifications for it
update_account_status ( ) ;
2018-02-21 16:43:56 +11:00
account . email_sent . disconnect ( on_sent ) ;
account . email_removed . disconnect ( on_account_email_removed ) ;
2013-01-31 15:17:44 -08:00
account . folders_available_unavailable . disconnect ( on_folders_available_unavailable ) ;
2013-09-17 16:55:03 -07:00
account . sending_monitor . start . disconnect ( on_sending_started ) ;
account . sending_monitor . finish . disconnect ( on_sending_finished ) ;
2018-02-21 16:43:56 +11:00
2013-01-31 15:17:44 -08:00
main_window . folder_list . remove_account ( account ) ;
2018-02-21 16:43:56 +11:00
context . cancellable . cancel ( ) ;
Geary . Folder ? inbox = context . inbox ;
if ( inbox ! = null ) {
2012-06-11 12:03:57 -07:00
try {
2018-02-21 16:43:56 +11:00
yield inbox . close_async ( cancellable ) ;
2013-01-31 15:17:44 -08:00
} catch ( Error close_inbox_err ) {
debug ( " Unable to close monitored inbox: %s " , close_inbox_err . message ) ;
2012-06-11 12:03:57 -07:00
}
2018-02-21 16:43:56 +11:00
context . inbox = null ;
2013-01-31 15:17:44 -08:00
}
2018-02-21 16:43:56 +11:00
2013-01-31 15:17:44 -08:00
try {
yield account . close_async ( cancellable ) ;
} catch ( Error close_err ) {
debug ( " Unable to close account %s: %s " , account . to_string ( ) , close_err . message ) ;
2012-06-04 12:39:46 -07:00
}
2018-02-21 16:43:56 +11:00
debug ( " Account closed: %s " , account . to_string ( ) ) ;
2013-04-02 15:37:06 -07:00
}
2018-02-21 16:43:56 +11:00
2013-07-08 14:22:23 -07:00
/ * *
* Returns true if we ' ve attempted to open all accounts at this point .
* /
private bool did_attempt_open_all_accounts ( ) {
try {
foreach ( Geary . AccountInformation info in Geary . Engine . instance . get_accounts ( ) . values ) {
Geary . Account a = Geary . Engine . instance . get_account_instance ( info ) ;
if ( a . get_data < bool ? > ( PROP_ATTEMPT_OPEN_ACCOUNT ) = = null )
return false ;
}
} catch ( Error e ) {
error ( " Could not open accounts: %s " , e . message ) ;
}
return true ;
}
/ * *
* Displays the main window if we ' re ready . Otherwise does nothing .
* /
private void display_main_window_if_ready ( ) {
2016-12-17 14:26:51 +11:00
if ( did_attempt_open_all_accounts ( ) & &
! upgrade_dialog . visible & &
! cancellable_open_account . is_cancelled ( ) & &
2017-03-03 19:47:58 +01:00
! Args . hidden_startup )
2018-02-07 14:49:59 +11:00
main_window . show ( ) ;
2013-07-08 14:22:23 -07:00
}
2013-06-07 16:23:45 -07:00
/ * *
* Returns the number of accounts that exist in Geary . Note that not all accounts may be
* open . Zero is returned on an error .
* /
public int get_num_accounts ( ) {
try {
return Geary . Engine . instance . get_accounts ( ) . size ;
} catch ( Error e ) {
debug ( " Error getting number of accounts: %s " , e . message ) ;
}
return 0 ; // on error
}
2016-06-02 10:35:43 +10:00
2018-01-27 16:22:45 +10:30
private bool is_inbox_descendant ( Geary . Folder target ) {
bool is_descendent = false ;
Geary . Account account = target . account ;
Geary . Folder ? inbox = null ;
try {
inbox = account . get_special_folder ( Geary . SpecialFolderType . INBOX ) ;
} catch ( Error err ) {
debug ( " Failed to get inbox for account %s " , account . information . id ) ;
}
if ( inbox ! = null ) {
is_descendent = inbox . path . is_descendant ( target . path ) ;
}
return is_descendent ;
}
2012-06-27 18:25:45 -07:00
// Update widgets and such to match capabilities of the current folder ... sensitivity is handled
// by other utility methods
private void update_ui ( ) {
2017-05-15 23:21:32 +02:00
main_window . main_toolbar . selected_conversations = this . selected_conversations . size ;
main_window . main_toolbar . show_trash_button = current_folder_supports_trash ( ) | |
! ( current_folder is Geary . FolderSupport . Remove ) ;
2012-06-27 18:25:45 -07:00
}
2016-06-02 10:35:43 +10:00
2011-11-22 18:55:07 -08:00
private void on_folder_selected ( Geary . Folder ? folder ) {
2013-02-04 19:26:48 -08:00
debug ( " Folder %s selected " , folder ! = null ? folder . to_string ( ) : " (null) " ) ;
2011-11-22 18:55:07 -08:00
if ( folder = = null ) {
2016-09-07 10:38:57 +10:00
this . current_folder = null ;
2016-08-19 12:03:40 +10:00
main_window . conversation_list_view . set_model ( null ) ;
2015-02-11 01:03:50 -05:00
main_window . main_toolbar . folder = null ;
2013-06-07 16:23:45 -07:00
folder_selected ( null ) ;
2016-09-07 10:38:57 +10:00
} else if ( folder ! = this . current_folder ) {
this . main_window . conversation_viewer . show_loading ( ) ;
2017-05-15 23:21:32 +02:00
get_window_action ( ACTION_FIND_IN_CONVERSATION ) . set_enabled ( false ) ;
2016-09-07 10:38:57 +10:00
enable_message_buttons ( false ) ;
2016-07-25 10:33:42 +10:00
2016-09-07 10:38:57 +10:00
// To prevent the user from selecting folders too quickly,
// we prevent additional selection changes to occur until
// after a timeout has expired from the last one
int64 now = get_monotonic_time ( ) ;
int64 diff = now - this . next_folder_select_allowed_usec ;
if ( diff < SELECT_FOLDER_TIMEOUT_USEC ) {
// only start timeout if another timeout is not
// running ... this means the user can click madly and
// will see the last clicked-on folder 100ms after the
// first one was clicked on
if ( this . select_folder_timeout_id = = 0 ) {
this . select_folder_timeout_id = Timeout . add (
( uint ) ( diff / 1000 ) ,
( ) = > {
this . select_folder_timeout_id = 0 ;
this . next_folder_select_allowed_usec = 0 ;
if ( folder ! = this . current_folder ) {
do_select_folder . begin (
folder , on_select_folder_completed
) ;
}
return false ;
} ) ;
}
} else {
do_select_folder . begin ( folder , on_select_folder_completed ) ;
this . next_folder_select_allowed_usec =
now + SELECT_FOLDER_TIMEOUT_USEC ;
}
2014-01-17 16:34:19 -08:00
}
2013-02-07 12:28:43 -08:00
}
2016-09-07 10:38:57 +10:00
2011-11-22 18:55:07 -08:00
private async void do_select_folder ( Geary . Folder folder ) throws Error {
2014-01-17 16:34:19 -08:00
debug ( " Switching to %s... " , folder . to_string ( ) ) ;
2015-01-26 17:07:10 -08:00
closed_folder ( ) ;
2013-03-18 13:19:39 -07:00
2013-02-07 12:28:43 -08:00
// This function is not reentrant. It should be, because it can be
// called reentrant-ly if you select folders quickly enough. This
// mutex lock is a bandaid solution to make the function safe to
// reenter.
2013-03-18 13:19:39 -07:00
int mutex_token = yield select_folder_mutex . claim_async ( cancellable_folder ) ;
2018-02-21 16:43:56 +11:00
2015-02-05 17:57:27 -08:00
// clear Revokable, as Undo is only available while a folder is selected
save_revokable ( null , null ) ;
2015-01-22 17:11:01 -08:00
// stop monitoring for conversations and close the folder
2012-05-01 20:32:31 -07:00
if ( current_conversations ! = null ) {
2015-01-22 17:11:01 -08:00
yield current_conversations . stop_monitoring_async ( null ) ;
2012-05-01 20:32:31 -07:00
current_conversations = null ;
2011-11-22 18:55:07 -08:00
}
2014-06-18 13:12:59 -07:00
// re-enable copy/move to the last selected folder
if ( current_folder ! = null ) {
main_window . main_toolbar . copy_folder_menu . enable_disable_folder ( current_folder , true ) ;
main_window . main_toolbar . move_folder_menu . enable_disable_folder ( current_folder , true ) ;
}
2018-02-21 16:43:56 +11:00
this . current_folder = folder ;
if ( this . current_account ! = folder . account ) {
this . current_account = folder . account ;
account_selected ( this . current_account ) ;
2013-09-30 18:29:14 -07:00
// If we were waiting for an account to be selected before issuing mailtos, do that now.
if ( pending_mailtos . size > 0 ) {
foreach ( string mailto in pending_mailtos )
compose_mailto ( mailto ) ;
pending_mailtos . clear ( ) ;
}
2018-09-12 11:49:22 +02:00
main_window . main_toolbar . copy_folder_menu . clear ( ) ;
main_window . main_toolbar . move_folder_menu . clear ( ) ;
foreach ( Geary . Folder f in current_folder . account . list_folders ( ) ) {
main_window . main_toolbar . copy_folder_menu . add_folder ( f ) ;
main_window . main_toolbar . move_folder_menu . add_folder ( f ) ;
}
2013-06-03 17:39:42 -07:00
}
2013-01-31 15:17:44 -08:00
2013-06-07 16:23:45 -07:00
folder_selected ( current_folder ) ;
2013-05-14 11:52:02 -07:00
2013-06-11 19:02:46 -07:00
if ( ! ( current_folder is Geary . SearchFolder ) )
previous_non_search_folder = current_folder ;
2014-06-18 13:12:59 -07:00
// disable copy/move to the new folder
2018-09-12 11:45:29 +02:00
main_window . main_toolbar . copy_folder_menu . enable_disable_folder ( current_folder , false ) ;
main_window . main_toolbar . move_folder_menu . enable_disable_folder ( current_folder , false ) ;
2014-06-18 13:12:59 -07:00
2012-06-27 18:25:45 -07:00
update_ui ( ) ;
2018-01-26 17:12:28 +10:30
current_conversations = new Geary . App . ConversationMonitor (
current_folder ,
Geary . Folder . OpenFlags . NO_DELAY ,
2019-01-16 14:57:11 +11:00
// Include fields for the conversation viewer as well so
// conversations can be displayed without having to go
// back to the db
ConversationListStore . REQUIRED_FIELDS |
ConversationListBox . REQUIRED_FIELDS |
ConversationEmail . REQUIRED_FOR_CONSTRUCT ,
Make ConversationMonitor more robust with no/changing connectivity.
Conversation monitor was built around older assumptions of how a folder's
remote connections work - that once a folder opens it will likely also
eventually establish a remote connection, that once the connection is up
it will hang around, and so on.
This patch removes any public notion of (re)seeding, since it can't be
relied to actually happen over the course of the session, ensures that
all folder operations are local-only when the folder does not have a
working remote connection so it doesn't block, and take the opportunity
to reorganise and clean up the monitor API and documentation comments.
* src/engine/app/app-conversation-monitor.vala (ConversationMonitor):
Remove seed signals, and don't bother running an initial reseed if the
folder is already open, since the fill operation will cause any locally
incomplete messages to be filled out from the report. Manage and use an
internal Cancellable for cancelling internal operations when shutting
down. Construct a queue only when starting to monitor conversations,
delete it when stopping. Move as much operation-specific code into the
operations themselves as reasonably possible, making some methods
internal so thy can be accessed from the ops. Ensure all folder listing
operations specify LOCAL_ONLY when the remote is not open. Removed
LocalLoadOperation since that is now redundant. Update the API for
accessing conversations to match Gee conventions and update call
sites. Update documentation comments. Hook back up to locally-complete
signals so we don't miss emails being filled out by the prefetcher, for
now.
* src/engine/app/conversation-monitor/app-conversation-set.vala
(ConversationSet): Rename conversations property to match Gee
conventions, update call sites.
* src/engine/app/conversation-monitor/app-conversation-operation.vala
(ConversationOperation): Allow operations to specify if they should
allow duplicates, and allow the execution method to throw errors, so
they can be handled in a uniform way.
* src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
(ConversationOperationQueue): Accept progress monitor property as a
ctor arg rather than constructing on itself, so it is tied to the
life-cycle of the ConversationMonitor rather than the queue. Add a
signal for notifying of errors thrown when running operations, and use
the new operation-independent support for determining if duplicates
should be queued.
* src/engine/app/conversation-monitor/app-fill-window-operation.vala
(FillWindowOperation): Enforce a maximum window size as well as minimum
to keep loading large windows semi-responsive. Remove code to handle
inserts now that they are handled by their own op.
* src/engine/app/conversation-monitor/app-insert-operation.vala
(InsertOperation): New operation to manage inserts, handle it them by
simply adding them to the conversation if they are newer than the
oldest message, rather that relisting all loaded messages.
2018-03-03 10:56:29 +11:00
MIN_CONVERSATION_COUNT
) ;
2018-01-26 17:12:28 +10:30
Make ConversationMonitor more robust with no/changing connectivity.
Conversation monitor was built around older assumptions of how a folder's
remote connections work - that once a folder opens it will likely also
eventually establish a remote connection, that once the connection is up
it will hang around, and so on.
This patch removes any public notion of (re)seeding, since it can't be
relied to actually happen over the course of the session, ensures that
all folder operations are local-only when the folder does not have a
working remote connection so it doesn't block, and take the opportunity
to reorganise and clean up the monitor API and documentation comments.
* src/engine/app/app-conversation-monitor.vala (ConversationMonitor):
Remove seed signals, and don't bother running an initial reseed if the
folder is already open, since the fill operation will cause any locally
incomplete messages to be filled out from the report. Manage and use an
internal Cancellable for cancelling internal operations when shutting
down. Construct a queue only when starting to monitor conversations,
delete it when stopping. Move as much operation-specific code into the
operations themselves as reasonably possible, making some methods
internal so thy can be accessed from the ops. Ensure all folder listing
operations specify LOCAL_ONLY when the remote is not open. Removed
LocalLoadOperation since that is now redundant. Update the API for
accessing conversations to match Gee conventions and update call
sites. Update documentation comments. Hook back up to locally-complete
signals so we don't miss emails being filled out by the prefetcher, for
now.
* src/engine/app/conversation-monitor/app-conversation-set.vala
(ConversationSet): Rename conversations property to match Gee
conventions, update call sites.
* src/engine/app/conversation-monitor/app-conversation-operation.vala
(ConversationOperation): Allow operations to specify if they should
allow duplicates, and allow the execution method to throw errors, so
they can be handled in a uniform way.
* src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
(ConversationOperationQueue): Accept progress monitor property as a
ctor arg rather than constructing on itself, so it is tied to the
life-cycle of the ConversationMonitor rather than the queue. Add a
signal for notifying of errors thrown when running operations, and use
the new operation-independent support for determining if duplicates
should be queued.
* src/engine/app/conversation-monitor/app-fill-window-operation.vala
(FillWindowOperation): Enforce a maximum window size as well as minimum
to keep loading large windows semi-responsive. Remove code to handle
inserts now that they are handled by their own op.
* src/engine/app/conversation-monitor/app-insert-operation.vala
(InsertOperation): New operation to manage inserts, handle it them by
simply adding them to the conversation if they are newer than the
oldest message, rather that relisting all loaded messages.
2018-03-03 10:56:29 +11:00
current_conversations . scan_completed . connect ( on_scan_completed ) ;
2011-11-22 18:55:07 -08:00
current_conversations . scan_error . connect ( on_scan_error ) ;
Make ConversationMonitor more robust with no/changing connectivity.
Conversation monitor was built around older assumptions of how a folder's
remote connections work - that once a folder opens it will likely also
eventually establish a remote connection, that once the connection is up
it will hang around, and so on.
This patch removes any public notion of (re)seeding, since it can't be
relied to actually happen over the course of the session, ensures that
all folder operations are local-only when the folder does not have a
working remote connection so it doesn't block, and take the opportunity
to reorganise and clean up the monitor API and documentation comments.
* src/engine/app/app-conversation-monitor.vala (ConversationMonitor):
Remove seed signals, and don't bother running an initial reseed if the
folder is already open, since the fill operation will cause any locally
incomplete messages to be filled out from the report. Manage and use an
internal Cancellable for cancelling internal operations when shutting
down. Construct a queue only when starting to monitor conversations,
delete it when stopping. Move as much operation-specific code into the
operations themselves as reasonably possible, making some methods
internal so thy can be accessed from the ops. Ensure all folder listing
operations specify LOCAL_ONLY when the remote is not open. Removed
LocalLoadOperation since that is now redundant. Update the API for
accessing conversations to match Gee conventions and update call
sites. Update documentation comments. Hook back up to locally-complete
signals so we don't miss emails being filled out by the prefetcher, for
now.
* src/engine/app/conversation-monitor/app-conversation-set.vala
(ConversationSet): Rename conversations property to match Gee
conventions, update call sites.
* src/engine/app/conversation-monitor/app-conversation-operation.vala
(ConversationOperation): Allow operations to specify if they should
allow duplicates, and allow the execution method to throw errors, so
they can be handled in a uniform way.
* src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
(ConversationOperationQueue): Accept progress monitor property as a
ctor arg rather than constructing on itself, so it is tied to the
life-cycle of the ConversationMonitor rather than the queue. Add a
signal for notifying of errors thrown when running operations, and use
the new operation-independent support for determining if duplicates
should be queued.
* src/engine/app/conversation-monitor/app-fill-window-operation.vala
(FillWindowOperation): Enforce a maximum window size as well as minimum
to keep loading large windows semi-responsive. Remove code to handle
inserts now that they are handled by their own op.
* src/engine/app/conversation-monitor/app-insert-operation.vala
(InsertOperation): New operation to manage inserts, handle it them by
simply adding them to the conversation if they are newer than the
oldest message, rather that relisting all loaded messages.
2018-03-03 10:56:29 +11:00
2013-09-06 11:32:58 -07:00
current_conversations . scan_completed . connect ( on_conversation_count_changed ) ;
current_conversations . conversations_added . connect ( on_conversation_count_changed ) ;
2016-10-20 16:27:43 +11:00
current_conversations . conversations_removed . connect ( on_conversation_count_changed ) ;
2018-02-21 16:43:56 +11:00
clear_new_messages ( " do_select_folder " , null ) ;
yield this . current_conversations . start_monitoring_async (
this . cancellable_folder
) ;
2013-02-07 12:28:43 -08:00
select_folder_mutex . release ( ref mutex_token ) ;
2014-01-17 16:34:19 -08:00
debug ( " Switched to %s " , folder . to_string ( ) ) ;
2011-11-22 18:55:07 -08:00
}
2016-09-07 10:38:57 +10:00
2013-09-06 11:32:58 -07:00
private void on_conversation_count_changed ( ) {
2016-07-25 10:33:42 +10:00
if ( this . current_conversations ! = null ) {
Make ConversationMonitor more robust with no/changing connectivity.
Conversation monitor was built around older assumptions of how a folder's
remote connections work - that once a folder opens it will likely also
eventually establish a remote connection, that once the connection is up
it will hang around, and so on.
This patch removes any public notion of (re)seeding, since it can't be
relied to actually happen over the course of the session, ensures that
all folder operations are local-only when the folder does not have a
working remote connection so it doesn't block, and take the opportunity
to reorganise and clean up the monitor API and documentation comments.
* src/engine/app/app-conversation-monitor.vala (ConversationMonitor):
Remove seed signals, and don't bother running an initial reseed if the
folder is already open, since the fill operation will cause any locally
incomplete messages to be filled out from the report. Manage and use an
internal Cancellable for cancelling internal operations when shutting
down. Construct a queue only when starting to monitor conversations,
delete it when stopping. Move as much operation-specific code into the
operations themselves as reasonably possible, making some methods
internal so thy can be accessed from the ops. Ensure all folder listing
operations specify LOCAL_ONLY when the remote is not open. Removed
LocalLoadOperation since that is now redundant. Update the API for
accessing conversations to match Gee conventions and update call
sites. Update documentation comments. Hook back up to locally-complete
signals so we don't miss emails being filled out by the prefetcher, for
now.
* src/engine/app/conversation-monitor/app-conversation-set.vala
(ConversationSet): Rename conversations property to match Gee
conventions, update call sites.
* src/engine/app/conversation-monitor/app-conversation-operation.vala
(ConversationOperation): Allow operations to specify if they should
allow duplicates, and allow the execution method to throw errors, so
they can be handled in a uniform way.
* src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
(ConversationOperationQueue): Accept progress monitor property as a
ctor arg rather than constructing on itself, so it is tied to the
life-cycle of the ConversationMonitor rather than the queue. Add a
signal for notifying of errors thrown when running operations, and use
the new operation-independent support for determining if duplicates
should be queued.
* src/engine/app/conversation-monitor/app-fill-window-operation.vala
(FillWindowOperation): Enforce a maximum window size as well as minimum
to keep loading large windows semi-responsive. Remove code to handle
inserts now that they are handled by their own op.
* src/engine/app/conversation-monitor/app-insert-operation.vala
(InsertOperation): New operation to manage inserts, handle it them by
simply adding them to the conversation if they are newer than the
oldest message, rather that relisting all loaded messages.
2018-03-03 10:56:29 +11:00
ConversationListView list = this . main_window . conversation_list_view ;
2017-10-10 17:09:13 -07:00
ConversationViewer viewer = this . main_window . conversation_viewer ;
Make ConversationMonitor more robust with no/changing connectivity.
Conversation monitor was built around older assumptions of how a folder's
remote connections work - that once a folder opens it will likely also
eventually establish a remote connection, that once the connection is up
it will hang around, and so on.
This patch removes any public notion of (re)seeding, since it can't be
relied to actually happen over the course of the session, ensures that
all folder operations are local-only when the folder does not have a
working remote connection so it doesn't block, and take the opportunity
to reorganise and clean up the monitor API and documentation comments.
* src/engine/app/app-conversation-monitor.vala (ConversationMonitor):
Remove seed signals, and don't bother running an initial reseed if the
folder is already open, since the fill operation will cause any locally
incomplete messages to be filled out from the report. Manage and use an
internal Cancellable for cancelling internal operations when shutting
down. Construct a queue only when starting to monitor conversations,
delete it when stopping. Move as much operation-specific code into the
operations themselves as reasonably possible, making some methods
internal so thy can be accessed from the ops. Ensure all folder listing
operations specify LOCAL_ONLY when the remote is not open. Removed
LocalLoadOperation since that is now redundant. Update the API for
accessing conversations to match Gee conventions and update call
sites. Update documentation comments. Hook back up to locally-complete
signals so we don't miss emails being filled out by the prefetcher, for
now.
* src/engine/app/conversation-monitor/app-conversation-set.vala
(ConversationSet): Rename conversations property to match Gee
conventions, update call sites.
* src/engine/app/conversation-monitor/app-conversation-operation.vala
(ConversationOperation): Allow operations to specify if they should
allow duplicates, and allow the execution method to throw errors, so
they can be handled in a uniform way.
* src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
(ConversationOperationQueue): Accept progress monitor property as a
ctor arg rather than constructing on itself, so it is tied to the
life-cycle of the ConversationMonitor rather than the queue. Add a
signal for notifying of errors thrown when running operations, and use
the new operation-independent support for determining if duplicates
should be queued.
* src/engine/app/conversation-monitor/app-fill-window-operation.vala
(FillWindowOperation): Enforce a maximum window size as well as minimum
to keep loading large windows semi-responsive. Remove code to handle
inserts now that they are handled by their own op.
* src/engine/app/conversation-monitor/app-insert-operation.vala
(InsertOperation): New operation to manage inserts, handle it them by
simply adding them to the conversation if they are newer than the
oldest message, rather that relisting all loaded messages.
2018-03-03 10:56:29 +11:00
int count = this . current_conversations . size ;
2016-07-25 10:33:42 +10:00
if ( count = = 0 ) {
// Let the user know if there's no available conversations
if ( this . current_folder is Geary . SearchFolder ) {
2017-10-10 17:09:13 -07:00
viewer . show_empty_search ( ) ;
2016-07-25 10:33:42 +10:00
} else {
2017-10-10 17:09:13 -07:00
viewer . show_empty_folder ( ) ;
2016-07-25 10:33:42 +10:00
}
2016-08-14 12:18:40 +10:00
enable_message_buttons ( false ) ;
2017-10-10 17:09:13 -07:00
} else {
// When not doing autoselect, we never get
2019-02-21 23:10:12 +11:00
// conversations_selected firing from the convo list,
// so we need to stop the loading spinner here. Only
// do so if there isn't already a selection or a
// composer to avoid interrupting those.
Make ConversationMonitor more robust with no/changing connectivity.
Conversation monitor was built around older assumptions of how a folder's
remote connections work - that once a folder opens it will likely also
eventually establish a remote connection, that once the connection is up
it will hang around, and so on.
This patch removes any public notion of (re)seeding, since it can't be
relied to actually happen over the course of the session, ensures that
all folder operations are local-only when the folder does not have a
working remote connection so it doesn't block, and take the opportunity
to reorganise and clean up the monitor API and documentation comments.
* src/engine/app/app-conversation-monitor.vala (ConversationMonitor):
Remove seed signals, and don't bother running an initial reseed if the
folder is already open, since the fill operation will cause any locally
incomplete messages to be filled out from the report. Manage and use an
internal Cancellable for cancelling internal operations when shutting
down. Construct a queue only when starting to monitor conversations,
delete it when stopping. Move as much operation-specific code into the
operations themselves as reasonably possible, making some methods
internal so thy can be accessed from the ops. Ensure all folder listing
operations specify LOCAL_ONLY when the remote is not open. Removed
LocalLoadOperation since that is now redundant. Update the API for
accessing conversations to match Gee conventions and update call
sites. Update documentation comments. Hook back up to locally-complete
signals so we don't miss emails being filled out by the prefetcher, for
now.
* src/engine/app/conversation-monitor/app-conversation-set.vala
(ConversationSet): Rename conversations property to match Gee
conventions, update call sites.
* src/engine/app/conversation-monitor/app-conversation-operation.vala
(ConversationOperation): Allow operations to specify if they should
allow duplicates, and allow the execution method to throw errors, so
they can be handled in a uniform way.
* src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
(ConversationOperationQueue): Accept progress monitor property as a
ctor arg rather than constructing on itself, so it is tied to the
life-cycle of the ConversationMonitor rather than the queue. Add a
signal for notifying of errors thrown when running operations, and use
the new operation-independent support for determining if duplicates
should be queued.
* src/engine/app/conversation-monitor/app-fill-window-operation.vala
(FillWindowOperation): Enforce a maximum window size as well as minimum
to keep loading large windows semi-responsive. Remove code to handle
inserts now that they are handled by their own op.
* src/engine/app/conversation-monitor/app-insert-operation.vala
(InsertOperation): New operation to manage inserts, handle it them by
simply adding them to the conversation if they are newer than the
oldest message, rather that relisting all loaded messages.
2018-03-03 10:56:29 +11:00
if ( ! this . application . config . autoselect & &
2019-02-21 23:10:12 +11:00
list . get_selection ( ) . count_selected_rows ( ) = = 0 & &
! viewer . is_composer_visible ) {
2017-10-10 17:09:13 -07:00
viewer . show_none_selected ( ) ;
enable_message_buttons ( false ) ;
}
2016-07-25 10:33:42 +10:00
}
conversation_count_changed ( count ) ;
}
2013-09-06 11:32:58 -07:00
}
2016-07-25 10:33:42 +10:00
2013-09-13 17:18:37 -07:00
private void on_libnotify_invoked ( Geary . Folder ? folder , Geary . Email ? email ) {
2013-02-12 16:17:16 -08:00
new_messages_monitor . clear_all_new_messages ( ) ;
2014-08-23 12:25:33 -04:00
if ( folder = = null | | email = = null | | ! can_switch_conversation_view ( ) )
2012-08-15 17:21:03 -07:00
return ;
2013-01-31 15:17:44 -08:00
main_window . folder_list . select_folder ( folder ) ;
Make ConversationMonitor more robust with no/changing connectivity.
Conversation monitor was built around older assumptions of how a folder's
remote connections work - that once a folder opens it will likely also
eventually establish a remote connection, that once the connection is up
it will hang around, and so on.
This patch removes any public notion of (re)seeding, since it can't be
relied to actually happen over the course of the session, ensures that
all folder operations are local-only when the folder does not have a
working remote connection so it doesn't block, and take the opportunity
to reorganise and clean up the monitor API and documentation comments.
* src/engine/app/app-conversation-monitor.vala (ConversationMonitor):
Remove seed signals, and don't bother running an initial reseed if the
folder is already open, since the fill operation will cause any locally
incomplete messages to be filled out from the report. Manage and use an
internal Cancellable for cancelling internal operations when shutting
down. Construct a queue only when starting to monitor conversations,
delete it when stopping. Move as much operation-specific code into the
operations themselves as reasonably possible, making some methods
internal so thy can be accessed from the ops. Ensure all folder listing
operations specify LOCAL_ONLY when the remote is not open. Removed
LocalLoadOperation since that is now redundant. Update the API for
accessing conversations to match Gee conventions and update call
sites. Update documentation comments. Hook back up to locally-complete
signals so we don't miss emails being filled out by the prefetcher, for
now.
* src/engine/app/conversation-monitor/app-conversation-set.vala
(ConversationSet): Rename conversations property to match Gee
conventions, update call sites.
* src/engine/app/conversation-monitor/app-conversation-operation.vala
(ConversationOperation): Allow operations to specify if they should
allow duplicates, and allow the execution method to throw errors, so
they can be handled in a uniform way.
* src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
(ConversationOperationQueue): Accept progress monitor property as a
ctor arg rather than constructing on itself, so it is tied to the
life-cycle of the ConversationMonitor rather than the queue. Add a
signal for notifying of errors thrown when running operations, and use
the new operation-independent support for determining if duplicates
should be queued.
* src/engine/app/conversation-monitor/app-fill-window-operation.vala
(FillWindowOperation): Enforce a maximum window size as well as minimum
to keep loading large windows semi-responsive. Remove code to handle
inserts now that they are handled by their own op.
* src/engine/app/conversation-monitor/app-insert-operation.vala
(InsertOperation): New operation to manage inserts, handle it them by
simply adding them to the conversation if they are newer than the
oldest message, rather that relisting all loaded messages.
2018-03-03 10:56:29 +11:00
Geary . App . Conversation ? conversation = current_conversations . get_by_email_identifier ( email . id ) ;
2012-08-15 17:21:03 -07:00
if ( conversation ! = null )
2012-09-07 20:01:12 -07:00
main_window . conversation_list_view . select_conversation ( conversation ) ;
2012-08-15 17:21:03 -07:00
}
2018-02-07 15:01:34 +11:00
2012-08-15 17:21:03 -07:00
private void on_indicator_activated_application ( uint32 timestamp ) {
2018-02-07 15:01:34 +11:00
this . application . present ( ) ;
2012-08-15 17:21:03 -07:00
}
2018-02-07 15:01:34 +11:00
2012-08-15 17:21:03 -07:00
private void on_indicator_activated_composer ( uint32 timestamp ) {
2016-05-08 18:36:49 +10:00
on_indicator_activated_application ( timestamp ) ;
2019-02-24 21:44:03 +11:00
compose ( ) ;
2012-08-15 17:21:03 -07:00
}
2019-02-24 21:44:03 +11:00
2013-02-12 16:17:16 -08:00
private void on_indicator_activated_inbox ( Geary . Folder folder , uint32 timestamp ) {
2016-05-08 18:36:49 +10:00
on_indicator_activated_application ( timestamp ) ;
2013-02-12 16:17:16 -08:00
main_window . folder_list . select_folder ( folder ) ;
2012-07-12 16:47:06 -07:00
}
2012-06-14 16:47:54 -07:00
2011-11-22 18:55:07 -08:00
private void on_load_more ( ) {
debug ( " on_load_more " ) ;
2013-05-20 15:50:15 -07:00
current_conversations . min_window_count + = MIN_CONVERSATION_COUNT ;
2011-11-22 18:55:07 -08:00
}
private void on_select_folder_completed ( Object ? source , AsyncResult result ) {
try {
do_select_folder . end ( result ) ;
} catch ( Error err ) {
debug ( " Unable to select folder: %s " , err . message ) ;
}
}
2016-07-25 10:33:42 +10:00
2013-08-19 16:39:53 -07:00
private void on_conversations_selected ( Gee . Set < Geary . App . Conversation > selected ) {
2016-08-14 12:18:40 +10:00
this . selected_conversations = selected ;
2017-05-15 23:21:32 +02:00
get_window_action ( ACTION_FIND_IN_CONVERSATION ) . set_enabled ( false ) ;
2016-08-14 12:18:40 +10:00
ConversationViewer viewer = this . main_window . conversation_viewer ;
if ( this . current_folder ! = null & & ! viewer . is_composer_visible ) {
switch ( selected . size ) {
case 0 :
enable_message_buttons ( false ) ;
viewer . show_none_selected ( ) ;
break ;
case 1 :
// Cancel existing avatar loads before loading new
// convo since that will start loading more avatars
2019-01-16 17:07:04 +11:00
Geary . App . Conversation convo = Geary . Collection . get_first (
selected
) ;
Geary . App . EmailStore ? store = get_store_for_folder (
convo . base_folder
2016-08-14 12:18:40 +10:00
) ;
2019-02-04 00:16:22 +11:00
// It's possible for a conversation with zero email to
// be selected, when it has just evaporated after its
// last email was removed but the conversation monitor
// hasn't signalled its removal yet. In this case,
// just don't load it since it will soon disappear.
if ( store ! = null & & convo . get_count ( ) > 0 ) {
2019-01-16 17:07:04 +11:00
viewer . load_conversation . begin (
convo ,
store ,
this . avatar_store ,
( obj , ret ) = > {
try {
viewer . load_conversation . end ( ret ) ;
enable_message_buttons ( true ) ;
get_window_action (
ACTION_FIND_IN_CONVERSATION
) . set_enabled ( true ) ;
2019-01-21 10:19:31 +10:30
} catch ( GLib . IOError . CANCELLED err ) {
// All good
2019-01-16 17:07:04 +11:00
} catch ( Error err ) {
debug ( " Unable to load conversation: %s " ,
err . message ) ;
}
}
) ;
}
2016-08-14 12:18:40 +10:00
break ;
default :
enable_multiple_message_buttons ( ) ;
viewer . show_multiple_selected ( ) ;
break ;
}
2016-04-09 18:06:41 +10:00
}
2011-11-22 18:55:07 -08:00
}
2016-06-21 14:48:24 +10:00
2013-08-19 16:39:53 -07:00
private void on_conversation_activated ( Geary . App . Conversation activated ) {
2013-07-19 15:44:20 -07:00
// Currently activating a conversation is only available for drafts folders.
if ( current_folder = = null | | current_folder . special_folder_type ! =
Geary . SpecialFolderType . DRAFTS )
return ;
2016-06-21 14:48:24 +10:00
2013-07-19 15:44:20 -07:00
// TODO: Determine how to map between conversations and drafts correctly.
2016-06-21 14:48:24 +10:00
Geary . Email draft = activated . get_latest_recv_email (
Geary . App . Conversation . Location . IN_FOLDER
) ;
create_compose_widget (
ComposerWidget . ComposeType . NEW_MESSAGE , draft , null , null , true
) ;
2013-07-19 15:44:20 -07:00
}
2016-06-21 14:48:24 +10:00
2018-01-27 16:22:45 +10:30
private void on_special_folder_type_changed ( Geary . Folder folder ,
Geary . SpecialFolderType old_type ,
Geary . SpecialFolderType new_type ) {
2018-05-24 11:25:30 +10:00
Geary . AccountInformation info = folder . account . information ;
2018-01-27 16:22:45 +10:30
// Update the main window
this . main_window . folder_list . remove_folder ( folder ) ;
this . main_window . folder_list . add_folder ( folder ) ;
2018-07-04 12:37:10 +10:00
// Since removing the folder will also remove its children
// from the folder list, we need to check for any and re-add
// them. See isssue #11.
try {
foreach ( Geary . Folder child in
folder . account . list_matching_folders ( folder . path ) ) {
main_window . folder_list . add_folder ( child ) ;
}
} catch ( Error err ) {
// Oh well
}
2018-01-27 16:22:45 +10:30
// Update notifications
this . new_messages_monitor . remove_folder ( folder ) ;
if ( folder . special_folder_type = = Geary . SpecialFolderType . INBOX | |
( folder . special_folder_type = = Geary . SpecialFolderType . NONE & &
is_inbox_descendant ( folder ) ) ) {
2018-02-21 16:43:56 +11:00
this . new_messages_monitor . add_folder (
2018-05-24 11:25:30 +10:00
folder , this . accounts . get ( info ) . cancellable
2018-02-21 16:43:56 +11:00
) ;
2013-04-01 12:50:49 -07:00
}
2013-02-14 16:01:51 -08:00
}
2018-01-27 16:22:45 +10:30
2019-02-19 16:39:11 +11:00
private void on_folders_available_unavailable (
Geary . Account account ,
Gee . BidirSortedSet < Geary . Folder > ? available ,
Gee . BidirSortedSet < Geary . Folder > ? unavailable ) {
2018-02-21 16:43:56 +11:00
AccountContext context = this . accounts . get ( account . information ) ;
2013-01-17 12:50:30 -08:00
if ( available ! = null & & available . size > 0 ) {
foreach ( Geary . Folder folder in available ) {
2017-03-02 12:35:39 +01:00
if ( ! should_add_folder ( available , folder ) ) {
continue ;
}
2012-06-22 14:42:51 -07:00
main_window . folder_list . add_folder ( folder ) ;
2013-01-31 15:17:44 -08:00
if ( folder . account = = current_account ) {
if ( ! main_window . main_toolbar . copy_folder_menu . has_folder ( folder ) )
main_window . main_toolbar . copy_folder_menu . add_folder ( folder ) ;
if ( ! main_window . main_toolbar . move_folder_menu . has_folder ( folder ) )
main_window . main_toolbar . move_folder_menu . add_folder ( folder ) ;
}
2018-01-26 17:12:28 +10:30
2018-02-21 16:43:56 +11:00
GLib . Cancellable cancellable = context . cancellable ;
2018-01-26 17:12:28 +10:30
2018-01-27 16:22:45 +10:30
switch ( folder . special_folder_type ) {
case Geary . SpecialFolderType . INBOX :
// Special case handling of inboxes
2018-02-21 16:43:56 +11:00
if ( context . inbox = = null ) {
context . inbox = folder ;
// Select this inbox if there isn't an
// existing folder selected and it is the
// inbox for the first account
if ( ! main_window . folder_list . is_any_selected ( ) ) {
Geary . AccountInformation ? first_account = null ;
foreach ( Geary . AccountInformation info in this . accounts . keys ) {
if ( first_account = = null | |
info . ordinal < first_account . ordinal ) {
first_account = info ;
}
}
if ( folder . account . information = = first_account ) {
// First we try to select the Inboxes branch inbox if
// it's there, falling back to the main folder list.
if ( ! main_window . folder_list . select_inbox ( folder . account ) )
main_window . folder_list . select_folder ( folder ) ;
}
2016-11-24 17:18:41 +01:00
}
2013-02-14 16:01:51 -08:00
}
2018-01-27 16:22:45 +10:30
folder . open_async . begin ( Geary . Folder . OpenFlags . NO_DELAY , cancellable ) ;
// Always notify for new messages in the Inbox
2018-02-07 15:03:23 +11:00
this . new_messages_monitor . add_folder ( folder , cancellable ) ;
2018-01-27 16:22:45 +10:30
break ;
2018-02-07 15:03:23 +11:00
2018-01-27 16:22:45 +10:30
case Geary . SpecialFolderType . NONE :
2018-02-07 15:03:23 +11:00
// Only notify for new messages in non-special
// descendants of the Inbox
2018-01-27 16:22:45 +10:30
if ( is_inbox_descendant ( folder ) ) {
this . new_messages_monitor . add_folder ( folder , cancellable ) ;
}
break ;
2012-05-22 13:44:57 -07:00
}
2018-02-21 16:43:56 +11:00
2012-06-22 14:42:51 -07:00
folder . special_folder_type_changed . connect ( on_special_folder_type_changed ) ;
2011-11-22 18:55:07 -08:00
}
}
2018-02-21 16:43:56 +11:00
2013-02-12 16:17:16 -08:00
if ( unavailable ! = null ) {
2019-02-19 16:39:11 +11:00
Gee . BidirIterator < Geary . Folder > unavailable_iterator =
unavailable . bidir_iterator ( ) ;
unavailable_iterator . last ( ) ;
2019-02-20 21:43:48 +11:00
while ( unavailable_iterator . previous ( ) ) {
2019-02-19 16:39:11 +11:00
Geary . Folder folder = unavailable_iterator . get ( ) ;
2014-02-14 16:03:19 -08:00
main_window . folder_list . remove_folder ( folder ) ;
if ( folder . account = = current_account ) {
if ( main_window . main_toolbar . copy_folder_menu . has_folder ( folder ) )
main_window . main_toolbar . copy_folder_menu . remove_folder ( folder ) ;
if ( main_window . main_toolbar . move_folder_menu . has_folder ( folder ) )
main_window . main_toolbar . move_folder_menu . remove_folder ( folder ) ;
}
2018-02-21 16:43:56 +11:00
switch ( folder . special_folder_type ) {
case Geary . SpecialFolderType . INBOX :
context . inbox = null ;
2013-02-12 16:17:16 -08:00
new_messages_monitor . remove_folder ( folder ) ;
2018-02-21 16:43:56 +11:00
break ;
case Geary . SpecialFolderType . NONE :
// Only notify for new messages in non-special
// descendants of the Inbox
if ( is_inbox_descendant ( folder ) ) {
this . new_messages_monitor . remove_folder ( folder ) ;
}
break ;
2013-02-12 16:17:16 -08:00
}
2018-02-21 16:43:56 +11:00
2014-02-14 16:03:19 -08:00
folder . special_folder_type_changed . disconnect ( on_special_folder_type_changed ) ;
2013-02-12 16:17:16 -08:00
}
}
2011-11-22 18:55:07 -08:00
}
2018-02-21 16:43:56 +11:00
2011-11-22 18:55:07 -08:00
private void cancel_folder ( ) {
Cancellable old_cancellable = cancellable_folder ;
cancellable_folder = new Cancellable ( ) ;
2012-06-14 16:47:54 -07:00
old_cancellable . cancel ( ) ;
}
2013-02-05 10:35:45 -08:00
2015-01-26 17:07:10 -08:00
// Like cancel_folder() but doesn't cancel outstanding operations, allowing them to complete
// in the background
private void closed_folder ( ) {
cancellable_folder = new Cancellable ( ) ;
}
2016-10-09 18:57:25 +11:00
2013-06-03 14:20:01 -07:00
private void cancel_search ( ) {
2016-10-09 18:57:25 +11:00
Cancellable old_cancellable = this . cancellable_search ;
this . cancellable_search = new Cancellable ( ) ;
2013-06-03 14:20:01 -07:00
old_cancellable . cancel ( ) ;
}
2016-10-09 18:57:25 +11:00
2013-08-29 12:37:31 -07:00
private void cancel_context_dependent_buttons ( ) {
Cancellable old_cancellable = cancellable_context_dependent_buttons ;
cancellable_context_dependent_buttons = new Cancellable ( ) ;
old_cancellable . cancel ( ) ;
}
2012-05-22 14:04:24 -07:00
// We need to include the second parameter, or valac doesn't recognize the function as matching
2013-12-06 16:43:11 -08:00
// GearyApplication.exiting's signature.
private bool on_application_exiting ( GearyApplication sender , bool panicked ) {
2012-05-22 14:04:24 -07:00
if ( close_composition_windows ( ) )
return true ;
return sender . cancel_exit ( ) ;
}
2016-04-27 11:56:25 +10:00
2014-01-07 15:04:49 -08:00
private void on_shift_key ( bool pressed ) {
2014-02-10 18:02:07 -08:00
if ( main_window ! = null & & main_window . main_toolbar ! = null
& & current_account ! = null & & current_folder ! = null ) {
2017-05-15 23:21:32 +02:00
main_window . main_toolbar . show_trash_button =
( ! pressed & & current_folder_supports_trash ( ) ) | |
! ( current_folder is Geary . FolderSupport . Remove ) ;
2014-02-10 18:02:07 -08:00
}
2014-01-07 15:04:49 -08:00
}
2016-06-02 10:35:43 +10:00
2012-09-27 19:22:34 -07:00
// this signal does not necessarily indicate that the application previously didn't have
// focus and now it does
private void on_has_toplevel_focus ( ) {
2013-03-19 14:03:54 -07:00
clear_new_messages ( " on_has_toplevel_focus " , null ) ;
2012-09-27 19:22:34 -07:00
}
2016-09-25 01:10:07 +10:00
2018-07-07 11:45:29 +10:00
// latest_sent_only uses Email's Date: field, which corresponds to
// how they're sorted in the ConversationViewer, not whether they
// are in the sent folder.
private Gee . Collection < Geary . EmailIdentifier > get_conversation_email_ids (
Gee . Collection < Geary . App . Conversation > conversations ,
bool latest_sent_only ) {
Gee . Collection < Geary . EmailIdentifier > ids =
new Gee . ArrayList < Geary . EmailIdentifier > ( ) ;
// Blacklist the Outbox unless that's currently selected since
// we don't want any operations to apply to messages there
// normally.
Gee . Collection < Geary . FolderPath > ? blacklist = null ;
if ( this . current_folder ! = null & &
this . current_folder . special_folder_type ! = Geary . SpecialFolderType . OUTBOX ) {
Geary . Folder ? outbox = null ;
try {
outbox = this . current_account . get_special_folder (
Geary . SpecialFolderType . OUTBOX
) ;
blacklist = new Gee . ArrayList < Geary . FolderPath > ( ) ;
blacklist . add ( outbox . path ) ;
} catch ( GLib . Error err ) {
// Oh well
}
2013-03-08 17:41:33 -08:00
}
2018-07-07 11:45:29 +10:00
foreach ( Geary . App . Conversation conversation in conversations ) {
if ( latest_sent_only ) {
Geary . Email ? latest = conversation . get_latest_sent_email (
Geary . App . Conversation . Location . IN_FOLDER_OUT_OF_FOLDER ,
blacklist
) ;
if ( latest ! = null ) {
ids . add ( latest . id ) ;
}
} else {
Geary . traverse < Geary . Email > (
conversation . get_emails (
Geary . App . Conversation . Ordering . NONE ,
Geary . App . Conversation . Location . ANYWHERE ,
blacklist
)
) . map < Geary . EmailIdentifier > ( e = > e . id )
. add_all_to ( ids ) ;
}
}
2013-08-19 16:39:53 -07:00
return ids ;
}
2018-07-07 11:45:29 +10:00
private Gee . Collection < Geary . EmailIdentifier >
get_selected_email_ids ( bool latest_sent_only ) {
return get_conversation_email_ids (
this . selected_conversations , latest_sent_only
) ;
}
2013-08-19 16:39:53 -07:00
private void mark_email ( Gee . Collection < Geary . EmailIdentifier > ids ,
Geary . EmailFlags ? flags_to_add , Geary . EmailFlags ? flags_to_remove ) {
2012-03-14 15:52:00 -07:00
if ( ids . size > 0 ) {
2019-01-15 12:19:22 +11:00
Geary . App . EmailStore ? store = get_store_for_folder ( current_folder ) ;
if ( store ! = null ) {
store . mark_email_async . begin (
ids , flags_to_add , flags_to_remove , cancellable_folder
) ;
}
2012-03-14 15:52:00 -07:00
}
}
2019-01-15 12:19:22 +11:00
2012-03-14 15:52:00 -07:00
private void on_show_mark_menu ( ) {
bool unread_selected = false ;
bool read_selected = false ;
2012-04-13 11:03:26 -07:00
bool starred_selected = false ;
bool unstarred_selected = false ;
2013-08-19 16:39:53 -07:00
foreach ( Geary . App . Conversation conversation in selected_conversations ) {
2012-07-31 10:55:10 +02:00
if ( conversation . is_unread ( ) )
2012-03-14 15:52:00 -07:00
unread_selected = true ;
2014-03-13 17:03:29 -07:00
// Only check the messages that "Mark as Unread" would mark, so we
// don't add the menu option and have it not do anything.
2015-02-02 17:40:31 -08:00
//
// Sort by Date: field to correspond with ConversationViewer ordering
Geary . Email ? latest = conversation . get_latest_sent_email (
2014-03-13 17:03:29 -07:00
Geary . App . Conversation . Location . IN_FOLDER_OUT_OF_FOLDER ) ;
if ( latest ! = null & & latest . email_flags ! = null
& & ! latest . email_flags . contains ( Geary . EmailFlags . UNREAD ) )
2012-03-14 15:52:00 -07:00
read_selected = true ;
2012-07-31 10:55:10 +02:00
2012-04-13 11:03:26 -07:00
if ( conversation . is_flagged ( ) ) {
starred_selected = true ;
} else {
unstarred_selected = true ;
2012-03-14 15:52:00 -07:00
}
}
2017-05-15 23:21:32 +02:00
get_window_action ( ACTION_MARK_AS_READ ) . set_enabled ( unread_selected ) ;
get_window_action ( ACTION_MARK_AS_UNREAD ) . set_enabled ( read_selected ) ;
get_window_action ( ACTION_MARK_AS_STARRED ) . set_enabled ( unstarred_selected ) ;
get_window_action ( ACTION_MARK_AS_UNSTARRED ) . set_enabled ( starred_selected ) ;
bool in_spam_folder = current_folder . special_folder_type = = Geary . SpecialFolderType . SPAM ;
get_window_action ( ACTION_MARK_AS_NOT_SPAM ) . set_enabled ( in_spam_folder ) ;
// If we're in Drafts/Outbox, we also shouldn't set a message as SPAM.
get_window_action ( ACTION_MARK_AS_SPAM ) . set_enabled ( ! in_spam_folder & &
current_folder . special_folder_type ! = Geary . SpecialFolderType . DRAFTS & &
current_folder . special_folder_type ! = Geary . SpecialFolderType . OUTBOX ) ;
2012-04-13 11:03:26 -07:00
}
2017-05-15 23:21:32 +02:00
2013-08-19 16:39:53 -07:00
private void on_visible_conversations_changed ( Gee . Set < Geary . App . Conversation > visible ) {
2013-03-19 14:03:54 -07:00
clear_new_messages ( " on_visible_conversations_changed " , visible ) ;
2012-09-27 19:22:34 -07:00
}
2013-03-19 11:47:40 -07:00
private bool should_notify_new_messages ( Geary . Folder folder ) {
2013-02-12 16:17:16 -08:00
// A monitored folder must be selected to squelch notifications;
2012-09-27 19:22:34 -07:00
// if conversation list is at top of display, don't display
// and don't display if main window has top-level focus
2013-03-19 11:47:40 -07:00
return folder ! = current_folder
2012-09-27 19:22:34 -07:00
| | main_window . conversation_list_view . vadjustment . value ! = 0.0
| | ! main_window . has_toplevel_focus ;
}
// Clears messages if conditions are true: anything in should_notify_new_messages() is
2013-03-19 14:03:54 -07:00
// false and the supplied visible messages are visible in the conversation list view
2013-08-19 16:39:53 -07:00
private void clear_new_messages ( string caller , Gee . Set < Geary . App . Conversation > ? supplied ) {
2013-03-19 14:03:54 -07:00
if ( current_folder = = null | | ! new_messages_monitor . get_folders ( ) . contains ( current_folder )
| | should_notify_new_messages ( current_folder ) )
2012-09-27 19:22:34 -07:00
return ;
2013-08-19 16:39:53 -07:00
Gee . Set < Geary . App . Conversation > visible =
2012-09-27 19:22:34 -07:00
supplied ? ? main_window . conversation_list_view . get_visible_conversations ( ) ;
2013-08-19 16:39:53 -07:00
foreach ( Geary . App . Conversation conversation in visible ) {
2013-03-19 14:03:54 -07:00
if ( new_messages_monitor . are_any_new_messages ( current_folder , conversation . get_email_ids ( ) ) ) {
2012-09-27 19:22:34 -07:00
debug ( " Clearing new messages: %s " , caller ) ;
2013-03-19 14:03:54 -07:00
new_messages_monitor . clear_new_messages ( current_folder ) ;
2012-09-27 19:22:34 -07:00
break ;
}
}
}
2018-07-07 11:45:29 +10:00
2013-08-26 14:56:46 -07:00
private void on_mark_conversations ( Gee . Collection < Geary . App . Conversation > conversations ,
Geary . EmailFlags ? flags_to_add , Geary . EmailFlags ? flags_to_remove ,
2014-03-10 11:52:54 -07:00
bool latest_only = false ) {
2018-07-07 11:45:29 +10:00
mark_email ( get_conversation_email_ids ( conversations , latest_only ) ,
2013-08-26 14:56:46 -07:00
flags_to_add , flags_to_remove ) ;
2012-04-13 11:03:26 -07:00
}
2018-07-07 11:45:29 +10:00
Reenable displaying sub-messages.
Geary currently displays RFC 822 attachments inline, below the email's
primary message body, using the same HTML chrome for the headers and
email body as for the primary body. Taking the same approach but using
GTK+ widgets meant splitting ConversationMessage up into a
ConversationEmail class that manages the UI for displaying an email in
its entirety, and a ConversationMessage to manage the only header widgets
and webview for displaying an individual RFC 822 message, usable for both
the primary body and any sub-messages. Thus, this is a big change.
One behavioural change is that each sub-message with remote images now
requires individual approval, rather than being dependant on the
containing message's sender and/or approval. This prevents some attacks
e.g. a trusted sender forwarding a spam/malware message, but does not
prevent it if the message is forwarded inline, obviosuly.
* src/client/conversation-viewer/conversation-email.vala (ConversationEmail):
New class for managing the UI for an overall email message. This
replaces the old ConversationMessage and contains much of it's code and
widgets - anything from that class which does not directly support
displaying headers or a message body.
* src/client/conversation-viewer/conversation-message.vala:
(ConversationMessage): Same class as before, but now with its scope
narrowed to only display message headers and body. The draft infobar
remains here rather than being put ConversationEmail where it belongs
since it's bit of a pain to insert in the right place and doesn't
really hurt.
(::email): Moved this property and any code that depends on it to
ConversationEmail.
(::always_load_remote_images): New property passed in via the ctor,
allowing one dependency on the old ::email property to be removed.
(::inlined_content_ids): Moved to ConversationEmail, since that is the
class that keeps track of attachments to display. Add the signal
attachment_displayed_inline to allow ConversationEmail to be notified
of inlined attachments instead.
(::flag_remote_images, ::remember_remote_images): New signals to notify
ConversationEmail that the user has flagged this message or the
message's sender for loading remote images. This is passed through
since in the former's case we may need to set flags on the email
itself, the latter because it is one less use of the contact_store
property, which should be removed from this class at some point.
* src/client/conversation-viewer/conversation-viewer.vala: Chase API
changes from the above. In general, replace use of the term "message"
with "email" since this class is now mostly dealing with
ConversationEmail instances, rather than ConversationMessage instances.
(ConversationViewer::check_mark_read): Only consider the
ConversationEmail's primary message body when checking for visibility
rather than that and any submessages to keep things simple.
(ConversationViewer::show_message, ::hide_message): Renamed to
expand_email/collapse_email respectively since we don't ever actually
hide it. Carry that change on to same methods on ConversationEmail.
* src/engine/rfc822/rfc822-message.vala (Geary.RFC822.Message): Add
get_primary_originator(), almost vermatim from Geary.Email, to support
determining the sender for remembering remote message loading for
senders of sub-emails.
* src/client/components/main-window.vala (MainWindow::set_styling): Fix
background transition for collapsed emails.
* src/client/application/geary-controller.vala: Chase API name changes.
* src/CMakeLists.txt: Include new ConversationEmail source file.
* ui/conversation-email.ui: New UI for ConversationEmail, move the email
action box, attachments box amd sub-messages box here from
conversation-message.ui.
* ui/CMakeLists.txt: Include new UI in compiled resources.
* po/POTFILES.in: Add new UI for transation.
2016-04-19 16:52:34 +10:00
private void on_conversation_viewer_mark_emails ( Gee . Collection < Geary . EmailIdentifier > emails ,
2013-08-19 16:39:53 -07:00
Geary . EmailFlags ? flags_to_add , Geary . EmailFlags ? flags_to_remove ) {
mark_email ( emails , flags_to_add , flags_to_remove ) ;
2012-03-14 15:52:00 -07:00
}
2017-05-15 23:21:32 +02:00
private void on_mark_as_read ( SimpleAction action ) {
2012-03-14 15:52:00 -07:00
Geary . EmailFlags flags = new Geary . EmailFlags ( ) ;
flags . add ( Geary . EmailFlags . UNREAD ) ;
2017-05-15 23:21:32 +02:00
2018-07-07 11:45:29 +10:00
Gee . Collection < Geary . EmailIdentifier > ids = get_selected_email_ids ( false ) ;
2013-08-19 16:39:53 -07:00
mark_email ( ids , null , flags ) ;
2016-07-25 10:33:42 +10:00
ConversationListBox ? list =
main_window . conversation_viewer . current_list ;
if ( list ! = null ) {
foreach ( Geary . EmailIdentifier id in ids )
list . mark_manual_read ( id ) ;
}
2012-03-14 15:52:00 -07:00
}
2017-05-15 23:21:32 +02:00
private void on_mark_as_unread ( SimpleAction action ) {
2012-03-14 15:52:00 -07:00
Geary . EmailFlags flags = new Geary . EmailFlags ( ) ;
flags . add ( Geary . EmailFlags . UNREAD ) ;
2017-05-15 23:21:32 +02:00
2018-07-07 11:45:29 +10:00
Gee . Collection < Geary . EmailIdentifier > ids = get_selected_email_ids ( true ) ;
2013-08-19 16:39:53 -07:00
mark_email ( ids , flags , null ) ;
2016-07-25 10:33:42 +10:00
ConversationListBox ? list =
main_window . conversation_viewer . current_list ;
if ( list ! = null ) {
foreach ( Geary . EmailIdentifier id in ids )
list . mark_manual_unread ( id ) ;
}
2012-03-14 15:52:00 -07:00
}
2012-04-13 11:03:26 -07:00
2017-05-15 23:21:32 +02:00
private void on_mark_as_starred ( SimpleAction action ) {
2012-04-13 11:03:26 -07:00
Geary . EmailFlags flags = new Geary . EmailFlags ( ) ;
flags . add ( Geary . EmailFlags . FLAGGED ) ;
2013-08-19 16:39:53 -07:00
mark_email ( get_selected_email_ids ( true ) , flags , null ) ;
2012-04-13 11:03:26 -07:00
}
2017-05-15 23:21:32 +02:00
private void on_mark_as_unstarred ( SimpleAction action ) {
2012-04-13 11:03:26 -07:00
Geary . EmailFlags flags = new Geary . EmailFlags ( ) ;
flags . add ( Geary . EmailFlags . FLAGGED ) ;
2013-08-19 16:39:53 -07:00
mark_email ( get_selected_email_ids ( false ) , null , flags ) ;
2012-04-13 11:03:26 -07:00
}
2017-05-15 23:21:32 +02:00
private void on_show_move_menu ( SimpleAction ? action ) {
this . main_window . main_toolbar . copy_message_button . clicked ( ) ;
}
private void on_show_copy_menu ( SimpleAction ? action ) {
this . main_window . main_toolbar . move_message_button . clicked ( ) ;
}
private async void mark_as_spam_toggle_async ( Cancellable ? cancellable ) {
2013-02-21 14:57:59 -08:00
Geary . Folder ? destination_folder = null ;
2013-06-26 15:02:33 -07:00
if ( current_folder . special_folder_type ! = Geary . SpecialFolderType . SPAM ) {
2013-02-21 14:57:59 -08:00
// Move to spam folder.
try {
2014-02-11 15:24:01 -08:00
destination_folder = yield current_account . get_required_special_folder_async (
Geary . SpecialFolderType . SPAM , cancellable ) ;
2013-02-21 14:57:59 -08:00
} catch ( Error e ) {
debug ( " Error getting spam folder: %s " , e . message ) ;
}
} else {
// Move out of spam folder, back to inbox.
try {
destination_folder = current_account . get_special_folder ( Geary . SpecialFolderType . INBOX ) ;
} catch ( Error e ) {
debug ( " Error getting inbox folder: %s " , e . message ) ;
}
}
2017-05-15 23:21:32 +02:00
2013-02-21 14:57:59 -08:00
if ( destination_folder ! = null )
on_move_conversation ( destination_folder ) ;
}
2017-05-15 23:21:32 +02:00
private void on_mark_as_spam_toggle ( SimpleAction action ) {
mark_as_spam_toggle_async . begin ( null ) ;
2014-02-11 15:24:01 -08:00
}
2017-05-15 23:21:32 +02:00
2013-08-19 16:39:53 -07:00
private void copy_email ( Gee . Collection < Geary . EmailIdentifier > ids ,
Geary . FolderPath destination ) {
if ( ids . size > 0 ) {
2019-01-15 12:19:22 +11:00
Geary . App . EmailStore ? store = get_store_for_folder ( current_folder ) ;
if ( store ! = null ) {
store . copy_email_async . begin (
ids , destination , cancellable_folder
) ;
}
2013-08-19 16:39:53 -07:00
}
}
2018-02-21 16:43:56 +11:00
2012-05-22 13:44:57 -07:00
private void on_copy_conversation ( Geary . Folder destination ) {
2013-08-19 16:39:53 -07:00
copy_email ( get_selected_email_ids ( false ) , destination . path ) ;
2012-05-22 13:44:57 -07:00
}
2013-07-11 12:50:19 -07:00
2012-05-22 13:44:57 -07:00
private void on_move_conversation ( Geary . Folder destination ) {
// Nothing to do if nothing selected.
2012-07-02 14:53:08 -07:00
if ( selected_conversations = = null | | selected_conversations . size = = 0 )
2012-05-22 13:44:57 -07:00
return ;
2012-06-27 18:25:45 -07:00
2018-07-07 11:45:29 +10:00
Gee . Collection < Geary . EmailIdentifier > ids = get_selected_email_ids ( false ) ;
2012-06-27 18:25:45 -07:00
if ( ids . size = = 0 )
return ;
2016-10-20 16:27:43 +11:00
2018-07-02 23:51:13 +10:00
selection_operation_started ( ) ;
2016-10-20 16:27:43 +11:00
2013-04-26 17:58:09 -07:00
Geary . FolderSupport . Move ? supports_move = current_folder as Geary . FolderSupport . Move ;
2015-02-05 17:57:27 -08:00
if ( supports_move ! = null )
2016-10-20 16:27:43 +11:00
move_conversation_async . begin (
supports_move , ids , destination . path , cancellable_folder ,
( obj , ret ) = > {
move_conversation_async . end ( ret ) ;
2018-07-02 23:51:13 +10:00
selection_operation_finished ( ) ;
2016-10-20 16:27:43 +11:00
} ) ;
2015-02-05 17:57:27 -08:00
}
2016-10-20 16:27:43 +11:00
2015-02-05 17:57:27 -08:00
private async void move_conversation_async ( Geary . FolderSupport . Move source_folder ,
2018-07-07 11:45:29 +10:00
Gee . Collection < Geary . EmailIdentifier > ids ,
Geary . FolderPath destination ,
Cancellable ? cancellable ) {
2015-02-05 17:57:27 -08:00
try {
save_revokable ( yield source_folder . move_email_async ( ids , destination , cancellable ) ,
_ ( " Undo move (Ctrl+Z) " ) ) ;
} catch ( Error err ) {
debug ( " %s: Unable to move %d emails: %s " , source_folder . to_string ( ) , ids . size ,
err . message ) ;
}
2012-05-22 13:44:57 -07:00
}
2016-04-18 11:01:46 +10:00
2016-08-16 18:08:21 +10:00
private void on_attachments_activated ( Gee . Collection < Geary . Attachment > attachments ) {
2016-10-11 01:40:12 +11:00
if ( this . application . config . ask_open_attachment ) {
2012-09-04 17:50:18 -07:00
QuestionDialog ask_to_open = new QuestionDialog . with_checkbox ( main_window ,
2016-04-23 17:02:32 +10:00
_ ( " Are you sure you want to open these attachments? " ) ,
2012-09-04 17:50:18 -07:00
_ ( " Attachments may cause damage to your system if opened. Only open files from trusted sources. " ) ,
2016-12-15 17:09:24 +01:00
Stock . _OPEN_BUTTON , Stock . _CANCEL , _ ( " Don’ t _ask me again " ) , false ) ;
2016-04-23 17:02:32 +10:00
if ( ask_to_open . run ( ) ! = Gtk . ResponseType . OK ) {
2012-09-04 17:50:18 -07:00
return ;
2016-04-23 17:02:32 +10:00
}
2012-09-04 17:50:18 -07:00
// only save checkbox state if OK was selected
2016-10-11 01:40:12 +11:00
this . application . config . ask_open_attachment = ! ask_to_open . is_checked ;
2012-09-04 17:50:18 -07:00
}
2016-04-18 11:01:46 +10:00
2016-08-16 18:08:21 +10:00
foreach ( Geary . Attachment attachment in attachments ) {
2017-02-27 11:39:29 +11:00
string uri = attachment . file . get_uri ( ) ;
try {
this . application . show_uri ( uri ) ;
} catch ( Error err ) {
message ( " Unable to open attachment \" %s \" : %s " , uri , err . message ) ;
2016-04-23 17:02:32 +10:00
}
2013-09-27 18:29:23 -07:00
}
2012-06-08 12:39:27 -07:00
}
2016-04-23 17:02:32 +10:00
2017-02-16 08:58:58 +11:00
private async void save_attachment_to_file ( Geary . Attachment attachment ,
2018-10-24 02:14:19 +11:00
string ? alt_text ,
GLib . Cancellable cancellable ) {
string alt_display_name = Geary . String . is_empty_or_whitespace ( alt_text )
? GearyController . untitled_file_name : alt_text ;
string display_name = yield attachment . get_safe_file_name (
alt_display_name
) ;
Geary . Memory . FileBuffer ? content = null ;
2017-02-11 11:34:30 +11:00
try {
2018-10-24 02:14:19 +11:00
content = new Geary . Memory . FileBuffer ( attachment . file , true ) ;
} catch ( GLib . Error err ) {
warning (
" Error opening attachment file \" %s \" : %s " ,
attachment . file . get_uri ( ) , err . message
) ;
report_problem (
new Geary . ProblemReport ( Geary . ProblemType . GENERIC_ERROR , err )
2017-02-11 11:34:30 +11:00
) ;
}
2018-10-24 02:14:19 +11:00
yield this . prompt_save_buffer ( display_name , content , cancellable ) ;
2012-08-28 18:17:28 -07:00
}
2016-04-23 17:02:32 +10:00
2018-10-24 02:14:19 +11:00
private async void
save_attachments_to_file ( Gee . Collection < Geary . Attachment > attachments ,
GLib . Cancellable ? cancellable ) {
2017-02-11 11:34:30 +11:00
Gtk . FileChooserNative dialog = new_save_chooser ( Gtk . FileChooserAction . SELECT_FOLDER ) ;
2012-08-28 18:17:28 -07:00
bool accepted = ( dialog . run ( ) = = Gtk . ResponseType . ACCEPT ) ;
2013-09-27 18:28:19 -07:00
string ? filename = dialog . get_filename ( ) ;
2012-06-08 12:39:27 -07:00
dialog . destroy ( ) ;
2013-09-27 18:28:19 -07:00
if ( ! accepted | | Geary . String . is_empty ( filename ) )
2012-08-28 18:17:28 -07:00
return ;
2017-02-11 11:34:30 +11:00
2018-10-24 02:14:19 +11:00
File dest_dir = File . new_for_path ( filename ) ;
2012-08-28 18:17:28 -07:00
foreach ( Geary . Attachment attachment in attachments ) {
2018-10-24 02:14:19 +11:00
Geary . Memory . FileBuffer ? content = null ;
GLib . File ? dest = null ;
2017-02-11 11:34:30 +11:00
try {
2018-10-24 02:14:19 +11:00
content = new Geary . Memory . FileBuffer ( attachment . file , true ) ;
dest = dest_dir . get_child_for_display_name (
yield attachment . get_safe_file_name (
GearyController . untitled_file_name
)
2017-02-11 11:34:30 +11:00
) ;
2018-10-24 02:14:19 +11:00
} catch ( GLib . Error err ) {
warning (
" Error opening attachment files \" %s \" : %s " ,
attachment . file . get_uri ( ) , err . message
) ;
report_problem (
new Geary . ProblemReport (
Geary . ProblemType . GENERIC_ERROR , err
)
2017-02-11 11:34:30 +11:00
) ;
}
2018-10-24 02:14:19 +11:00
if ( content ! = null & &
dest ! = null & &
yield check_overwrite ( dest , cancellable ) ) {
yield write_buffer_to_file ( content , dest , cancellable ) ;
}
2012-06-08 12:39:27 -07:00
}
}
2017-02-11 11:34:30 +11:00
2018-10-24 02:14:19 +11:00
private async void prompt_save_buffer ( string display_name ,
Geary . Memory . Buffer buffer ,
GLib . Cancellable ? cancellable ) {
Gtk . FileChooserNative dialog = new_save_chooser (
Gtk . FileChooserAction . SAVE
) ;
dialog . set_current_name ( display_name ) ;
2017-02-11 11:34:30 +11:00
2018-10-24 02:14:19 +11:00
string ? accepted_path = null ;
if ( dialog . run ( ) = = Gtk . ResponseType . ACCEPT ) {
accepted_path = dialog . get_filename ( ) ;
}
2013-09-11 17:53:17 -07:00
dialog . destroy ( ) ;
2017-02-11 11:34:30 +11:00
2018-10-24 02:14:19 +11:00
if ( ! Geary . String . is_empty_or_whitespace ( accepted_path ) ) {
GLib . File dest_file = File . new_for_path ( accepted_path ) ;
if ( yield check_overwrite ( dest_file , cancellable ) ) {
yield write_buffer_to_file ( buffer , dest_file , cancellable ) ;
}
2013-09-11 17:53:17 -07:00
}
}
2017-02-11 11:34:30 +11:00
2018-10-24 02:14:19 +11:00
private async bool check_overwrite ( GLib . File to_overwrite ,
GLib . Cancellable ? cancellable ) {
bool overwrite = true ;
try {
GLib . FileInfo file_info = yield to_overwrite . query_info_async (
GLib . FileAttribute . STANDARD_DISPLAY_NAME ,
GLib . FileQueryInfoFlags . NONE ,
GLib . Priority . DEFAULT ,
cancellable
) ;
GLib . FileInfo parent_info = yield to_overwrite . get_parent ( )
. query_info_async (
GLib . FileAttribute . STANDARD_DISPLAY_NAME ,
GLib . FileQueryInfoFlags . NONE ,
GLib . Priority . DEFAULT ,
cancellable
) ;
2017-02-11 11:34:30 +11:00
2018-10-24 02:14:19 +11:00
// Translators: Dialog primary label when prompting to
// overwrite a file. The string substitution is the file'sx
// name.
string primary = _ (
" A file named “%s” already exists. Do you want to replace it? "
) . printf ( file_info . get_display_name ( ) ) ;
// Translators: Dialog secondary label when prompting to
// overwrite a file. The string substitution is the parent
// folder's name.
string secondary = _ (
" The file already exists in “%s”. Replacing it will overwrite its contents. "
) . printf ( parent_info . get_display_name ( ) ) ;
ConfirmationDialog dialog = new ConfirmationDialog (
main_window , primary , secondary , _ ( " _Replace " ) , " destructive-action "
) ;
overwrite = ( dialog . run ( ) = = Gtk . ResponseType . OK ) ;
} catch ( GLib . Error err ) {
// Oh well
}
return overwrite ;
}
2017-02-11 11:34:30 +11:00
2018-10-24 02:14:19 +11:00
private async void write_buffer_to_file ( Geary . Memory . Buffer buffer ,
File dest ,
GLib . Cancellable ? cancellable ) {
try {
FileOutputStream outs = dest . replace (
null , false , FileCreateFlags . REPLACE_DESTINATION , cancellable
) ;
yield outs . splice_async (
buffer . get_input_stream ( ) ,
OutputStreamSpliceFlags . CLOSE_SOURCE | OutputStreamSpliceFlags . CLOSE_TARGET ,
Priority . DEFAULT ,
cancellable
) ;
} catch ( GLib . IOError . CANCELLED err ) {
try {
yield dest . delete_async ( GLib . Priority . HIGH , null ) ;
} catch ( GLib . Error err ) {
// Oh well
}
} catch ( GLib . Error err ) {
warning (
" Error writing buffer \" %s \" : %s " ,
dest . get_uri ( ) , err . message
) ;
report_problem (
new Geary . ProblemReport ( Geary . ProblemType . GENERIC_ERROR , err )
) ;
}
2017-02-11 11:34:30 +11:00
}
private inline Gtk . FileChooserNative new_save_chooser ( Gtk . FileChooserAction action ) {
Gtk . FileChooserNative dialog = new Gtk . FileChooserNative (
null ,
this . main_window ,
action ,
Stock . _SAVE ,
Stock . _CANCEL
) ;
dialog . set_local_only ( false ) ;
return dialog ;
}
2012-01-20 17:31:56 -08:00
// Opens a link in an external browser.
2013-09-27 18:29:23 -07:00
private bool open_uri ( string _link ) {
2012-01-25 11:43:51 -08:00
string link = _link ;
2017-02-27 11:36:17 +11:00
2012-01-25 11:43:51 -08:00
// Support web URLs that ommit the protocol.
if ( ! link . contains ( " : " ) )
link = " http:// " + link ;
2017-02-27 11:36:17 +11:00
bool success = true ;
2011-11-22 18:55:07 -08:00
try {
2017-02-27 11:36:17 +11:00
this . application . show_uri ( link ) ;
2011-11-22 18:55:07 -08:00
} catch ( Error err ) {
2017-02-27 11:36:17 +11:00
success = false ;
debug ( " Unable to open URL: \" %s \" %s " , link , err . message ) ;
2011-11-22 18:55:07 -08:00
}
2017-02-27 11:36:17 +11:00
return success ;
2011-11-22 18:55:07 -08:00
}
2016-12-17 14:35:36 +11:00
internal bool close_composition_windows ( bool main_window_only = false ) {
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Basic inline composition
Many of the details don't work, or don't work well, but the basics are
in place.
Allow only a single inline composition at a time
With this, we introduce a dialog when you would try to add another. We
also use this when changing the selected conversation with a composer
open.
Compose new messages inline, with no conversation selected
Hook up composer accelerators only when focus is in composer editor
It would be nice to only activate these accelerators when the composer
has focus generally, but that doesn't seem to be easy to detect.
Only disconnect accelerators if they're connected
Maintain focus when composer is popped out
The selection isn't, though.
Fix Tab focus for embedded composer
There are two things that needed to be fixed: The tab key doesn't
usually advance focus for embedded widgets (huh?), so we handle tab
presses by hand for ComposerWidgets. Also, the EmailEntrys do their own
tab handling, which needs to know about the composer widget, not the
toplevel widget in the embedded case.
Remove close() from ComposerContainer interface
I don't think it was actually doing anything, and it conflicts with the new close() method of Gtk.Window.
2014-02-11 00:26:14 -05:00
Gee . List < ComposerWidget > composers_to_destroy = new Gee . ArrayList < ComposerWidget > ( ) ;
2013-08-08 18:47:23 -07:00
bool quit_cancelled = false ;
2016-12-17 14:35:36 +11:00
// If there's composer windows open, give the user a chance to
// save or cancel.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Basic inline composition
Many of the details don't work, or don't work well, but the basics are
in place.
Allow only a single inline composition at a time
With this, we introduce a dialog when you would try to add another. We
also use this when changing the selected conversation with a composer
open.
Compose new messages inline, with no conversation selected
Hook up composer accelerators only when focus is in composer editor
It would be nice to only activate these accelerators when the composer
has focus generally, but that doesn't seem to be easy to detect.
Only disconnect accelerators if they're connected
Maintain focus when composer is popped out
The selection isn't, though.
Fix Tab focus for embedded composer
There are two things that needed to be fixed: The tab key doesn't
usually advance focus for embedded widgets (huh?), so we handle tab
presses by hand for ComposerWidgets. Also, the EmailEntrys do their own
tab handling, which needs to know about the composer widget, not the
toplevel widget in the embedded case.
Remove close() from ComposerContainer interface
I don't think it was actually doing anything, and it conflicts with the new close() method of Gtk.Window.
2014-02-11 00:26:14 -05:00
foreach ( ComposerWidget cw in composer_widgets ) {
2016-12-17 14:35:36 +11:00
if ( ! main_window_only | |
cw . state ! = ComposerWidget . ComposerState . DETACHED ) {
// Check if we should close the window immediately, or
// if we need to wait.
ComposerWidget . CloseStatus status = cw . should_close ( ) ;
if ( status = = ComposerWidget . CloseStatus . PENDING_CLOSE ) {
// Window is currently busy saving.
waiting_to_close . add ( cw ) ;
} else if ( status = = ComposerWidget . CloseStatus . CANCEL_CLOSE ) {
// User cancelled operation.
quit_cancelled = true ;
break ;
} else if ( status = = ComposerWidget . CloseStatus . DO_CLOSE ) {
// Hide any existing composer windows for the
// moment; actually deleting the windows will
// result in their removal from composer_windows,
// which could crash this loop.
composers_to_destroy . add ( cw ) ;
( ( ComposerContainer ) cw . parent ) . vanish ( ) ;
}
2013-08-08 18:47:23 -07:00
}
}
2016-12-17 14:35:36 +11:00
2013-08-08 18:47:23 -07:00
// Safely destroy windows.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Basic inline composition
Many of the details don't work, or don't work well, but the basics are
in place.
Allow only a single inline composition at a time
With this, we introduce a dialog when you would try to add another. We
also use this when changing the selected conversation with a composer
open.
Compose new messages inline, with no conversation selected
Hook up composer accelerators only when focus is in composer editor
It would be nice to only activate these accelerators when the composer
has focus generally, but that doesn't seem to be easy to detect.
Only disconnect accelerators if they're connected
Maintain focus when composer is popped out
The selection isn't, though.
Fix Tab focus for embedded composer
There are two things that needed to be fixed: The tab key doesn't
usually advance focus for embedded widgets (huh?), so we handle tab
presses by hand for ComposerWidgets. Also, the EmailEntrys do their own
tab handling, which needs to know about the composer widget, not the
toplevel widget in the embedded case.
Remove close() from ComposerContainer interface
I don't think it was actually doing anything, and it conflicts with the new close() method of Gtk.Window.
2014-02-11 00:26:14 -05:00
foreach ( ComposerWidget cw in composers_to_destroy )
2014-05-20 17:35:50 -07:00
( ( ComposerContainer ) cw . parent ) . close_container ( ) ;
2013-08-08 18:47:23 -07:00
// If we cancelled the quit we can bail here.
if ( quit_cancelled ) {
waiting_to_close . clear ( ) ;
return false ;
}
// If there's still windows saving, we can't exit just yet. Hide the main window and wait.
if ( waiting_to_close . size > 0 ) {
main_window . hide ( ) ;
2012-05-22 14:04:24 -07:00
2013-08-08 18:47:23 -07:00
return false ;
2012-05-22 14:04:24 -07:00
}
// If we deleted all composer windows without the user cancelling, we can exit.
return true ;
}
2016-06-21 14:48:24 +10:00
// View contains the email from whose menu this reply or forward
// was triggered. If null, this was triggered from the headerbar
// or shortcut.
2014-10-19 23:24:23 -04:00
private void create_reply_forward_widget ( ComposerWidget . ComposeType compose_type ,
2016-07-25 10:33:42 +10:00
owned ConversationEmail ? email_view ) {
if ( email_view = = null ) {
ConversationListBox ? list_view =
main_window . conversation_viewer . current_list ;
if ( list_view ! = null ) {
2016-08-22 11:31:06 +10:00
email_view = list_view . get_reply_target ( ) ;
2016-07-25 10:33:42 +10:00
}
2016-07-04 22:31:47 +10:00
}
2016-12-01 12:06:44 +11:00
2016-07-25 10:33:42 +10:00
if ( email_view ! = null ) {
2016-12-01 12:06:44 +11:00
email_view . get_selection_for_quoting . begin ( ( obj , res ) = > {
string ? quote = email_view . get_selection_for_quoting . end ( res ) ;
create_compose_widget ( compose_type , email_view . email , quote ) ;
} ) ;
} else {
create_compose_widget ( compose_type , email_view . email , null ) ;
2016-07-04 22:31:47 +10:00
}
2014-10-19 23:24:23 -04:00
}
2016-06-21 14:48:24 +10:00
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Basic inline composition
Many of the details don't work, or don't work well, but the basics are
in place.
Allow only a single inline composition at a time
With this, we introduce a dialog when you would try to add another. We
also use this when changing the selected conversation with a composer
open.
Compose new messages inline, with no conversation selected
Hook up composer accelerators only when focus is in composer editor
It would be nice to only activate these accelerators when the composer
has focus generally, but that doesn't seem to be easy to detect.
Only disconnect accelerators if they're connected
Maintain focus when composer is popped out
The selection isn't, though.
Fix Tab focus for embedded composer
There are two things that needed to be fixed: The tab key doesn't
usually advance focus for embedded widgets (huh?), so we handle tab
presses by hand for ComposerWidgets. Also, the EmailEntrys do their own
tab handling, which needs to know about the composer widget, not the
toplevel widget in the embedded case.
Remove close() from ComposerContainer interface
I don't think it was actually doing anything, and it conflicts with the new close() method of Gtk.Window.
2014-02-11 00:26:14 -05:00
private void create_compose_widget ( ComposerWidget . ComposeType compose_type ,
2014-08-24 19:52:12 -04:00
Geary . Email ? referred = null , string ? quote = null , string ? mailto = null ,
bool is_draft = false ) {
create_compose_widget_async . begin ( compose_type , referred , quote , mailto , is_draft ) ;
2013-07-19 15:44:20 -07:00
}
2016-08-22 11:09:25 +02:00
/ * *
* Creates a composer widget . Depending on the arguments , this can be inline in the
* conversation or as a new window .
* @ param compose_type - Whether it ' s a new message , a reply , a forwarded mail , . . .
* @ param referred - The mail of which we should copy the from / to / . . . addresses
* @ param quote - The quote after the mail body
* @ param mailto - A " mailto: " - link
* @ param is_draft - Whether we ' re starting from a draft ( true ) or a new mail ( false )
* /
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Basic inline composition
Many of the details don't work, or don't work well, but the basics are
in place.
Allow only a single inline composition at a time
With this, we introduce a dialog when you would try to add another. We
also use this when changing the selected conversation with a composer
open.
Compose new messages inline, with no conversation selected
Hook up composer accelerators only when focus is in composer editor
It would be nice to only activate these accelerators when the composer
has focus generally, but that doesn't seem to be easy to detect.
Only disconnect accelerators if they're connected
Maintain focus when composer is popped out
The selection isn't, though.
Fix Tab focus for embedded composer
There are two things that needed to be fixed: The tab key doesn't
usually advance focus for embedded widgets (huh?), so we handle tab
presses by hand for ComposerWidgets. Also, the EmailEntrys do their own
tab handling, which needs to know about the composer widget, not the
toplevel widget in the embedded case.
Remove close() from ComposerContainer interface
I don't think it was actually doing anything, and it conflicts with the new close() method of Gtk.Window.
2014-02-11 00:26:14 -05:00
private async void create_compose_widget_async ( ComposerWidget . ComposeType compose_type ,
2014-08-24 19:52:12 -04:00
Geary . Email ? referred = null , string ? quote = null , string ? mailto = null ,
bool is_draft = false ) {
2013-02-01 17:18:11 -08:00
if ( current_account = = null )
return ;
2014-05-19 13:09:56 -07:00
bool inline ;
2015-01-16 14:35:19 -08:00
if ( ! should_create_new_composer ( compose_type , referred , quote , is_draft , out inline ) )
2014-05-15 16:33:25 -07:00
return ;
2016-07-25 10:33:42 +10:00
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Basic inline composition
Many of the details don't work, or don't work well, but the basics are
in place.
Allow only a single inline composition at a time
With this, we introduce a dialog when you would try to add another. We
also use this when changing the selected conversation with a composer
open.
Compose new messages inline, with no conversation selected
Hook up composer accelerators only when focus is in composer editor
It would be nice to only activate these accelerators when the composer
has focus generally, but that doesn't seem to be easy to detect.
Only disconnect accelerators if they're connected
Maintain focus when composer is popped out
The selection isn't, though.
Fix Tab focus for embedded composer
There are two things that needed to be fixed: The tab key doesn't
usually advance focus for embedded widgets (huh?), so we handle tab
presses by hand for ComposerWidgets. Also, the EmailEntrys do their own
tab handling, which needs to know about the composer widget, not the
toplevel widget in the embedded case.
Remove close() from ComposerContainer interface
I don't think it was actually doing anything, and it conflicts with the new close() method of Gtk.Window.
2014-02-11 00:26:14 -05:00
ComposerWidget widget ;
2013-08-19 16:39:53 -07:00
if ( mailto ! = null ) {
2016-12-11 14:30:59 +01:00
widget = new ComposerWidget . from_mailto ( current_account , contact_list_store_cache ,
mailto , application . config ) ;
2013-08-19 16:39:53 -07:00
} else {
2016-12-11 14:30:59 +01:00
widget = new ComposerWidget ( current_account , contact_list_store_cache , compose_type , application . config ) ;
2017-01-16 15:55:38 +11:00
}
Clean up how composer loads content into its web view.
The main gist of this is to ensure that the composer's widgets are
constructed seperately to loading its content, and that we only ever call
ComposerWebView::load_html precisely once per composer instance.
* src/client/composer/composer-widget.vala: Remove referred message,
quote text and draft flag param from constructor signature, move any
calls that loaded data from them to new load method. Don't load
anything into the editor here. Make loading the signature file async,
and call new ComposerWebView::updateSignature method on the editor to
update it.
(ComposerWidget::load): New async message for loading content into the
composer. Move related code from the constructor and GearyController
here, make methods that were previously public for that private
again. Tidy up calls a bit now that we have a single place from which
to do it all, and can understand the process a bit better.
(ComposerWidget::on_editor_key_press_event): Don't reload the editor to
remove the quoted text, call new ComposerWebView::delete_quoted_message
method on it instead.
* src/client/composer/composer-web-view.vala
(ComposerWebView): Add ::delete_quoted_message ::update_signature
methods, thunk to JS.
(ComposerWebView::load_html): Add quote and is_draft parameters,
construct HTML for the composer using apporporate spacing here, instead
of relying on all the disparate parts from doing the right thing.
* src/client/application/geary-controller.vala
(GearyController::create_compose_widget_async): Load composer content
after adding it to the widget hierarchy, set focus only after
everything is set up.
* src/engine/rfc822/rfc822-utils.vala (quote_email_for_reply,
quote_email_for_forward): Don't add extra padding around quoted parts -
let callers manage their own whitespace.
* test/client/components/client-web-view-test-case.vala
(TestCase:load_body_fixture): Make HTML param non-nullable, update
subclasses.
* ui/composer-web-view.js (ComposerPageState): Add ::updateSignature and
::deleteQuotedMessage method stubs.
2017-01-25 11:16:20 +11:00
widget . destroy . connect ( on_composer_widget_destroy ) ;
Replace composer link dialog with a popover.
* src/client/composer/composer-link-popover.vala: New GtkPopover subclass
for creating/editing links.
* src/client/composer/composer-web-view.vala (EditContext): Add is_link
and link_uri properties, decode them from the message string, add
decoding tests.
(ComposerWebView): Add some as-yet un-implemented methods for
inserting/deleting links.
* src/client/composer/composer-widget.vala (ComposerWidget): Add
cursor_url for storing current text cursor link, update it from the
cursor_context_changed signal param, rename hover_url to pointer_url to
match. Add link_activated signal to let user's open links they are
adding, hook that up in the controller. Rename
::update_selection_actions to ::update_cursor_actions, since that's a
little more apt now, also enable insert link action if there is a
cursor_url set as well as a selection. Remove ::link_dialog, replace
with ::new_link_popover, hook up the new popover's signals there as
appropriate.
(ComposerWidget::on_insert_link): Create and show a lin popover instead
of a dialog.
* ui/composer-web-view.js: Take note of whther the context node is a link
and if so, also it's href. Include both when serialsing for the
cursorContextChanged message. Add serialisation tests.
* ui/composer-link-popover.ui: New UI for link popover.
2017-01-19 02:23:57 +11:00
widget . link_activated . connect ( ( uri ) = > { open_uri ( uri ) ; } ) ;
2016-09-19 12:20:47 +10:00
2012-05-22 14:04:24 -07:00
// We want to keep track of the open composer windows, so we can allow the user to cancel
// an exit without losing their data.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Basic inline composition
Many of the details don't work, or don't work well, but the basics are
in place.
Allow only a single inline composition at a time
With this, we introduce a dialog when you would try to add another. We
also use this when changing the selected conversation with a composer
open.
Compose new messages inline, with no conversation selected
Hook up composer accelerators only when focus is in composer editor
It would be nice to only activate these accelerators when the composer
has focus generally, but that doesn't seem to be easy to detect.
Only disconnect accelerators if they're connected
Maintain focus when composer is popped out
The selection isn't, though.
Fix Tab focus for embedded composer
There are two things that needed to be fixed: The tab key doesn't
usually advance focus for embedded widgets (huh?), so we handle tab
presses by hand for ComposerWidgets. Also, the EmailEntrys do their own
tab handling, which needs to know about the composer widget, not the
toplevel widget in the embedded case.
Remove close() from ComposerContainer interface
I don't think it was actually doing anything, and it conflicts with the new close() method of Gtk.Window.
2014-02-11 00:26:14 -05:00
composer_widgets . add ( widget ) ;
2015-01-16 14:35:19 -08:00
debug ( @" Creating composer of type $(widget.compose_type); $(composer_widgets.size) composers total " ) ;
Clean up how composer loads content into its web view.
The main gist of this is to ensure that the composer's widgets are
constructed seperately to loading its content, and that we only ever call
ComposerWebView::load_html precisely once per composer instance.
* src/client/composer/composer-widget.vala: Remove referred message,
quote text and draft flag param from constructor signature, move any
calls that loaded data from them to new load method. Don't load
anything into the editor here. Make loading the signature file async,
and call new ComposerWebView::updateSignature method on the editor to
update it.
(ComposerWidget::load): New async message for loading content into the
composer. Move related code from the constructor and GearyController
here, make methods that were previously public for that private
again. Tidy up calls a bit now that we have a single place from which
to do it all, and can understand the process a bit better.
(ComposerWidget::on_editor_key_press_event): Don't reload the editor to
remove the quoted text, call new ComposerWebView::delete_quoted_message
method on it instead.
* src/client/composer/composer-web-view.vala
(ComposerWebView): Add ::delete_quoted_message ::update_signature
methods, thunk to JS.
(ComposerWebView::load_html): Add quote and is_draft parameters,
construct HTML for the composer using apporporate spacing here, instead
of relying on all the disparate parts from doing the right thing.
* src/client/application/geary-controller.vala
(GearyController::create_compose_widget_async): Load composer content
after adding it to the widget hierarchy, set focus only after
everything is set up.
* src/engine/rfc822/rfc822-utils.vala (quote_email_for_reply,
quote_email_for_forward): Don't add extra padding around quoted parts -
let callers manage their own whitespace.
* test/client/components/client-web-view-test-case.vala
(TestCase:load_body_fixture): Make HTML param non-nullable, update
subclasses.
* ui/composer-web-view.js (ComposerPageState): Add ::updateSignature and
::deleteQuotedMessage method stubs.
2017-01-25 11:16:20 +11:00
2014-05-19 13:09:56 -07:00
if ( inline ) {
2018-02-07 11:16:30 +11:00
if ( widget . state = = ComposerWidget . ComposerState . PANED ) {
Reenable and update code for attached, full pane message composer.
Display attached+un-embedded composer as an additional ConversationViewer
stack page, although it realy should be broken as its own top-level
widget - there's already too much state in ConversationViewer.
* src/client/conversation-viewer/conversation-viewer.vala: Remove old
composer boxes code. Add new ViewState enum, property and methods to
define and manipulate the current view state - either conversation or
composer.
(do_conversation): New method to put the viewer in conversation mode.
(do_compose): New method to put the viewer in compose mode, hook up the
composer widget, and handle ConversationListView selection management
for now.
(on_folder_selected, on_conversation_count_changed,
on_conversations_selected): Ensure these methods do the right thing
depending on the viewer's current view state.
(set_paned_composer): Replaced by ::do_compose, fixed call sites.
(show_multiple_selected): Minor code clean up - moved down to a more
appropriate location.
* src/client/composer/composer-box.vala (ComposerBox): Don't attempt to
up-manage it's parent's state, since the parent has a much better idea
of how best to do that. Likewise move code to manage previous
ConversationList selection out, provide signal so the a more
appropriate class can manage it instead.
* src/client/composer/composer-container.vala: Add some method comments.
* ui/conversation-viewer.ui: Add new page to the stack for the composer.
2016-04-11 02:52:53 +10:00
main_window . conversation_viewer . do_compose ( widget ) ;
2017-05-15 23:21:32 +02:00
get_window_action ( ACTION_FIND_IN_CONVERSATION ) . set_enabled ( false ) ;
2016-04-12 01:06:09 +10:00
} else {
2016-09-19 14:20:03 +10:00
main_window . conversation_viewer . do_compose_embedded (
2016-07-25 10:33:42 +10:00
widget ,
2016-09-19 14:20:03 +10:00
referred ,
is_draft
2016-07-25 10:33:42 +10:00
) ;
2016-04-12 01:06:09 +10:00
}
2014-05-19 13:09:56 -07:00
} else {
new ComposerWindow ( widget ) ;
2015-01-12 15:18:30 -08:00
widget . state = ComposerWidget . ComposerState . DETACHED ;
2014-05-19 13:09:56 -07:00
}
2016-09-23 23:31:01 +10:00
Clean up how composer loads content into its web view.
The main gist of this is to ensure that the composer's widgets are
constructed seperately to loading its content, and that we only ever call
ComposerWebView::load_html precisely once per composer instance.
* src/client/composer/composer-widget.vala: Remove referred message,
quote text and draft flag param from constructor signature, move any
calls that loaded data from them to new load method. Don't load
anything into the editor here. Make loading the signature file async,
and call new ComposerWebView::updateSignature method on the editor to
update it.
(ComposerWidget::load): New async message for loading content into the
composer. Move related code from the constructor and GearyController
here, make methods that were previously public for that private
again. Tidy up calls a bit now that we have a single place from which
to do it all, and can understand the process a bit better.
(ComposerWidget::on_editor_key_press_event): Don't reload the editor to
remove the quoted text, call new ComposerWebView::delete_quoted_message
method on it instead.
* src/client/composer/composer-web-view.vala
(ComposerWebView): Add ::delete_quoted_message ::update_signature
methods, thunk to JS.
(ComposerWebView::load_html): Add quote and is_draft parameters,
construct HTML for the composer using apporporate spacing here, instead
of relying on all the disparate parts from doing the right thing.
* src/client/application/geary-controller.vala
(GearyController::create_compose_widget_async): Load composer content
after adding it to the widget hierarchy, set focus only after
everything is set up.
* src/engine/rfc822/rfc822-utils.vala (quote_email_for_reply,
quote_email_for_forward): Don't add extra padding around quoted parts -
let callers manage their own whitespace.
* test/client/components/client-web-view-test-case.vala
(TestCase:load_body_fixture): Make HTML param non-nullable, update
subclasses.
* ui/composer-web-view.js (ComposerPageState): Add ::updateSignature and
::deleteQuotedMessage method stubs.
2017-01-25 11:16:20 +11:00
// Load the widget's content
Geary . Email ? full = null ;
if ( referred ! = null ) {
2019-01-15 12:19:22 +11:00
Geary . App . EmailStore ? store = get_store_for_folder ( current_folder ) ;
if ( store ! = null ) {
try {
full = yield store . fetch_email_async (
referred . id ,
Geary . ComposedEmail . REQUIRED_REPLY_FIELDS ,
Geary . Folder . ListFlags . NONE ,
cancellable_folder
) ;
} catch ( Error e ) {
message ( " Could not load full message: %s " , e . message ) ;
}
Clean up how composer loads content into its web view.
The main gist of this is to ensure that the composer's widgets are
constructed seperately to loading its content, and that we only ever call
ComposerWebView::load_html precisely once per composer instance.
* src/client/composer/composer-widget.vala: Remove referred message,
quote text and draft flag param from constructor signature, move any
calls that loaded data from them to new load method. Don't load
anything into the editor here. Make loading the signature file async,
and call new ComposerWebView::updateSignature method on the editor to
update it.
(ComposerWidget::load): New async message for loading content into the
composer. Move related code from the constructor and GearyController
here, make methods that were previously public for that private
again. Tidy up calls a bit now that we have a single place from which
to do it all, and can understand the process a bit better.
(ComposerWidget::on_editor_key_press_event): Don't reload the editor to
remove the quoted text, call new ComposerWebView::delete_quoted_message
method on it instead.
* src/client/composer/composer-web-view.vala
(ComposerWebView): Add ::delete_quoted_message ::update_signature
methods, thunk to JS.
(ComposerWebView::load_html): Add quote and is_draft parameters,
construct HTML for the composer using apporporate spacing here, instead
of relying on all the disparate parts from doing the right thing.
* src/client/application/geary-controller.vala
(GearyController::create_compose_widget_async): Load composer content
after adding it to the widget hierarchy, set focus only after
everything is set up.
* src/engine/rfc822/rfc822-utils.vala (quote_email_for_reply,
quote_email_for_forward): Don't add extra padding around quoted parts -
let callers manage their own whitespace.
* test/client/components/client-web-view-test-case.vala
(TestCase:load_body_fixture): Make HTML param non-nullable, update
subclasses.
* ui/composer-web-view.js (ComposerPageState): Add ::updateSignature and
::deleteQuotedMessage method stubs.
2017-01-25 11:16:20 +11:00
}
2016-09-23 23:31:01 +10:00
}
Clean up how composer loads content into its web view.
The main gist of this is to ensure that the composer's widgets are
constructed seperately to loading its content, and that we only ever call
ComposerWebView::load_html precisely once per composer instance.
* src/client/composer/composer-widget.vala: Remove referred message,
quote text and draft flag param from constructor signature, move any
calls that loaded data from them to new load method. Don't load
anything into the editor here. Make loading the signature file async,
and call new ComposerWebView::updateSignature method on the editor to
update it.
(ComposerWidget::load): New async message for loading content into the
composer. Move related code from the constructor and GearyController
here, make methods that were previously public for that private
again. Tidy up calls a bit now that we have a single place from which
to do it all, and can understand the process a bit better.
(ComposerWidget::on_editor_key_press_event): Don't reload the editor to
remove the quoted text, call new ComposerWebView::delete_quoted_message
method on it instead.
* src/client/composer/composer-web-view.vala
(ComposerWebView): Add ::delete_quoted_message ::update_signature
methods, thunk to JS.
(ComposerWebView::load_html): Add quote and is_draft parameters,
construct HTML for the composer using apporporate spacing here, instead
of relying on all the disparate parts from doing the right thing.
* src/client/application/geary-controller.vala
(GearyController::create_compose_widget_async): Load composer content
after adding it to the widget hierarchy, set focus only after
everything is set up.
* src/engine/rfc822/rfc822-utils.vala (quote_email_for_reply,
quote_email_for_forward): Don't add extra padding around quoted parts -
let callers manage their own whitespace.
* test/client/components/client-web-view-test-case.vala
(TestCase:load_body_fixture): Make HTML param non-nullable, update
subclasses.
* ui/composer-web-view.js (ComposerPageState): Add ::updateSignature and
::deleteQuotedMessage method stubs.
2017-01-25 11:16:20 +11:00
yield widget . load ( full , quote , is_draft ) ;
2016-09-23 23:31:01 +10:00
Clean up how composer loads content into its web view.
The main gist of this is to ensure that the composer's widgets are
constructed seperately to loading its content, and that we only ever call
ComposerWebView::load_html precisely once per composer instance.
* src/client/composer/composer-widget.vala: Remove referred message,
quote text and draft flag param from constructor signature, move any
calls that loaded data from them to new load method. Don't load
anything into the editor here. Make loading the signature file async,
and call new ComposerWebView::updateSignature method on the editor to
update it.
(ComposerWidget::load): New async message for loading content into the
composer. Move related code from the constructor and GearyController
here, make methods that were previously public for that private
again. Tidy up calls a bit now that we have a single place from which
to do it all, and can understand the process a bit better.
(ComposerWidget::on_editor_key_press_event): Don't reload the editor to
remove the quoted text, call new ComposerWebView::delete_quoted_message
method on it instead.
* src/client/composer/composer-web-view.vala
(ComposerWebView): Add ::delete_quoted_message ::update_signature
methods, thunk to JS.
(ComposerWebView::load_html): Add quote and is_draft parameters,
construct HTML for the composer using apporporate spacing here, instead
of relying on all the disparate parts from doing the right thing.
* src/client/application/geary-controller.vala
(GearyController::create_compose_widget_async): Load composer content
after adding it to the widget hierarchy, set focus only after
everything is set up.
* src/engine/rfc822/rfc822-utils.vala (quote_email_for_reply,
quote_email_for_forward): Don't add extra padding around quoted parts -
let callers manage their own whitespace.
* test/client/components/client-web-view-test-case.vala
(TestCase:load_body_fixture): Make HTML param non-nullable, update
subclasses.
* ui/composer-web-view.js (ComposerPageState): Add ::updateSignature and
::deleteQuotedMessage method stubs.
2017-01-25 11:16:20 +11:00
widget . set_focus ( ) ;
2014-05-13 12:31:26 -07:00
}
Clean up how composer loads content into its web view.
The main gist of this is to ensure that the composer's widgets are
constructed seperately to loading its content, and that we only ever call
ComposerWebView::load_html precisely once per composer instance.
* src/client/composer/composer-widget.vala: Remove referred message,
quote text and draft flag param from constructor signature, move any
calls that loaded data from them to new load method. Don't load
anything into the editor here. Make loading the signature file async,
and call new ComposerWebView::updateSignature method on the editor to
update it.
(ComposerWidget::load): New async message for loading content into the
composer. Move related code from the constructor and GearyController
here, make methods that were previously public for that private
again. Tidy up calls a bit now that we have a single place from which
to do it all, and can understand the process a bit better.
(ComposerWidget::on_editor_key_press_event): Don't reload the editor to
remove the quoted text, call new ComposerWebView::delete_quoted_message
method on it instead.
* src/client/composer/composer-web-view.vala
(ComposerWebView): Add ::delete_quoted_message ::update_signature
methods, thunk to JS.
(ComposerWebView::load_html): Add quote and is_draft parameters,
construct HTML for the composer using apporporate spacing here, instead
of relying on all the disparate parts from doing the right thing.
* src/client/application/geary-controller.vala
(GearyController::create_compose_widget_async): Load composer content
after adding it to the widget hierarchy, set focus only after
everything is set up.
* src/engine/rfc822/rfc822-utils.vala (quote_email_for_reply,
quote_email_for_forward): Don't add extra padding around quoted parts -
let callers manage their own whitespace.
* test/client/components/client-web-view-test-case.vala
(TestCase:load_body_fixture): Make HTML param non-nullable, update
subclasses.
* ui/composer-web-view.js (ComposerPageState): Add ::updateSignature and
::deleteQuotedMessage method stubs.
2017-01-25 11:16:20 +11:00
2014-05-19 13:09:56 -07:00
private bool should_create_new_composer ( ComposerWidget . ComposeType ? compose_type ,
2015-01-16 14:35:19 -08:00
Geary . Email ? referred , string ? quote , bool is_draft , out bool inline ) {
2014-05-19 13:09:56 -07:00
inline = true ;
// In we're replying, see whether we already have a reply for that message.
if ( compose_type ! = null & & compose_type ! = ComposerWidget . ComposeType . NEW_MESSAGE ) {
2014-05-15 16:33:25 -07:00
foreach ( ComposerWidget cw in composer_widgets ) {
2014-05-16 14:55:47 -07:00
if ( cw . state ! = ComposerWidget . ComposerState . DETACHED & &
2015-01-13 13:52:35 -08:00
( ( referred ! = null & & cw . referred_ids . contains ( referred . id ) ) | |
quote ! = null ) ) {
2014-08-24 22:29:25 -04:00
cw . change_compose_type ( compose_type , referred , quote ) ;
2014-05-15 16:33:25 -07:00
return false ;
}
}
2015-01-12 15:18:30 -08:00
inline = ! any_inline_composers ( ) ;
2014-05-15 16:33:25 -07:00
return true ;
}
2014-05-19 13:09:56 -07:00
// If there are no inline composers, go ahead!
2014-05-16 12:24:42 -07:00
if ( ! any_inline_composers ( ) )
return true ;
2015-01-16 14:35:19 -08:00
// If we're resuming a draft with open composers, open in a new window.
if ( is_draft ) {
inline = false ;
return true ;
}
2014-05-19 13:09:56 -07:00
// If we're creating a new message, and there's already a new message open, focus on
// it if it hasn't been modified; otherwise open a new composer in a new window.
if ( compose_type = = ComposerWidget . ComposeType . NEW_MESSAGE ) {
foreach ( ComposerWidget cw in composer_widgets ) {
2018-02-07 11:16:30 +11:00
if ( cw . state = = ComposerWidget . ComposerState . PANED ) {
2017-01-06 10:48:40 +11:00
if ( ! cw . is_blank ) {
2014-05-19 13:09:56 -07:00
inline = false ;
return true ;
} else {
cw . change_compose_type ( compose_type ) ; // To refocus
return false ;
}
}
}
}
// Find out what to do with the inline composers.
2014-05-15 16:33:25 -07:00
// TODO: Remove this in favor of automatically saving drafts
2018-02-07 15:01:34 +11:00
this . application . present ( ) ;
2018-01-10 18:33:15 +11:00
Gee . List < ComposerWidget > composers_to_destroy = new Gee . ArrayList < ComposerWidget > ( ) ;
foreach ( ComposerWidget cw in composer_widgets ) {
if ( cw . state ! = ComposerWidget . ComposerState . DETACHED )
composers_to_destroy . add ( cw ) ;
}
string message = ngettext (
" Close the draft message? " ,
" Close all draft messages? " ,
composers_to_destroy . size
) ;
ConfirmationDialog dialog = new ConfirmationDialog (
main_window , message , null , Stock . _CLOSE , " destructive-action "
) ;
2014-05-20 16:41:22 -07:00
if ( dialog . run ( ) = = Gtk . ResponseType . OK ) {
2014-05-15 16:33:25 -07:00
foreach ( ComposerWidget cw in composers_to_destroy )
2014-05-20 17:35:50 -07:00
( ( ComposerContainer ) cw . parent ) . close_container ( ) ;
2014-05-13 12:31:26 -07:00
return true ;
}
return false ;
2011-11-22 18:55:07 -08:00
}
2014-05-19 13:09:56 -07:00
public bool can_switch_conversation_view ( ) {
bool inline ;
2015-01-16 14:35:19 -08:00
return should_create_new_composer ( null , null , null , false , out inline ) ;
2014-05-19 13:09:56 -07:00
}
2014-05-15 16:33:25 -07:00
public bool any_inline_composers ( ) {
foreach ( ComposerWidget cw in composer_widgets )
2014-05-16 14:55:47 -07:00
if ( cw . state ! = ComposerWidget . ComposerState . DETACHED )
2014-05-15 16:33:25 -07:00
return true ;
return false ;
}
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Basic inline composition
Many of the details don't work, or don't work well, but the basics are
in place.
Allow only a single inline composition at a time
With this, we introduce a dialog when you would try to add another. We
also use this when changing the selected conversation with a composer
open.
Compose new messages inline, with no conversation selected
Hook up composer accelerators only when focus is in composer editor
It would be nice to only activate these accelerators when the composer
has focus generally, but that doesn't seem to be easy to detect.
Only disconnect accelerators if they're connected
Maintain focus when composer is popped out
The selection isn't, though.
Fix Tab focus for embedded composer
There are two things that needed to be fixed: The tab key doesn't
usually advance focus for embedded widgets (huh?), so we handle tab
presses by hand for ComposerWidgets. Also, the EmailEntrys do their own
tab handling, which needs to know about the composer widget, not the
toplevel widget in the embedded case.
Remove close() from ComposerContainer interface
I don't think it was actually doing anything, and it conflicts with the new close() method of Gtk.Window.
2014-02-11 00:26:14 -05:00
private void on_composer_widget_destroy ( Gtk . Widget sender ) {
composer_widgets . remove ( ( ComposerWidget ) sender ) ;
2014-06-12 13:04:58 -04:00
debug ( @" Destroying composer of type $(((ComposerWidget) sender).compose_type); "
+ @" $(composer_widgets.size) composers remaining " ) ;
2013-08-08 18:47:23 -07:00
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Basic inline composition
Many of the details don't work, or don't work well, but the basics are
in place.
Allow only a single inline composition at a time
With this, we introduce a dialog when you would try to add another. We
also use this when changing the selected conversation with a composer
open.
Compose new messages inline, with no conversation selected
Hook up composer accelerators only when focus is in composer editor
It would be nice to only activate these accelerators when the composer
has focus generally, but that doesn't seem to be easy to detect.
Only disconnect accelerators if they're connected
Maintain focus when composer is popped out
The selection isn't, though.
Fix Tab focus for embedded composer
There are two things that needed to be fixed: The tab key doesn't
usually advance focus for embedded widgets (huh?), so we handle tab
presses by hand for ComposerWidgets. Also, the EmailEntrys do their own
tab handling, which needs to know about the composer widget, not the
toplevel widget in the embedded case.
Remove close() from ComposerContainer interface
I don't think it was actually doing anything, and it conflicts with the new close() method of Gtk.Window.
2014-02-11 00:26:14 -05:00
if ( waiting_to_close . remove ( ( ComposerWidget ) sender ) ) {
2013-08-08 18:47:23 -07:00
// If we just removed the last window in the waiting to close list, it's time to exit!
if ( waiting_to_close . size = = 0 )
2016-10-11 01:40:12 +11:00
this . application . exit ( ) ;
2013-08-08 18:47:23 -07:00
}
2012-05-22 14:04:24 -07:00
}
2019-02-24 21:44:03 +11:00
private void on_close ( ) {
this . application . exit ( ) ;
2012-01-17 18:42:20 -08:00
}
2016-06-21 14:48:24 +10:00
private void on_reply_to_message ( ConversationEmail target_view ) {
create_reply_forward_widget ( ComposerWidget . ComposeType . REPLY , target_view ) ;
2012-07-02 18:37:46 -07:00
}
2016-06-21 14:48:24 +10:00
2017-05-15 23:21:32 +02:00
private void on_reply_to_message_action ( SimpleAction action ) {
2014-10-19 23:24:23 -04:00
create_reply_forward_widget ( ComposerWidget . ComposeType . REPLY , null ) ;
2012-07-02 18:37:46 -07:00
}
2016-06-21 14:48:24 +10:00
private void on_reply_all_message ( ConversationEmail target_view ) {
create_reply_forward_widget ( ComposerWidget . ComposeType . REPLY_ALL , target_view ) ;
2012-01-17 18:42:20 -08:00
}
2016-06-21 14:48:24 +10:00
2017-05-15 23:21:32 +02:00
private void on_reply_all_message_action ( SimpleAction action ) {
2014-10-19 23:24:23 -04:00
create_reply_forward_widget ( ComposerWidget . ComposeType . REPLY_ALL , null ) ;
2012-07-02 18:37:46 -07:00
}
2016-06-21 14:48:24 +10:00
private void on_forward_message ( ConversationEmail target_view ) {
create_reply_forward_widget ( ComposerWidget . ComposeType . FORWARD , target_view ) ;
2012-01-17 18:42:20 -08:00
}
2016-06-21 14:48:24 +10:00
2017-05-15 23:21:32 +02:00
private void on_forward_message_action ( SimpleAction action ) {
2014-10-19 23:24:23 -04:00
create_reply_forward_widget ( ComposerWidget . ComposeType . FORWARD , null ) ;
2012-01-24 18:24:32 -08:00
}
Reimplement in-conversation find.
* src/client/application/geary-controller.vala (GearyController): Remove
ACTION_FIND_NEXT_IN_CONVERSATION and
ACTION_FIND_PREVIOUS_IN_CONVERSATION arctions and callbacks since they
will be taken care of by the search entry & search bar buttons, and
remove from accelerators.ui. Add ACTION_TOGGLE_FIND action to handle
toggling find bar in the same way as the search bar.
* src/client/components/main-toolbar.vala (MainToolbar): Add new button
and infrastrcuture for toggling the find bar.
* src/client/conversation-viewer/conversation-viewer.vala
(ConversationViewer): Convert ::conversation_page to be grid, add new
::conversation_scroller property for the scrollbar, update call
sites. Add props for accessing find widgets, remove old find methods
and add callbacks for handling find start, change, etc.
* src/client/conversation-viewer/conversation-email.vala,
src/client/conversation-viewer/conversation-message.vala: Add methods
for accessing selected text for find.
* src/client/conversation-viewer/conversation-listbox.vala
(ConversationListBox::highlight_search_terms): Updated to return a flag
specifiying whether any search results were found, and to
expand/collapse messsages depending on whether they have any.
* src/client/conversation-viewer/conversation-message.vala
(ConversationMessage::highlight_search_terms): Keep track of how many
results were found, and return that.
* ui/conversation-viewer.ui: Convert conversation_page to be a grid, add
a search bar and search widgets to it, and move conversation
ScrolledWindow to it.
2016-08-22 11:42:54 +10:00
2017-05-15 23:21:32 +02:00
private void on_find_in_conversation_action ( SimpleAction action ) {
2019-02-04 22:35:00 +11:00
this . main_window . conversation_viewer . enable_find ( ) ;
2013-05-07 13:59:54 -07:00
}
Reimplement in-conversation find.
* src/client/application/geary-controller.vala (GearyController): Remove
ACTION_FIND_NEXT_IN_CONVERSATION and
ACTION_FIND_PREVIOUS_IN_CONVERSATION arctions and callbacks since they
will be taken care of by the search entry & search bar buttons, and
remove from accelerators.ui. Add ACTION_TOGGLE_FIND action to handle
toggling find bar in the same way as the search bar.
* src/client/components/main-toolbar.vala (MainToolbar): Add new button
and infrastrcuture for toggling the find bar.
* src/client/conversation-viewer/conversation-viewer.vala
(ConversationViewer): Convert ::conversation_page to be grid, add new
::conversation_scroller property for the scrollbar, update call
sites. Add props for accessing find widgets, remove old find methods
and add callbacks for handling find start, change, etc.
* src/client/conversation-viewer/conversation-email.vala,
src/client/conversation-viewer/conversation-message.vala: Add methods
for accessing selected text for find.
* src/client/conversation-viewer/conversation-listbox.vala
(ConversationListBox::highlight_search_terms): Updated to return a flag
specifiying whether any search results were found, and to
expand/collapse messsages depending on whether they have any.
* src/client/conversation-viewer/conversation-message.vala
(ConversationMessage::highlight_search_terms): Keep track of how many
results were found, and return that.
* ui/conversation-viewer.ui: Convert conversation_page to be a grid, add
a search bar and search widgets to it, and move conversation
ScrolledWindow to it.
2016-08-22 11:42:54 +10:00
2017-05-15 23:21:32 +02:00
private void on_search_activated ( SimpleAction action ) {
show_search_bar ( ) ;
}
private void on_archive_conversation ( SimpleAction action ) {
2014-01-07 15:04:49 -08:00
archive_or_delete_selection_async . begin ( true , false , cancellable_folder ,
on_archive_or_delete_selection_finished ) ;
}
2017-05-15 23:21:32 +02:00
private void on_trash_conversation ( SimpleAction action ) {
2014-01-07 15:04:49 -08:00
archive_or_delete_selection_async . begin ( false , true , cancellable_folder ,
on_archive_or_delete_selection_finished ) ;
}
2017-05-15 23:21:32 +02:00
private void on_delete_conversation ( SimpleAction action ) {
2014-01-07 15:04:49 -08:00
archive_or_delete_selection_async . begin ( false , false , cancellable_folder ,
on_archive_or_delete_selection_finished ) ;
}
2017-05-15 23:21:32 +02:00
private void on_empty_spam ( SimpleAction action ) {
2015-01-13 13:48:16 -08:00
on_empty_trash_or_spam ( Geary . SpecialFolderType . SPAM ) ;
}
2017-05-15 23:21:32 +02:00
private void on_empty_trash ( SimpleAction action ) {
2015-01-13 13:48:16 -08:00
on_empty_trash_or_spam ( Geary . SpecialFolderType . TRASH ) ;
}
2017-05-15 23:21:32 +02:00
2015-01-13 13:48:16 -08:00
private void on_empty_trash_or_spam ( Geary . SpecialFolderType special_folder_type ) {
// Account must be in place, must have the specified special folder type, and that folder
// must support Empty in order for this command to proceed
if ( current_account = = null )
return ;
Geary . Folder ? folder = null ;
try {
folder = current_account . get_special_folder ( special_folder_type ) ;
} catch ( Error err ) {
debug ( " %s: Unable to get special folder %s: %s " , current_account . to_string ( ) ,
special_folder_type . to_string ( ) , err . message ) ;
// fall through
}
if ( folder = = null )
return ;
Geary . FolderSupport . Empty ? emptyable = folder as Geary . FolderSupport . Empty ;
if ( emptyable = = null ) {
debug ( " %s: Special folder %s (%s) does not support emptying " , current_account . to_string ( ) ,
folder . path . to_string ( ) , special_folder_type . to_string ( ) ) ;
return ;
}
ConfirmationDialog dialog = new ConfirmationDialog ( main_window ,
_ ( " Empty all email from your %s folder? " ) . printf ( special_folder_type . get_display_name ( ) ) ,
_ ( " This removes the email from Geary and your email server. " )
+ " <b> " + _ ( " This cannot be undone. " ) + " </b> " ,
2016-10-07 15:03:12 +02:00
_ ( " Empty %s " ) . printf ( special_folder_type . get_display_name ( ) ) , " destructive-action " ) ;
2015-01-13 13:48:16 -08:00
dialog . use_secondary_markup ( true ) ;
dialog . set_focus_response ( Gtk . ResponseType . CANCEL ) ;
if ( dialog . run ( ) = = Gtk . ResponseType . OK )
empty_folder_async . begin ( emptyable , cancellable_folder ) ;
}
private async void empty_folder_async ( Geary . FolderSupport . Empty emptyable , Cancellable ? cancellable ) {
try {
yield do_empty_folder_async ( emptyable , cancellable ) ;
} catch ( Error err ) {
// don't report to user if cancelled
2018-11-27 14:53:56 +01:00
if ( err is IOError . CANCELLED )
2015-01-13 13:48:16 -08:00
return ;
ErrorDialog dialog = new ErrorDialog ( main_window ,
_ ( " Error emptying %s " ) . printf ( emptyable . get_display_name ( ) ) , err . message ) ;
dialog . run ( ) ;
}
}
2018-01-26 17:12:28 +10:30
2015-01-13 13:48:16 -08:00
private async void do_empty_folder_async ( Geary . FolderSupport . Empty emptyable , Cancellable ? cancellable )
throws Error {
2018-01-26 17:12:28 +10:30
bool open = false ;
2015-01-13 13:48:16 -08:00
try {
2018-01-26 17:12:28 +10:30
yield emptyable . open_async ( Geary . Folder . OpenFlags . NO_DELAY , cancellable ) ;
open = true ;
2015-01-13 13:48:16 -08:00
yield emptyable . empty_folder_async ( cancellable ) ;
} finally {
2018-01-26 17:12:28 +10:30
if ( open ) {
try {
yield emptyable . close_async ( null ) ;
} catch ( Error err ) {
// ignored
}
2015-01-13 13:48:16 -08:00
}
}
}
2018-01-26 17:12:28 +10:30
2014-02-11 15:24:01 -08:00
private bool current_folder_supports_trash ( ) {
return ( current_folder ! = null & & current_folder . special_folder_type ! = Geary . SpecialFolderType . TRASH
& & ! current_folder . properties . is_local_only & & current_account ! = null
& & ( current_folder as Geary . FolderSupport . Move ) ! = null ) ;
2014-01-07 15:04:49 -08:00
}
2018-02-07 15:01:34 +11:00
2018-07-02 23:51:13 +10:00
private bool confirm_delete ( int num_messages ) {
2018-02-07 15:01:34 +11:00
this . application . present ( ) ;
2016-10-07 15:03:12 +02:00
ConfirmationDialog dialog = new ConfirmationDialog ( main_window , ngettext (
2014-02-10 18:02:07 -08:00
" Do you want to permanently delete this message? " ,
" Do you want to permanently delete these messages? " , num_messages ) ,
2016-10-07 15:03:12 +02:00
null , _ ( " Delete " ) , " destructive-action " ) ;
2018-02-07 15:01:34 +11:00
2014-02-10 18:02:07 -08:00
return ( dialog . run ( ) = = Gtk . ResponseType . OK ) ;
}
2018-02-07 15:01:34 +11:00
2018-08-12 09:03:05 -06:00
private async void trash_messages_async ( Gee . Collection < Geary . EmailIdentifier > ids , Cancellable ? cancellable )
throws Error {
debug ( " Trashing selected messages " ) ;
Geary . FolderSupport . Move ? supports_move = current_folder as Geary . FolderSupport . Move ;
if ( current_folder_supports_trash ( ) & & supports_move ! = null ) {
Geary . FolderPath trash_path = ( yield current_account . get_required_special_folder_async (
Geary . SpecialFolderType . TRASH , cancellable ) ) . path ;
save_revokable ( yield supports_move . move_email_async ( ids , trash_path , cancellable ) ,
_ ( " Undo trash (Ctrl+Z) " ) ) ;
} else {
debug ( " Folder %s doesn't support move or account %s doesn't have a trash folder " ,
current_folder . to_string ( ) , current_account . to_string ( ) ) ;
}
}
private async void delete_messages_async ( Gee . Collection < Geary . EmailIdentifier > ids , Cancellable ? cancellable )
throws Error {
debug ( " Deleting selected messages " ) ;
Geary . FolderSupport . Remove ? supports_remove = current_folder as Geary . FolderSupport . Remove ;
if ( supports_remove ! = null ) {
if ( confirm_delete ( ids . size ) ) {
yield supports_remove . remove_email_async ( ids , cancellable ) ;
} else {
last_deleted_conversation = null ;
}
} else {
debug ( " Folder %s doesn't support remove " , current_folder . to_string ( ) ) ;
}
}
2014-01-07 15:04:49 -08:00
private async void archive_or_delete_selection_async ( bool archive , bool trash ,
Cancellable ? cancellable ) throws Error {
2014-08-23 12:35:59 -04:00
if ( ! can_switch_conversation_view ( ) )
return ;
2016-07-25 10:33:42 +10:00
ConversationListBox list_view =
main_window . conversation_viewer . current_list ;
if ( list_view ! = null & &
list_view . conversation = = last_deleted_conversation ) {
2014-01-07 15:04:49 -08:00
debug ( " Not archiving/trashing/deleting; viewed conversation is last deleted conversation " ) ;
2012-01-09 10:58:13 -08:00
return ;
2013-04-04 13:33:17 -07:00
}
2016-07-25 10:33:42 +10:00
2018-07-02 23:51:13 +10:00
selection_operation_started ( ) ;
2012-07-02 14:53:08 -07:00
last_deleted_conversation = selected_conversations . size > 0
2014-01-07 15:04:49 -08:00
? Geary . traverse < Geary . App . Conversation > ( selected_conversations ) . first ( ) : null ;
2016-10-20 16:27:43 +11:00
2018-07-07 11:45:29 +10:00
Gee . Collection < Geary . EmailIdentifier > ids = get_selected_email_ids ( false ) ;
2014-01-07 15:04:49 -08:00
if ( archive ) {
debug ( " Archiving selected messages " ) ;
2012-06-27 18:25:45 -07:00
2014-01-07 15:04:49 -08:00
Geary . FolderSupport . Archive ? supports_archive = current_folder as Geary . FolderSupport . Archive ;
2015-02-05 17:57:27 -08:00
if ( supports_archive = = null ) {
2014-01-07 15:04:49 -08:00
debug ( " Folder %s doesn't support archive " , current_folder . to_string ( ) ) ;
2015-02-05 17:57:27 -08:00
} else {
save_revokable ( yield supports_archive . archive_email_async ( ids , cancellable ) ,
_ ( " Undo archive (Ctrl+Z) " ) ) ;
}
2012-06-27 18:25:45 -07:00
return ;
}
2014-01-07 15:04:49 -08:00
if ( trash ) {
2018-08-12 09:03:05 -06:00
yield trash_messages_async ( ids , cancellable ) ;
2014-02-10 18:02:07 -08:00
} else {
2018-08-12 09:03:05 -06:00
yield delete_messages_async ( ids , cancellable ) ;
2014-02-10 18:02:07 -08:00
}
2011-12-15 16:16:27 -08:00
}
2018-07-02 23:51:13 +10:00
2014-01-07 15:04:49 -08:00
private void on_archive_or_delete_selection_finished ( Object ? source , AsyncResult result ) {
2012-04-27 18:54:21 -07:00
try {
2014-01-07 15:04:49 -08:00
archive_or_delete_selection_async . end ( result ) ;
} catch ( Error e ) {
debug ( " Unable to archive/trash/delete messages: %s " , e . message ) ;
2012-04-27 18:54:21 -07:00
}
2018-07-02 23:51:13 +10:00
selection_operation_finished ( ) ;
2012-04-27 18:54:21 -07:00
}
2018-07-02 23:51:13 +10:00
2015-02-05 17:57:27 -08:00
private void save_revokable ( Geary . Revokable ? new_revokable , string ? description ) {
// disconnect old revokable & blindly commit it
if ( revokable ! = null ) {
revokable . notify [ Geary . Revokable . PROP_VALID ] . disconnect ( on_revokable_valid_changed ) ;
revokable . notify [ Geary . Revokable . PROP_IN_PROCESS ] . disconnect ( update_revokable_action ) ;
revokable . committed . disconnect ( on_revokable_committed ) ;
revokable . commit_async . begin ( ) ;
}
// store new revokable
revokable = new_revokable ;
// connect to new revokable
if ( revokable ! = null ) {
revokable . notify [ Geary . Revokable . PROP_VALID ] . connect ( on_revokable_valid_changed ) ;
revokable . notify [ Geary . Revokable . PROP_IN_PROCESS ] . connect ( update_revokable_action ) ;
revokable . committed . connect ( on_revokable_committed ) ;
}
2016-10-11 01:40:12 +11:00
2017-05-15 23:21:32 +02:00
if ( revokable ! = null & & description ! = null )
this . main_window . main_toolbar . undo_tooltip = description ;
else
this . main_window . main_toolbar . undo_tooltip = _ ( " Undo (Ctrl+Z) " ) ;
2016-10-11 01:40:12 +11:00
2015-02-05 17:57:27 -08:00
update_revokable_action ( ) ;
}
2017-05-15 23:21:32 +02:00
2015-02-05 17:57:27 -08:00
private void update_revokable_action ( ) {
2019-02-24 20:26:11 +11:00
get_window_action ( GearyApplication . ACTION_UNDO ) . set_enabled (
this . revokable ! = null & &
this . revokable . valid & &
! this . revokable . in_process
) ;
2015-02-05 17:57:27 -08:00
}
2017-05-15 23:21:32 +02:00
2015-02-05 17:57:27 -08:00
private void on_revokable_valid_changed ( ) {
// remove revokable if it goes invalid
if ( revokable ! = null & & ! revokable . valid )
save_revokable ( null , null ) ;
}
private void on_revokable_committed ( Geary . Revokable ? committed_revokable ) {
if ( committed_revokable = = null )
return ;
2016-10-11 01:40:12 +11:00
2015-02-05 17:57:27 -08:00
// use existing description
2017-05-15 23:21:32 +02:00
save_revokable ( committed_revokable , this . main_window . main_toolbar . undo_tooltip ) ;
2015-02-05 17:57:27 -08:00
}
2017-05-15 23:21:32 +02:00
2015-02-05 17:57:27 -08:00
private void on_revoke ( ) {
if ( revokable ! = null & & revokable . valid )
revokable . revoke_async . begin ( null , on_revoke_completed ) ;
}
private void on_revoke_completed ( Object ? object , AsyncResult result ) {
// Don't use the "revokable" instance because it might have gone null before this callback
// was reached
Geary . Revokable ? origin = object as Geary . Revokable ;
if ( origin = = null )
return ;
try {
origin . revoke_async . end ( result ) ;
} catch ( Error err ) {
debug ( " Unable to revoke operation: %s " , err . message ) ;
}
}
2016-07-10 21:56:55 +10:00
2018-07-02 23:51:13 +10:00
private void selection_operation_started ( ) {
this . operation_count + = 1 ;
if ( this . operation_count = = 1 ) {
this . main_window . conversation_list_view . set_changing_selection ( true ) ;
}
}
private void selection_operation_finished ( ) {
this . operation_count - = 1 ;
if ( this . operation_count = = 0 ) {
this . main_window . conversation_list_view . set_changing_selection ( false ) ;
}
}
2017-05-15 23:21:32 +02:00
private void on_zoom ( SimpleAction action , Variant ? parameter ) {
ConversationListBox ? view = main_window . conversation_viewer . current_list ;
if ( view ! = null & & parameter ! = null ) {
string zoom_action = parameter . get_string ( ) ;
if ( zoom_action = = " in " )
view . zoom_in ( ) ;
else if ( zoom_action = = " out " )
view . zoom_out ( ) ;
else
view . zoom_reset ( ) ;
2016-07-25 10:33:42 +10:00
}
2013-09-12 14:45:29 -07:00
}
2016-07-10 21:56:55 +10:00
2015-03-05 17:23:04 -08:00
private void on_conversation_list ( ) {
2016-10-20 16:58:48 +11:00
this . main_window . conversation_list_view . grab_focus ( ) ;
2015-03-05 17:23:04 -08:00
}
2016-10-20 16:58:48 +11:00
2012-06-25 16:30:23 -07:00
private void on_sent ( Geary . RFC822 . Message rfc822 ) {
2017-05-27 14:29:45 +02:00
// Translators: The label for an in-app notification. The
// string substitution is a list of recipients of the email.
string message = _ (
" Successfully sent mail to %s. "
) . printf ( EmailUtil . to_short_recipient_display ( rfc822 . to ) ) ;
InAppNotification notification = new InAppNotification ( message ) ;
this . main_window . add_notification ( notification ) ;
2013-09-13 17:18:37 -07:00
Libnotify . play_sound ( " message-sent-email " ) ;
2012-06-25 16:30:23 -07:00
}
2016-04-18 10:17:21 +10:00
2016-07-25 10:33:42 +10:00
private void on_conversation_view_added ( ConversationListBox list ) {
list . email_added . connect ( on_conversation_viewer_email_added ) ;
list . mark_emails . connect ( on_conversation_viewer_mark_emails ) ;
}
private void on_conversation_viewer_email_added ( ConversationEmail view ) {
2016-08-22 23:15:20 +10:00
view . attachments_activated . connect ( on_attachments_activated ) ;
2019-02-02 12:41:01 +11:00
view . forward_message . connect ( on_forward_message ) ;
view . load_error . connect ( on_email_load_error ) ;
2016-07-25 10:33:42 +10:00
view . reply_to_message . connect ( on_reply_to_message ) ;
view . reply_all_message . connect ( on_reply_all_message ) ;
2018-08-12 09:03:05 -06:00
Geary . App . Conversation conversation = main_window . conversation_viewer . current_list . conversation ;
bool in_current_folder = ( conversation . is_in_base_folder ( view . email . id ) & &
conversation . base_folder = = current_folder ) ;
bool supports_trash = in_current_folder & & current_folder_supports_trash ( ) ;
bool supports_delete = in_current_folder & & current_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 ) ;
main_window . on_shift_key . connect ( view . shift_key_changed ) ;
2016-08-22 23:15:20 +10:00
view . edit_draft . connect ( ( draft_view ) = > {
create_compose_widget (
ComposerWidget . ComposeType . NEW_MESSAGE ,
draft_view . email , null , null , true
) ;
} ) ;
2017-02-23 10:19:40 +11:00
foreach ( ConversationMessage msg_view in view ) {
2018-02-06 17:53:35 +11:00
msg_view . link_activated . connect ( on_link_activated ) ;
2017-02-23 10:19:40 +11:00
msg_view . save_image . connect ( ( url , alt_text , buf ) = > {
on_save_image_extended ( view , url , alt_text , buf ) ;
} ) ;
msg_view . search_activated . connect ( ( op , value ) = > {
string search = op + " : " + value ;
show_search_bar ( search ) ;
} ) ;
}
2016-07-25 10:33:42 +10:00
view . save_attachments . connect ( on_save_attachments ) ;
view . view_source . connect ( on_view_source ) ;
2016-04-18 10:17:21 +10:00
}
2018-08-12 09:03:05 -06:00
private void on_trash_message ( ConversationEmail target_view ) {
Gee . Collection < Geary . EmailIdentifier > ids =
new Gee . ArrayList < Geary . EmailIdentifier > ( ) ;
ids . add ( target_view . email . id ) ;
trash_messages_async . begin ( ids , cancellable_folder ) ;
}
private void on_delete_message ( ConversationEmail target_view ) {
Gee . Collection < Geary . EmailIdentifier > ids =
new Gee . ArrayList < Geary . EmailIdentifier > ( ) ;
ids . add ( target_view . email . id ) ;
delete_messages_async . begin ( ids , cancellable_folder ) ;
}
2016-06-21 14:48:24 +10:00
private void on_view_source ( ConversationEmail email_view ) {
string source = ( email_view . email . header . buffer . to_string ( ) +
email_view . email . body . buffer . to_string ( ) ) ;
2016-04-23 14:17:41 +10:00
string temporary_filename ;
try {
int temporary_handle = FileUtils . open_tmp ( " geary-message-XXXXXX.txt " ,
out temporary_filename ) ;
FileUtils . set_contents ( temporary_filename , source ) ;
FileUtils . close ( temporary_handle ) ;
// ensure this file is only readable by the user ... this
// needs to be done after the file is closed
FileUtils . chmod ( temporary_filename , ( int ) ( Posix . S_IRUSR | Posix . S_IWUSR ) ) ;
string temporary_uri = Filename . to_uri ( temporary_filename , null ) ;
2017-02-27 11:36:17 +11:00
this . application . show_uri ( temporary_uri ) ;
2016-04-23 14:17:41 +10:00
} catch ( Error error ) {
ErrorDialog dialog = new ErrorDialog (
main_window ,
_ ( " Failed to open default text editor. " ) ,
error . message
) ;
dialog . run ( ) ;
2012-01-20 17:31:56 -08:00
}
}
2012-05-22 13:44:57 -07:00
2017-05-15 23:21:32 +02:00
private SimpleAction get_window_action ( string action_name ) {
return ( SimpleAction ) this . main_window . lookup_action ( action_name ) ;
}
2012-03-02 18:34:51 -08:00
// Disables all single-message buttons and enables all multi-message buttons.
2012-09-19 17:23:02 -07:00
public void enable_multiple_message_buttons ( ) {
2017-05-15 23:21:32 +02:00
main_window . main_toolbar . selected_conversations = this . selected_conversations . size ;
2016-10-11 01:40:12 +11:00
2012-03-02 18:34:51 -08:00
// Single message only buttons.
2017-05-15 23:21:32 +02:00
get_window_action ( ACTION_REPLY_TO_MESSAGE ) . set_enabled ( false ) ;
get_window_action ( ACTION_REPLY_ALL_MESSAGE ) . set_enabled ( false ) ;
get_window_action ( ACTION_FORWARD_MESSAGE ) . set_enabled ( false ) ;
2012-03-02 18:34:51 -08:00
// Mutliple message buttons.
2017-05-15 23:21:32 +02:00
get_window_action ( ACTION_MOVE_MENU ) . set_enabled ( current_folder is Geary . FolderSupport . Move ) ;
get_window_action ( ACTION_ARCHIVE_CONVERSATION ) . set_enabled ( current_folder is Geary . FolderSupport . Archive ) ;
get_window_action ( ACTION_TRASH_CONVERSATION ) . set_enabled ( current_folder_supports_trash ( ) ) ;
get_window_action ( ACTION_DELETE_CONVERSATION ) . set_enabled ( current_folder is Geary . FolderSupport . Remove ) ;
2016-10-11 01:40:12 +11:00
2013-08-29 12:37:31 -07:00
cancel_context_dependent_buttons ( ) ;
enable_context_dependent_buttons_async . begin ( true , cancellable_context_dependent_buttons ) ;
2012-03-02 18:34:51 -08:00
}
2012-05-22 13:44:57 -07:00
2012-01-30 12:20:25 -08:00
// Enables or disables the message buttons on the toolbar.
public void enable_message_buttons ( bool sensitive ) {
2017-05-15 23:21:32 +02:00
main_window . main_toolbar . selected_conversations = this . selected_conversations . size ;
2013-07-19 15:44:20 -07:00
// No reply/forward in drafts folder.
bool respond_sensitive = sensitive ;
if ( current_folder ! = null & & current_folder . special_folder_type = = Geary . SpecialFolderType . DRAFTS )
respond_sensitive = false ;
2016-10-11 01:40:12 +11:00
2017-05-15 23:21:32 +02:00
get_window_action ( ACTION_REPLY_TO_MESSAGE ) . set_enabled ( respond_sensitive ) ;
get_window_action ( ACTION_REPLY_ALL_MESSAGE ) . set_enabled ( respond_sensitive ) ;
get_window_action ( ACTION_FORWARD_MESSAGE ) . set_enabled ( respond_sensitive ) ;
get_window_action ( ACTION_MOVE_MENU ) . set_enabled ( sensitive & & ( current_folder is Geary . FolderSupport . Move ) ) ;
get_window_action ( ACTION_ARCHIVE_CONVERSATION ) . set_enabled ( sensitive & & ( current_folder is Geary . FolderSupport . Archive ) ) ;
get_window_action ( ACTION_TRASH_CONVERSATION ) . set_enabled ( sensitive & & current_folder_supports_trash ( ) ) ;
get_window_action ( ACTION_DELETE_CONVERSATION ) . set_enabled ( sensitive & & ( current_folder is Geary . FolderSupport . Remove ) ) ;
2016-10-11 01:40:12 +11:00
2013-08-29 12:37:31 -07:00
cancel_context_dependent_buttons ( ) ;
enable_context_dependent_buttons_async . begin ( sensitive , cancellable_context_dependent_buttons ) ;
2013-08-19 16:39:53 -07:00
}
2017-05-15 23:21:32 +02:00
2013-08-19 16:39:53 -07:00
private async void enable_context_dependent_buttons_async ( bool sensitive , Cancellable ? cancellable ) {
Gee . MultiMap < Geary . EmailIdentifier , Type > ? selected_operations = null ;
try {
if ( current_folder ! = null ) {
2018-02-21 16:43:56 +11:00
Geary . App . EmailStore ? store = get_store_for_folder ( current_folder ) ;
2013-10-02 15:52:28 -07:00
if ( store ! = null ) {
selected_operations = yield store
. get_supported_operations_async ( get_selected_email_ids ( false ) , cancellable ) ;
}
2013-08-19 16:39:53 -07:00
}
} catch ( Error e ) {
debug ( " Error checking for what operations are supported in the selected conversations: %s " ,
e . message ) ;
}
2013-08-29 12:37:31 -07:00
// Exit here if the user has cancelled.
if ( cancellable ! = null & & cancellable . is_cancelled ( ) )
return ;
2013-08-19 16:39:53 -07:00
Gee . HashSet < Type > supported_operations = new Gee . HashSet < Type > ( ) ;
if ( selected_operations ! = null )
supported_operations . add_all ( selected_operations . get_values ( ) ) ;
2016-10-11 01:40:12 +11:00
2017-05-15 23:21:32 +02:00
get_window_action ( ACTION_SHOW_MARK_MENU ) . set_enabled ( sensitive & & ( typeof ( Geary . FolderSupport . Mark ) in supported_operations ) ) ;
get_window_action ( ACTION_COPY_MENU ) . set_enabled ( sensitive & & ( supported_operations . contains ( typeof ( Geary . FolderSupport . Copy ) ) ) ) ;
2013-02-22 17:54:05 -08:00
}
2016-09-25 01:10:07 +10:00
2013-02-08 11:59:27 -08:00
// Returns a list of composer windows for an account, or null if none.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Basic inline composition
Many of the details don't work, or don't work well, but the basics are
in place.
Allow only a single inline composition at a time
With this, we introduce a dialog when you would try to add another. We
also use this when changing the selected conversation with a composer
open.
Compose new messages inline, with no conversation selected
Hook up composer accelerators only when focus is in composer editor
It would be nice to only activate these accelerators when the composer
has focus generally, but that doesn't seem to be easy to detect.
Only disconnect accelerators if they're connected
Maintain focus when composer is popped out
The selection isn't, though.
Fix Tab focus for embedded composer
There are two things that needed to be fixed: The tab key doesn't
usually advance focus for embedded widgets (huh?), so we handle tab
presses by hand for ComposerWidgets. Also, the EmailEntrys do their own
tab handling, which needs to know about the composer widget, not the
toplevel widget in the embedded case.
Remove close() from ComposerContainer interface
I don't think it was actually doing anything, and it conflicts with the new close() method of Gtk.Window.
2014-02-11 00:26:14 -05:00
public Gee . List < ComposerWidget > ? get_composer_widgets_for_account ( Geary . AccountInformation account ) {
Gee . LinkedList < ComposerWidget > ret = Geary . traverse < ComposerWidget > ( composer_widgets )
2013-12-12 12:42:02 -08:00
. filter ( w = > w . account . information = = account )
. to_linked_list ( ) ;
2013-02-08 11:59:27 -08:00
return ret . size > = 1 ? ret : null ;
2012-04-24 18:28:45 -07:00
}
2016-08-22 23:15:20 +10:00
private void show_search_bar ( string ? text = null ) {
main_window . search_bar . give_search_focus ( ) ;
if ( text ! = null ) {
main_window . search_bar . set_search_text ( text ) ;
}
}
2013-06-11 12:12:55 -07:00
private void do_search ( string search_text ) {
2018-08-02 20:16:16 +10:00
Geary . SearchFolder ? search_folder = null ;
if ( this . current_account ! = null ) {
try {
search_folder =
this . current_account . get_special_folder (
Geary . SpecialFolderType . SEARCH
) as Geary . SearchFolder ;
} catch ( Error e ) {
debug ( " Could not get search folder: %s " , e . message ) ;
}
2013-05-14 11:52:02 -07:00
}
2016-10-09 18:57:25 +11:00
2018-08-02 20:16:16 +10:00
if ( Geary . String . is_empty_or_whitespace ( search_text ) ) {
if ( this . previous_non_search_folder ! = null & &
this . current_folder is Geary . SearchFolder ) {
this . main_window . folder_list . select_folder (
this . previous_non_search_folder
) ;
}
this . main_window . folder_list . remove_search ( ) ;
if ( search_folder ! = null ) {
search_folder . clear ( ) ;
}
} else if ( search_folder ! = null ) {
cancel_search ( ) ; // Stop any search in progress
search_folder . search (
search_text ,
this . application . config . get_search_strategy ( ) ,
this . cancellable_search
) ;
this . main_window . folder_list . set_search ( search_folder ) ;
}
2016-10-09 18:57:25 +11:00
2018-08-02 20:16:16 +10:00
search_text_changed ( search_text ) ;
2013-05-14 11:52:02 -07:00
}
2016-10-09 18:57:25 +11:00
2013-08-26 14:56:46 -07:00
/ * *
* Returns a read - only set of currently selected conversations .
* /
public Gee . Set < Geary . App . Conversation > get_selected_conversations ( ) {
return selected_conversations . read_only_view ;
}
2018-02-21 16:43:56 +11:00
2019-01-15 12:19:22 +11:00
private inline Geary . App . EmailStore ? get_store_for_folder ( Geary . Folder target ) {
AccountContext ? context = this . accounts . get ( target . account . information ) ;
return context ! = null ? context . store : null ;
2018-02-21 16:43:56 +11:00
}
2019-02-19 16:39:11 +11:00
private bool should_add_folder ( Gee . Collection < Geary . Folder > ? all ,
Geary . Folder folder ) {
2017-03-02 12:35:39 +01:00
// if folder is openable, add it
if ( folder . properties . is_openable ! = Geary . Trillian . FALSE )
return true ;
else if ( folder . properties . has_children = = Geary . Trillian . FALSE )
return false ;
// if folder contains children, we must ensure that there is at least one of the same type
Geary . SpecialFolderType type = folder . special_folder_type ;
foreach ( Geary . Folder other in all ) {
2019-01-15 14:19:07 +11:00
if ( other . special_folder_type = = type & & other . path . parent = = folder . path )
2017-03-02 12:35:39 +01:00
return true ;
}
return false ;
}
2018-02-21 16:43:56 +11:00
private void on_account_available ( Geary . AccountInformation info ) {
Geary . Account ? account = null ;
2013-09-19 17:58:04 -07:00
try {
2018-02-21 16:43:56 +11:00
account = Geary . Engine . instance . get_account_instance ( info ) ;
2013-09-19 17:58:04 -07:00
} catch ( Error e ) {
2018-02-21 16:43:56 +11:00
error ( " Error creating account instance: %s " , e . message ) ;
}
if ( account ! = null ) {
upgrade_dialog . add_account ( account , cancellable_open_account ) ;
open_account ( account ) ;
2013-09-19 17:58:04 -07:00
}
}
2016-10-11 01:40:12 +11:00
2018-06-02 01:04:59 +02:00
private void on_account_added ( Geary . AccountInformation added ,
2018-07-23 14:38:57 +10:00
Accounts . Manager . Status status ) {
if ( status = = Accounts . Manager . Status . ENABLED ) {
2018-06-02 01:04:59 +02:00
try {
this . application . engine . add_account ( added ) ;
} catch ( GLib . Error err ) {
report_problem (
new Geary . AccountProblemReport (
Geary . ProblemType . GENERIC_ERROR , added , err
)
) ;
}
2018-05-28 00:54:43 +10:00
}
}
2018-06-02 01:04:59 +02:00
private void on_account_status_changed ( Geary . AccountInformation changed ,
2018-07-23 14:38:57 +10:00
Accounts . Manager . Status status ) {
2018-06-02 01:04:59 +02:00
switch ( status ) {
2018-07-23 14:38:57 +10:00
case Accounts . Manager . Status . ENABLED :
2018-06-02 01:04:59 +02:00
if ( ! this . application . engine . has_account ( changed . id ) ) {
try {
this . application . engine . add_account ( changed ) ;
} catch ( GLib . Error err ) {
report_problem (
new Geary . AccountProblemReport (
Geary . ProblemType . GENERIC_ERROR , changed , err
)
) ;
}
}
break ;
2018-07-23 14:38:57 +10:00
case Accounts . Manager . Status . UNAVAILABLE :
case Accounts . Manager . Status . DISABLED :
2018-06-02 01:04:59 +02:00
if ( this . application . engine . has_account ( changed . id ) ) {
this . close_account . begin (
changed ,
( obj , res ) = > {
this . close_account . end ( res ) ;
try {
this . application . engine . remove_account ( changed ) ;
} catch ( GLib . Error err ) {
report_problem (
new Geary . AccountProblemReport (
Geary . ProblemType . GENERIC_ERROR ,
changed ,
err
)
) ;
}
}
) ;
}
break ;
}
2018-05-28 00:54:43 +10:00
}
2018-06-17 18:03:09 +10:00
private void on_account_removed ( Geary . AccountInformation removed ) {
debug ( " %s: Closing account for removal " , removed . id ) ;
this . close_account . begin (
removed ,
( obj , res ) = > {
this . close_account . end ( res ) ;
debug ( " %s: Account closed " , removed . id ) ;
try {
this . application . engine . remove_account ( removed ) ;
debug ( " %s: Account removed from engine " , removed . id ) ;
} catch ( GLib . Error err ) {
report_problem (
new Geary . AccountProblemReport (
Geary . ProblemType . GENERIC_ERROR ,
removed ,
err
)
) ;
}
}
) ;
}
2019-01-01 23:22:54 +11:00
private void on_report_problem ( Geary . ProblemReport problem ) {
report_problem ( problem ) ;
}
private void on_retry_problem ( MainWindowInfoBar info_bar ) {
Geary . ServiceProblemReport ? service_report =
info_bar . report as Geary . ServiceProblemReport ;
if ( service_report ! = null ) {
AccountContext ? context = this . accounts . get ( service_report . account ) ;
if ( context ! = null & & context . account . is_open ( ) ) {
switch ( service_report . service . protocol ) {
case Geary . Protocol . IMAP :
context . account . incoming . restart . begin ( context . cancellable ) ;
break ;
case Geary . Protocol . SMTP :
context . account . outgoing . start . begin ( context . cancellable ) ;
break ;
}
}
}
}
private void on_account_status_notify ( ) {
update_account_status ( ) ;
}
private void on_authentication_failure ( Geary . AccountInformation account ,
Geary . ServiceInformation service ) {
AccountContext ? context = this . accounts . get ( account ) ;
if ( context ! = null & & ! is_currently_prompting ( ) ) {
this . prompt_for_password . begin ( context , service ) ;
}
}
2018-12-27 16:56:11 +11:00
private void on_untrusted_host ( Geary . AccountInformation account ,
Geary . ServiceInformation service ,
Geary . Endpoint endpoint ,
TlsConnection cx ) {
2019-01-08 23:34:45 +11:00
AccountContext ? context = this . accounts . get ( account ) ;
if ( context ! = null & & ! is_currently_prompting ( ) ) {
this . prompt_untrusted_host . begin ( context , service , endpoint , cx ) ;
}
2018-12-27 16:56:11 +11:00
}
2019-01-01 23:22:54 +11:00
private void on_retry_service_problem ( Geary . ClientService . Status type ) {
2019-01-08 23:34:45 +11:00
bool has_restarted = false ;
2019-01-07 23:47:11 +11:00
foreach ( AccountContext context in this . accounts . values ) {
2019-01-01 23:22:54 +11:00
Geary . Account account = context . account ;
2019-01-07 23:47:11 +11:00
if ( account . current_status . has_service_problem ( ) & &
( account . incoming . current_status = = type | |
account . outgoing . current_status = = type ) ) {
Geary . ClientService service =
( account . incoming . current_status = = type )
? account . incoming
: account . outgoing ;
2019-01-08 23:34:45 +11:00
bool do_restart = true ;
2019-01-07 23:47:11 +11:00
switch ( type ) {
case AUTHENTICATION_FAILED :
2019-01-08 23:34:45 +11:00
if ( has_restarted ) {
2019-01-07 23:47:11 +11:00
// Only restart at most one at a time, so we
// don't attempt to re-auth multiple bad
// accounts at once.
2019-01-08 23:34:45 +11:00
do_restart = false ;
2019-01-07 23:47:11 +11:00
} else {
// Reset so the infobar does not show up again
context . authentication_failed = false ;
2019-01-08 23:34:45 +11:00
}
break ;
case TLS_VALIDATION_FAILED :
if ( has_restarted ) {
// Only restart at most one at a time, so we
// don't attempt to re-pin multiple bad
// accounts at once.
do_restart = false ;
} else {
// Reset so the infobar does not show up again
context . tls_validation_failed = false ;
2019-01-07 23:47:11 +11:00
}
break ;
}
2019-01-01 23:22:54 +11:00
2019-01-08 23:34:45 +11:00
if ( do_restart ) {
has_restarted = true ;
2019-01-07 23:47:11 +11:00
service . restart . begin ( context . cancellable ) ;
}
2019-01-01 23:22:54 +11:00
}
}
}
Make ConversationMonitor more robust with no/changing connectivity.
Conversation monitor was built around older assumptions of how a folder's
remote connections work - that once a folder opens it will likely also
eventually establish a remote connection, that once the connection is up
it will hang around, and so on.
This patch removes any public notion of (re)seeding, since it can't be
relied to actually happen over the course of the session, ensures that
all folder operations are local-only when the folder does not have a
working remote connection so it doesn't block, and take the opportunity
to reorganise and clean up the monitor API and documentation comments.
* src/engine/app/app-conversation-monitor.vala (ConversationMonitor):
Remove seed signals, and don't bother running an initial reseed if the
folder is already open, since the fill operation will cause any locally
incomplete messages to be filled out from the report. Manage and use an
internal Cancellable for cancelling internal operations when shutting
down. Construct a queue only when starting to monitor conversations,
delete it when stopping. Move as much operation-specific code into the
operations themselves as reasonably possible, making some methods
internal so thy can be accessed from the ops. Ensure all folder listing
operations specify LOCAL_ONLY when the remote is not open. Removed
LocalLoadOperation since that is now redundant. Update the API for
accessing conversations to match Gee conventions and update call
sites. Update documentation comments. Hook back up to locally-complete
signals so we don't miss emails being filled out by the prefetcher, for
now.
* src/engine/app/conversation-monitor/app-conversation-set.vala
(ConversationSet): Rename conversations property to match Gee
conventions, update call sites.
* src/engine/app/conversation-monitor/app-conversation-operation.vala
(ConversationOperation): Allow operations to specify if they should
allow duplicates, and allow the execution method to throw errors, so
they can be handled in a uniform way.
* src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
(ConversationOperationQueue): Accept progress monitor property as a
ctor arg rather than constructing on itself, so it is tied to the
life-cycle of the ConversationMonitor rather than the queue. Add a
signal for notifying of errors thrown when running operations, and use
the new operation-independent support for determining if duplicates
should be queued.
* src/engine/app/conversation-monitor/app-fill-window-operation.vala
(FillWindowOperation): Enforce a maximum window size as well as minimum
to keep loading large windows semi-responsive. Remove code to handle
inserts now that they are handled by their own op.
* src/engine/app/conversation-monitor/app-insert-operation.vala
(InsertOperation): New operation to manage inserts, handle it them by
simply adding them to the conversation if they are newer than the
oldest message, rather that relisting all loaded messages.
2018-03-03 10:56:29 +11:00
private void on_scan_completed ( ) {
// Done scanning. Check if we have enough messages to fill
// the conversation list; if not, trigger a load_more();
if ( ! main_window . conversation_list_has_scrollbar ( ) ) {
debug ( " Not enough messages, loading more for folder %s " , current_folder . to_string ( ) ) ;
on_load_more ( ) ;
}
}
private void on_scan_error ( Geary . App . ConversationMonitor monitor , Error err ) {
// XXX determine the problem better here
2018-08-02 20:27:14 +10:00
Geary . AccountInformation account =
monitor . base_folder . account . information ;
Make ConversationMonitor more robust with no/changing connectivity.
Conversation monitor was built around older assumptions of how a folder's
remote connections work - that once a folder opens it will likely also
eventually establish a remote connection, that once the connection is up
it will hang around, and so on.
This patch removes any public notion of (re)seeding, since it can't be
relied to actually happen over the course of the session, ensures that
all folder operations are local-only when the folder does not have a
working remote connection so it doesn't block, and take the opportunity
to reorganise and clean up the monitor API and documentation comments.
* src/engine/app/app-conversation-monitor.vala (ConversationMonitor):
Remove seed signals, and don't bother running an initial reseed if the
folder is already open, since the fill operation will cause any locally
incomplete messages to be filled out from the report. Manage and use an
internal Cancellable for cancelling internal operations when shutting
down. Construct a queue only when starting to monitor conversations,
delete it when stopping. Move as much operation-specific code into the
operations themselves as reasonably possible, making some methods
internal so thy can be accessed from the ops. Ensure all folder listing
operations specify LOCAL_ONLY when the remote is not open. Removed
LocalLoadOperation since that is now redundant. Update the API for
accessing conversations to match Gee conventions and update call
sites. Update documentation comments. Hook back up to locally-complete
signals so we don't miss emails being filled out by the prefetcher, for
now.
* src/engine/app/conversation-monitor/app-conversation-set.vala
(ConversationSet): Rename conversations property to match Gee
conventions, update call sites.
* src/engine/app/conversation-monitor/app-conversation-operation.vala
(ConversationOperation): Allow operations to specify if they should
allow duplicates, and allow the execution method to throw errors, so
they can be handled in a uniform way.
* src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
(ConversationOperationQueue): Accept progress monitor property as a
ctor arg rather than constructing on itself, so it is tied to the
life-cycle of the ConversationMonitor rather than the queue. Add a
signal for notifying of errors thrown when running operations, and use
the new operation-independent support for determining if duplicates
should be queued.
* src/engine/app/conversation-monitor/app-fill-window-operation.vala
(FillWindowOperation): Enforce a maximum window size as well as minimum
to keep loading large windows semi-responsive. Remove code to handle
inserts now that they are handled by their own op.
* src/engine/app/conversation-monitor/app-insert-operation.vala
(InsertOperation): New operation to manage inserts, handle it them by
simply adding them to the conversation if they are newer than the
oldest message, rather that relisting all loaded messages.
2018-03-03 10:56:29 +11:00
report_problem (
2018-08-02 20:27:14 +10:00
new Geary . ServiceProblemReport (
Make ConversationMonitor more robust with no/changing connectivity.
Conversation monitor was built around older assumptions of how a folder's
remote connections work - that once a folder opens it will likely also
eventually establish a remote connection, that once the connection is up
it will hang around, and so on.
This patch removes any public notion of (re)seeding, since it can't be
relied to actually happen over the course of the session, ensures that
all folder operations are local-only when the folder does not have a
working remote connection so it doesn't block, and take the opportunity
to reorganise and clean up the monitor API and documentation comments.
* src/engine/app/app-conversation-monitor.vala (ConversationMonitor):
Remove seed signals, and don't bother running an initial reseed if the
folder is already open, since the fill operation will cause any locally
incomplete messages to be filled out from the report. Manage and use an
internal Cancellable for cancelling internal operations when shutting
down. Construct a queue only when starting to monitor conversations,
delete it when stopping. Move as much operation-specific code into the
operations themselves as reasonably possible, making some methods
internal so thy can be accessed from the ops. Ensure all folder listing
operations specify LOCAL_ONLY when the remote is not open. Removed
LocalLoadOperation since that is now redundant. Update the API for
accessing conversations to match Gee conventions and update call
sites. Update documentation comments. Hook back up to locally-complete
signals so we don't miss emails being filled out by the prefetcher, for
now.
* src/engine/app/conversation-monitor/app-conversation-set.vala
(ConversationSet): Rename conversations property to match Gee
conventions, update call sites.
* src/engine/app/conversation-monitor/app-conversation-operation.vala
(ConversationOperation): Allow operations to specify if they should
allow duplicates, and allow the execution method to throw errors, so
they can be handled in a uniform way.
* src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
(ConversationOperationQueue): Accept progress monitor property as a
ctor arg rather than constructing on itself, so it is tied to the
life-cycle of the ConversationMonitor rather than the queue. Add a
signal for notifying of errors thrown when running operations, and use
the new operation-independent support for determining if duplicates
should be queued.
* src/engine/app/conversation-monitor/app-fill-window-operation.vala
(FillWindowOperation): Enforce a maximum window size as well as minimum
to keep loading large windows semi-responsive. Remove code to handle
inserts now that they are handled by their own op.
* src/engine/app/conversation-monitor/app-insert-operation.vala
(InsertOperation): New operation to manage inserts, handle it them by
simply adding them to the conversation if they are newer than the
oldest message, rather that relisting all loaded messages.
2018-03-03 10:56:29 +11:00
Geary . ProblemType . GENERIC_ERROR ,
2018-08-02 20:27:14 +10:00
account ,
2018-12-09 10:11:03 +11:00
account . incoming ,
Make ConversationMonitor more robust with no/changing connectivity.
Conversation monitor was built around older assumptions of how a folder's
remote connections work - that once a folder opens it will likely also
eventually establish a remote connection, that once the connection is up
it will hang around, and so on.
This patch removes any public notion of (re)seeding, since it can't be
relied to actually happen over the course of the session, ensures that
all folder operations are local-only when the folder does not have a
working remote connection so it doesn't block, and take the opportunity
to reorganise and clean up the monitor API and documentation comments.
* src/engine/app/app-conversation-monitor.vala (ConversationMonitor):
Remove seed signals, and don't bother running an initial reseed if the
folder is already open, since the fill operation will cause any locally
incomplete messages to be filled out from the report. Manage and use an
internal Cancellable for cancelling internal operations when shutting
down. Construct a queue only when starting to monitor conversations,
delete it when stopping. Move as much operation-specific code into the
operations themselves as reasonably possible, making some methods
internal so thy can be accessed from the ops. Ensure all folder listing
operations specify LOCAL_ONLY when the remote is not open. Removed
LocalLoadOperation since that is now redundant. Update the API for
accessing conversations to match Gee conventions and update call
sites. Update documentation comments. Hook back up to locally-complete
signals so we don't miss emails being filled out by the prefetcher, for
now.
* src/engine/app/conversation-monitor/app-conversation-set.vala
(ConversationSet): Rename conversations property to match Gee
conventions, update call sites.
* src/engine/app/conversation-monitor/app-conversation-operation.vala
(ConversationOperation): Allow operations to specify if they should
allow duplicates, and allow the execution method to throw errors, so
they can be handled in a uniform way.
* src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
(ConversationOperationQueue): Accept progress monitor property as a
ctor arg rather than constructing on itself, so it is tied to the
life-cycle of the ConversationMonitor rather than the queue. Add a
signal for notifying of errors thrown when running operations, and use
the new operation-independent support for determining if duplicates
should be queued.
* src/engine/app/conversation-monitor/app-fill-window-operation.vala
(FillWindowOperation): Enforce a maximum window size as well as minimum
to keep loading large windows semi-responsive. Remove code to handle
inserts now that they are handled by their own op.
* src/engine/app/conversation-monitor/app-insert-operation.vala
(InsertOperation): New operation to manage inserts, handle it them by
simply adding them to the conversation if they are newer than the
oldest message, rather that relisting all loaded messages.
2018-03-03 10:56:29 +11:00
err
)
) ;
}
2019-02-02 12:41:01 +11:00
private void on_email_load_error ( ConversationEmail view , GLib . Error err ) {
// XXX determine the problem better here
report_problem (
new Geary . ServiceProblemReport (
Geary . ProblemType . GENERIC_ERROR ,
this . current_account . information ,
this . current_account . information . incoming ,
err
)
) ;
}
2017-02-11 11:34:30 +11:00
private void on_save_attachments ( Gee . Collection < Geary . Attachment > attachments ) {
2018-10-24 02:14:19 +11:00
GLib . Cancellable ? cancellable = null ;
if ( this . current_account ! = null ) {
cancellable = this . accounts . get (
this . current_account . information
) . cancellable ;
}
2017-02-11 11:34:30 +11:00
if ( attachments . size = = 1 ) {
2018-10-24 02:14:19 +11:00
this . save_attachment_to_file . begin (
attachments . to_array ( ) [ 0 ] , null , cancellable
) ;
2017-02-11 11:34:30 +11:00
} else {
2018-10-24 02:14:19 +11:00
this . save_attachments_to_file . begin ( attachments , cancellable ) ;
2017-02-11 11:34:30 +11:00
}
}
2018-02-06 17:53:35 +11:00
private void on_link_activated ( string uri ) {
if ( uri . down ( ) . has_prefix ( Geary . ComposedEmail . MAILTO_SCHEME ) ) {
2018-02-07 15:44:20 +11:00
compose_mailto ( uri ) ;
2018-02-06 17:53:35 +11:00
} else {
open_uri ( uri ) ;
}
}
2017-02-11 11:34:30 +11:00
private void on_save_image_extended ( ConversationEmail view ,
string url ,
2017-02-16 08:58:58 +11:00
string ? alt_text ,
2017-02-11 11:34:30 +11:00
Geary . Memory . Buffer resource_buf ) {
2018-10-24 02:14:19 +11:00
GLib . Cancellable ? cancellable = null ;
if ( this . current_account ! = null ) {
cancellable = this . accounts . get (
this . current_account . information
) . cancellable ;
}
2017-02-11 11:34:30 +11:00
// This is going to be either an inline image, or a remote
// image, so either treat it as an attachment ot assume we'll
// have a valid filename in the URL
bool handled = false ;
if ( url . has_prefix ( ClientWebView . CID_URL_PREFIX ) ) {
string cid = url . substring ( ClientWebView . CID_URL_PREFIX . length ) ;
Geary . Attachment ? attachment = null ;
try {
attachment = view . email . get_attachment_by_content_id ( cid ) ;
} catch ( Error err ) {
debug ( " Could not get attachment \" %s \" : %s " , cid , err . message ) ;
}
if ( attachment ! = null ) {
2018-10-24 02:14:19 +11:00
this . save_attachment_to_file . begin (
attachment , alt_text , cancellable
) ;
2017-02-11 11:34:30 +11:00
handled = true ;
}
}
if ( ! handled ) {
2018-10-24 02:14:19 +11:00
GLib . File source = GLib . File . new_for_uri ( url ) ;
// Querying the URL-based file for the display name
// results in it being looked up, so just get the basename
// from it directly. GIO seems to decode any %-encoded
// chars anyway.
string ? display_name = source . get_basename ( ) ;
if ( Geary . String . is_empty_or_whitespace ( display_name ) ) {
display_name = GearyController . untitled_file_name ;
}
2017-02-11 11:34:30 +11:00
this . prompt_save_buffer . begin (
2018-10-24 02:14:19 +11:00
display_name , resource_buf , cancellable
) ;
2017-02-11 11:34:30 +11:00
}
}
2011-11-22 18:55:07 -08:00
2017-11-08 18:13:51 +11:00
}