Merge branch 'master' into bug/5224-dovecot

This commit is contained in:
Jim Nelson 2013-06-07 18:17:45 -07:00
commit f246ff4919
7 changed files with 445 additions and 341 deletions

View file

@ -112,7 +112,7 @@ public class AccountDialog : Gtk.Dialog {
// Check for open composer windows.
bool composer_window_found = false;
Gee.List<ComposerWindow>? windows =
GearyApplication.instance.get_composer_windows_for_account(account);
GearyApplication.instance.controller.get_composer_windows_for_account(account);
if (windows != null) {
foreach (ComposerWindow cw in windows) {
@ -139,7 +139,7 @@ public class AccountDialog : Gtk.Dialog {
assert(account != null); // Should not be able to happen since we checked earlier.
// Remove account, then set the page back to the account list.
GearyApplication.instance.remove_account_async.begin(account, null, () => {
GearyApplication.instance.controller.remove_account_async.begin(account, null, () => {
account_list_pane.present(); });
}
@ -151,7 +151,7 @@ public class AccountDialog : Gtk.Dialog {
bool validate_connection = true;
if (add_edit_pane.get_mode() == AddEditPage.PageMode.EDIT && info.is_copy()) {
Geary.AccountInformation? real_info =
GearyApplication.instance.get_real_account_information(info);
GearyApplication.instance.controller.get_real_account_information(info);
if (real_info != null) {
validate_connection = !real_info.imap_credentials.equal_to(info.imap_credentials) ||
(info.smtp_credentials != null && !real_info.smtp_credentials.equal_to(info.smtp_credentials));
@ -159,13 +159,13 @@ public class AccountDialog : Gtk.Dialog {
}
// Validate account.
GearyApplication.instance.validate_async.begin(info, validate_connection, null,
GearyApplication.instance.controller.validate_async.begin(info, validate_connection, null,
on_save_add_or_edit_completed);
}
private void on_save_add_or_edit_completed(Object? source, AsyncResult result) {
Geary.Engine.ValidationResult validation_result =
GearyApplication.instance.validate_async.end(result);
GearyApplication.instance.controller.validate_async.end(result);
// If account was successfully added return to the account list. Otherwise, go back to the
// account add page so the user can try again.

View file

@ -96,7 +96,7 @@ public class FolderList.FolderEntry : Geary.BaseObject, Sidebar.Entry, Sidebar.I
Gdk.ModifierType mask;
double[] axes = new double[2];
context.get_device().get_state(context.get_dest_window(), axes, out mask);
MainWindow main_window = GearyApplication.instance.get_main_window() as MainWindow;
MainWindow main_window = GearyApplication.instance.controller.main_window;
if ((mask & Gdk.ModifierType.CONTROL_MASK) != 0) {
main_window.folder_list.copy_conversation(folder);
} else {

View file

@ -67,6 +67,8 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
}
}
public GearyController controller { get; private set; default = new GearyController(); }
public Gtk.ActionGroup actions {
get; private set; default = new Gtk.ActionGroup("GearyActionGroup");
}
@ -79,10 +81,6 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
private static GearyApplication _instance = null;
private GearyController? controller = null;
private LoginDialog? login_dialog = null;
private File exec_dir;
public GearyApplication() {
@ -107,9 +105,7 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
}
public override bool exiting(bool panicked) {
if (controller.main_window != null)
controller.main_window.destroy();
controller.close();
Date.terminate();
return true;
@ -136,22 +132,9 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
message("%s %s prefix=%s exec_dir=%s is_installed=%s", NAME, VERSION, INSTALL_PREFIX,
exec_dir.get_path(), is_installed().to_string());
Geary.Engine.instance.account_available.connect(on_account_available);
Geary.Engine.instance.account_unavailable.connect(on_account_unavailable);
config = new Configuration();
controller = new GearyController();
yield controller.open_async();
// Start Geary.
try {
yield Geary.Engine.instance.open_async(get_user_data_directory(), get_resource_directory(),
new SecretMediator());
if (Geary.Engine.instance.get_accounts().size == 0)
create_account();
} catch (Error e) {
error("Error opening Geary.Engine instance: %s", e.message);
}
handle_args(args);
}
@ -163,204 +146,6 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
return action;
}
private void open_account(Geary.Account account) {
account.report_problem.connect(on_report_problem);
controller.connect_account_async.begin(account);
}
private void close_account(Geary.Account account) {
account.report_problem.disconnect(on_report_problem);
controller.disconnect_account_async.begin(account);
}
private Geary.Account get_account_instance(Geary.AccountInformation account_information) {
try {
return Geary.Engine.instance.get_account_instance(account_information);
} catch (Error e) {
error("Error creating account instance: %s", e.message);
}
}
private void on_account_available(Geary.AccountInformation account_information) {
open_account(get_account_instance(account_information));
}
private void on_account_unavailable(Geary.AccountInformation account_information) {
close_account(get_account_instance(account_information));
}
private void create_account() {
Geary.AccountInformation? account_information = request_account_information(null);
if (account_information != null)
do_validate_until_successful_async.begin(account_information);
}
private async void do_validate_until_successful_async(Geary.AccountInformation account_information,
Cancellable? cancellable = null) {
Geary.AccountInformation? result = account_information;
do {
result = yield validate_or_retry_async(result, cancellable);
} while (result != null);
if (login_dialog != null)
login_dialog.hide();
}
// Returns null if we are done validating, or the revised account information if we should retry.
private async Geary.AccountInformation? validate_or_retry_async(Geary.AccountInformation account_information,
Cancellable? cancellable = null) {
Geary.Engine.ValidationResult result = yield validate_async(account_information, true, cancellable);
if (result == Geary.Engine.ValidationResult.OK)
return null;
debug("Validation failed. Prompting user for revised account information");
Geary.AccountInformation? new_account_information =
request_account_information(account_information, result);
// If the user refused to enter account information. There is currently no way that we
// could see this--we exit in request_account_information, and the only way that an
// exit could be canceled is if there are unsaved composer windows open (which won't
// happen before an account is created). However, best to include this check for the
// future.
if (new_account_information == null)
return null;
debug("User entered revised account information, retrying validation");
return new_account_information;
}
// Attempts to validate and add an account. Returns a result code indicating
// success or one or more errors.
public async Geary.Engine.ValidationResult validate_async(
Geary.AccountInformation account_information, bool validate_connection,
Cancellable? cancellable = null) {
Geary.Engine.ValidationResult result = Geary.Engine.ValidationResult.OK;
try {
result = yield Geary.Engine.instance.validate_account_information_async(account_information,
validate_connection, cancellable);
} catch (Error err) {
debug("Error validating account: %s", err.message);
exit(-1); // Fatal error
return result;
}
if (result == Geary.Engine.ValidationResult.OK) {
Geary.AccountInformation real_account_information = account_information;
if (account_information.is_copy()) {
// We have a temporary copy of the account. Find the "real" acct info object and
// copy the new data into it.
real_account_information = get_real_account_information(account_information);
real_account_information.copy_from(account_information);
}
real_account_information.store_async.begin(cancellable);
do_update_stored_passwords_async.begin(Geary.CredentialsMediator.ServiceFlag.IMAP |
Geary.CredentialsMediator.ServiceFlag.SMTP, real_account_information);
debug("Successfully validated account information");
}
return result;
}
// Returns the "real" account info associated with a copy. If it's not a copy, null is returned.
public Geary.AccountInformation? get_real_account_information(
Geary.AccountInformation account_information) {
if (account_information.is_copy()) {
try {
return Geary.Engine.instance.get_accounts().get(account_information.email);
} catch (Error e) {
error("Account information is out of sync: %s", e.message);
}
}
return null;
}
// Prompt the user for a service, real name, username, and password, and try to start Geary.
private Geary.AccountInformation? request_account_information(Geary.AccountInformation? old_info,
Geary.Engine.ValidationResult result = Geary.Engine.ValidationResult.OK) {
Geary.AccountInformation? new_info = old_info;
if (login_dialog == null)
login_dialog = new LoginDialog(); // Create here so we know GTK is initialized.
if (new_info != null)
login_dialog.set_account_information(new_info, result);
login_dialog.present();
for (;;) {
login_dialog.show_spinner(false);
if (login_dialog.run() != Gtk.ResponseType.OK) {
debug("User refused to enter account information. Exiting...");
exit(1);
return null;
}
login_dialog.show_spinner(true);
new_info = login_dialog.get_account_information();
if ((!new_info.default_imap_server_ssl && !new_info.default_imap_server_starttls)
|| (!new_info.default_smtp_server_ssl && !new_info.default_smtp_server_starttls)) {
ConfirmationDialog security_dialog = new ConfirmationDialog(controller.main_window,
_("Your settings are insecure"),
_("Your IMAP and/or SMTP settings do not specify SSL or TLS. This means your username and password could be read by another person on the network. Are you sure you want to do this?"),
_("Co_ntinue"));
if (security_dialog.run() != Gtk.ResponseType.OK)
continue;
}
break;
}
do_update_stored_passwords_async.begin(Geary.CredentialsMediator.ServiceFlag.IMAP |
Geary.CredentialsMediator.ServiceFlag.SMTP, new_info);
return new_info;
}
private async void do_update_stored_passwords_async(Geary.CredentialsMediator.ServiceFlag services,
Geary.AccountInformation account_information) {
try {
yield account_information.update_stored_passwords_async(services);
} catch (Error e) {
debug("Error updating stored passwords: %s", e.message);
}
}
private void on_report_problem(Geary.Account account, Geary.Account.Problem problem, Error? err) {
debug("Reported problem: %s Error: %s", problem.to_string(), err != null ? err.message : "(N/A)");
switch (problem) {
case Geary.Account.Problem.DATABASE_FAILURE:
case Geary.Account.Problem.HOST_UNREACHABLE:
case Geary.Account.Problem.NETWORK_UNAVAILABLE:
// TODO
break;
case Geary.Account.Problem.RECV_EMAIL_LOGIN_FAILED:
case Geary.Account.Problem.SEND_EMAIL_LOGIN_FAILED:
// At this point, we've prompted them for the password and
// they've hit cancel, so there's not much for us to do here.
close_account(account);
break;
default:
assert_not_reached();
}
}
// Removes an existing account.
public async void remove_account_async(Geary.AccountInformation account,
Cancellable? cancellable = null) {
try {
yield GearyApplication.instance.get_account_instance(account).close_async(cancellable);
yield Geary.Engine.instance.remove_account_async(account, cancellable);
} catch (Error e) {
message("Error removing account: %s", e.message);
}
}
public File get_user_data_directory() {
return File.new_for_path(Environment.get_user_data_dir()).get_child("geary");
}
@ -455,10 +240,6 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
load_ui_file_for_manager(ui_manager, ui_filename);
}
public Gtk.Window get_main_window() {
return controller.main_window;
}
private void handle_args(string[] args) {
foreach(string arg in args) {
if (arg.has_prefix(Geary.ComposedEmail.MAILTO_SCHEME)) {
@ -466,9 +247,5 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
}
}
}
public Gee.List<ComposerWindow>? get_composer_windows_for_account(Geary.AccountInformation account) {
return controller.get_composer_windows_for_account(account);
}
}

View file

@ -81,8 +81,35 @@ public class GearyController {
private Geary.Folder? folder_to_select = null;
private Geary.Nonblocking.Mutex select_folder_mutex = new Geary.Nonblocking.Mutex();
private Geary.Account? account_to_select = null;
private LoginDialog? login_dialog = null;
/**
* 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);
/**
* Fired when the currently selected conversation(s) has/have changed.
*/
public signal void conversations_selected(Gee.Set<Geary.Conversation>? conversations,
Geary.Folder? current_folder);
public GearyController() {
}
~GearyController() {
assert(current_account == null);
}
/**
* Starts the controller and brings up Geary.
*/
public async void open_async() {
// This initializes the IconFactory, important to do before the actions are created (as they
// refer to some of Geary's custom icons)
IconFactory.instance.init();
@ -105,7 +132,10 @@ public class GearyController {
main_window.notify["has-toplevel-focus"].connect(on_has_toplevel_focus);
enable_message_buttons(false);
Geary.Engine.instance.account_available.connect(on_account_available);
Geary.Engine.instance.account_unavailable.connect(on_account_unavailable);
// Connect to various UI signals.
main_window.conversation_list_view.conversations_selected.connect(on_conversations_selected);
main_window.conversation_list_view.load_more.connect(on_load_more);
@ -146,13 +176,30 @@ public class GearyController {
set_busy(false);
main_window.show_all();
// Start Geary.
try {
yield Geary.Engine.instance.open_async(GearyApplication.instance.get_user_data_directory(),
GearyApplication.instance.get_resource_directory(), new SecretMediator());
if (Geary.Engine.instance.get_accounts().size == 0) {
create_account();
} else {
main_window.show_all();
}
} catch (Error e) {
error("Error opening Geary.Engine instance: %s", e.message);
}
}
~GearyController() {
assert(current_account == null);
/**
* Stops the controller and shuts down Geary.
*/
public void close() {
main_window.destroy();
main_window = null;
current_account = null;
account_selected(null);
}
private void add_accelerator(string accelerator, string action) {
GtkUtil.add_accelerator(GearyApplication.instance.ui_manager, GearyApplication.instance.actions,
accelerator, action);
@ -307,9 +354,211 @@ public class GearyController {
GearyApplication.instance.get_action(ACTION_DELETE_MESSAGE).is_important = true;
}
private void open_account(Geary.Account account) {
account.report_problem.connect(on_report_problem);
connect_account_async.begin(account);
}
private void close_account(Geary.Account account) {
account.report_problem.disconnect(on_report_problem);
disconnect_account_async.begin(account);
}
private Geary.Account get_account_instance(Geary.AccountInformation account_information) {
try {
return Geary.Engine.instance.get_account_instance(account_information);
} catch (Error e) {
error("Error creating account instance: %s", e.message);
}
}
private void on_account_available(Geary.AccountInformation account_information) {
open_account(get_account_instance(account_information));
}
private void on_account_unavailable(Geary.AccountInformation account_information) {
close_account(get_account_instance(account_information));
}
private void create_account() {
Geary.AccountInformation? account_information = request_account_information(null);
if (account_information != null)
do_validate_until_successful_async.begin(account_information);
}
private async void do_validate_until_successful_async(Geary.AccountInformation account_information,
Cancellable? cancellable = null) {
Geary.AccountInformation? result = account_information;
do {
result = yield validate_or_retry_async(result, cancellable);
} while (result != null);
if (main_window != null) {
main_window.show_all();
}
if (login_dialog != null)
login_dialog.hide();
}
// Returns null if we are done validating, or the revised account information if we should retry.
private async Geary.AccountInformation? validate_or_retry_async(Geary.AccountInformation account_information,
Cancellable? cancellable = null) {
Geary.Engine.ValidationResult result = yield validate_async(account_information, true, cancellable);
if (result == Geary.Engine.ValidationResult.OK)
return null;
debug("Validation failed. Prompting user for revised account information");
Geary.AccountInformation? new_account_information =
request_account_information(account_information, result);
// If the user refused to enter account information. There is currently no way that we
// could see this--we exit in request_account_information, and the only way that an
// exit could be canceled is if there are unsaved composer windows open (which won't
// happen before an account is created). However, best to include this check for the
// future.
if (new_account_information == null)
return null;
debug("User entered revised account information, retrying validation");
return new_account_information;
}
// Attempts to validate and add an account. Returns a result code indicating
// success or one or more errors.
public async Geary.Engine.ValidationResult validate_async(
Geary.AccountInformation account_information, bool validate_connection,
Cancellable? cancellable = null) {
Geary.Engine.ValidationResult result = Geary.Engine.ValidationResult.OK;
try {
result = yield Geary.Engine.instance.validate_account_information_async(account_information,
validate_connection, cancellable);
} catch (Error err) {
debug("Error validating account: %s", err.message);
GearyApplication.instance.exit(-1); // Fatal error
return result;
}
if (result == Geary.Engine.ValidationResult.OK) {
Geary.AccountInformation real_account_information = account_information;
if (account_information.is_copy()) {
// We have a temporary copy of the account. Find the "real" acct info object and
// copy the new data into it.
real_account_information = get_real_account_information(account_information);
real_account_information.copy_from(account_information);
}
real_account_information.store_async.begin(cancellable);
do_update_stored_passwords_async.begin(Geary.CredentialsMediator.ServiceFlag.IMAP |
Geary.CredentialsMediator.ServiceFlag.SMTP, real_account_information);
debug("Successfully validated account information");
}
return result;
}
// Returns the "real" account info associated with a copy. If it's not a copy, null is returned.
public Geary.AccountInformation? get_real_account_information(
Geary.AccountInformation account_information) {
if (account_information.is_copy()) {
try {
return Geary.Engine.instance.get_accounts().get(account_information.email);
} catch (Error e) {
error("Account information is out of sync: %s", e.message);
}
}
return null;
}
// Prompt the user for a service, real name, username, and password, and try to start Geary.
private Geary.AccountInformation? request_account_information(Geary.AccountInformation? old_info,
Geary.Engine.ValidationResult result = Geary.Engine.ValidationResult.OK) {
Geary.AccountInformation? new_info = old_info;
if (login_dialog == null)
login_dialog = new LoginDialog(); // Create here so we know GTK is initialized.
if (new_info != null)
login_dialog.set_account_information(new_info, result);
login_dialog.present();
for (;;) {
login_dialog.show_spinner(false);
if (login_dialog.run() != Gtk.ResponseType.OK) {
debug("User refused to enter account information. Exiting...");
GearyApplication.instance.exit(1);
return null;
}
login_dialog.show_spinner(true);
new_info = login_dialog.get_account_information();
if ((!new_info.default_imap_server_ssl && !new_info.default_imap_server_starttls)
|| (!new_info.default_smtp_server_ssl && !new_info.default_smtp_server_starttls)) {
ConfirmationDialog security_dialog = new ConfirmationDialog(main_window,
_("Your settings are insecure"),
_("Your IMAP and/or SMTP settings do not specify SSL or TLS. This means your username and password could be read by another person on the network. Are you sure you want to do this?"),
_("Co_ntinue"));
if (security_dialog.run() != Gtk.ResponseType.OK)
continue;
}
break;
}
do_update_stored_passwords_async.begin(Geary.CredentialsMediator.ServiceFlag.IMAP |
Geary.CredentialsMediator.ServiceFlag.SMTP, new_info);
return new_info;
}
private async void do_update_stored_passwords_async(Geary.CredentialsMediator.ServiceFlag services,
Geary.AccountInformation account_information) {
try {
yield account_information.update_stored_passwords_async(services);
} catch (Error e) {
debug("Error updating stored passwords: %s", e.message);
}
}
private void on_report_problem(Geary.Account account, Geary.Account.Problem problem, Error? err) {
debug("Reported problem: %s Error: %s", problem.to_string(), err != null ? err.message : "(N/A)");
switch (problem) {
case Geary.Account.Problem.DATABASE_FAILURE:
case Geary.Account.Problem.HOST_UNREACHABLE:
case Geary.Account.Problem.NETWORK_UNAVAILABLE:
// TODO
break;
case Geary.Account.Problem.RECV_EMAIL_LOGIN_FAILED:
case Geary.Account.Problem.SEND_EMAIL_LOGIN_FAILED:
// At this point, we've prompted them for the password and
// they've hit cancel, so there's not much for us to do here.
close_account(account);
break;
default:
assert_not_reached();
}
}
// Removes an existing account.
public async void remove_account_async(Geary.AccountInformation account,
Cancellable? cancellable = null) {
try {
yield get_account_instance(account).close_async(cancellable);
yield remove_account_async(account, cancellable);
} catch (Error e) {
message("Error removing account: %s", e.message);
}
}
public async void connect_account_async(Geary.Account account, Cancellable? cancellable = null) {
account.folders_available_unavailable.connect(on_folders_available_unavailable);
try {
yield account.open_async(cancellable);
} catch (Error open_err) {
@ -379,11 +628,6 @@ public class GearyController {
return num;
}
private bool is_viewed_conversation(Geary.Conversation? conversation) {
return conversation != null && selected_conversations.size > 0 &&
Geary.Collection.get_first<Geary.Conversation>(selected_conversations) == conversation;
}
// Update widgets and such to match capabilities of the current folder ... sensitivity is handled
// by other utility methods
private void update_ui() {
@ -407,7 +651,7 @@ public class GearyController {
if (folder == null) {
current_folder = null;
main_window.conversation_list_store.clear();
main_window.conversation_viewer.clear(null, null);
folder_selected(null);
return;
}
@ -434,6 +678,9 @@ public class GearyController {
}
private async void do_select_folder(Geary.Folder folder) throws Error {
if (folder == current_folder)
return;
set_busy(true);
cancel_folder();
@ -462,7 +709,12 @@ public class GearyController {
debug("switching to %s", folder.to_string());
current_folder = folder;
current_account = folder.account;
if (current_account != folder.account) {
current_account = folder.account;
account_selected(current_account);
}
folder_selected(current_folder);
main_window.conversation_list_store.set_current_folder(current_folder, conversation_cancellable);
main_window.conversation_list_store.account_owner_email = current_account.information.email;
@ -488,9 +740,6 @@ public class GearyController {
current_conversations.scan_error.connect(on_scan_error);
current_conversations.scan_completed.connect(on_scan_completed);
current_conversations.seed_completed.connect(on_seed_completed);
current_conversations.conversation_appended.connect(on_conversation_appended);
current_conversations.conversation_trimmed.connect(on_conversation_trimmed);
current_conversations.email_flags_changed.connect(on_email_flags_changed);
main_window.conversation_list_store.set_conversation_monitor(current_conversations);
main_window.conversation_list_view.set_conversation_monitor(current_conversations);
@ -551,28 +800,11 @@ public class GearyController {
main_window.folder_list.select_folder(folder);
}
private void on_conversation_appended(Geary.Conversation conversation,
Gee.Collection<Geary.Email> email) {
if (is_viewed_conversation(conversation)) {
do_show_message.begin(conversation.get_emails(Geary.Conversation.Ordering.NONE), cancellable_message,
false, on_show_message_completed);
}
}
private void on_conversation_trimmed(Geary.Conversation conversation, Geary.Email email) {
if (is_viewed_conversation(conversation))
main_window.conversation_viewer.remove_message(email);
}
private void on_load_more() {
debug("on_load_more");
current_conversations.min_window_count += MIN_CONVERSATION_COUNT;
}
private void on_email_flags_changed(Geary.Conversation conversation, Geary.Email email) {
main_window.conversation_viewer.update_flags(email);
}
private void on_select_folder_completed(Object? source, AsyncResult result) {
try {
do_select_folder.end(result);
@ -583,17 +815,14 @@ public class GearyController {
private void on_conversations_selected(Gee.Set<Geary.Conversation> selected) {
cancel_message();
selected_conversations = selected;
conversations_selected(selected_conversations, current_folder);
// Disable message buttons until conversation loads.
enable_message_buttons(false);
if (selected.size == 1 && current_folder != null) {
Geary.Conversation conversation = Geary.Collection.get_first(selected);
do_show_message.begin(conversation.get_emails(Geary.Conversation.Ordering.DATE_ASCENDING),
cancellable_message, true, on_conversation_selected_completed);
} else if (current_folder != null) {
if (selected.size > 1 && current_folder != null) {
main_window.conversation_viewer.show_multiple_selected(selected.size);
if (selected.size > 1) {
enable_multiple_message_buttons();
@ -603,62 +832,6 @@ public class GearyController {
}
}
private async void do_show_message(Gee.Collection<Geary.Email> messages, Cancellable?
cancellable = null, bool clear_view = true) throws Error {
set_busy(true);
// Clear view before we yield, to make sure it happens
if (clear_view) {
main_window.conversation_viewer.clear(current_folder, current_account.information);
main_window.conversation_viewer.scroll_reset();
}
// Fetch full messages.
Gee.Collection<Geary.Email> messages_to_add = new Gee.HashSet<Geary.Email>();
foreach (Geary.Email email in messages) {
Geary.Email.Field required_fields = ConversationViewer.REQUIRED_FIELDS |
Geary.ComposedEmail.REQUIRED_REPLY_FIELDS;
Geary.Email full_email;
if (email.id.get_folder_path() == null) {
full_email = yield current_folder.account.local_fetch_email_async(
email.id, required_fields, cancellable);
} else {
full_email = yield current_folder.fetch_email_async(email.id,
required_fields, Geary.Folder.ListFlags.NONE, cancellable);
}
if (cancellable.is_cancelled())
throw new IOError.CANCELLED("do_select_message cancelled");
messages_to_add.add(full_email);
}
// Add messages. conversation_viewer.add_message only adds new messages
foreach (Geary.Email email in messages_to_add)
main_window.conversation_viewer.add_message(email);
main_window.conversation_viewer.unhide_last_email();
main_window.conversation_viewer.compress_emails();
}
private void on_show_message_completed(Object? source, AsyncResult result) {
try {
do_show_message.end(result);
enable_message_buttons(true);
} catch (Error err) {
if (!(err is IOError.CANCELLED))
debug("Unable to show message: %s", err.message);
}
set_busy(false);
}
private void on_conversation_selected_completed(Object? source, AsyncResult result) {
on_show_message_completed(source, result);
main_window.conversation_viewer.mark_read();
}
private void on_special_folder_type_changed(Geary.Folder folder, Geary.SpecialFolderType old_type,
Geary.SpecialFolderType new_type) {
main_window.folder_list.remove_folder(folder);
@ -1285,7 +1458,7 @@ public class GearyController {
// one or the other in a folder
private void on_delete_message() {
// Prevent deletes of the same conversation from repeating.
if (is_viewed_conversation(last_deleted_conversation)) {
if (main_window.conversation_viewer.current_conversation ==last_deleted_conversation) {
debug("not archiving/deleting, viewed conversation is last deleted conversation");
return;

View file

@ -47,6 +47,9 @@ public class ConversationViewer : Gtk.Box {
// The HTML viewer to view the emails.
public ConversationWebView web_view { get; private set; }
// Current conversation, or null if none.
public Geary.Conversation? current_conversation = null;
// Label for displaying overlay messages.
private Gtk.Label message_overlay_label;
@ -61,12 +64,16 @@ public class ConversationViewer : Gtk.Box {
private weak Geary.Folder? current_folder = null;
private Geary.AccountInformation? current_account_information = null;
private ConversationFindBar conversation_find_bar;
private Cancellable cancellable_fetch = new Cancellable();
public ConversationViewer() {
Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0);
web_view = new ConversationWebView();
GearyApplication.instance.controller.conversations_selected.connect(on_conversations_selected);
GearyApplication.instance.controller.folder_selected.connect(on_folder_selected);
web_view.hovering_over_link.connect(on_hovering_over_link);
web_view.context_menu.connect(() => { return true; }); // Suppress default context menu.
web_view.realize.connect( () => { web_view.get_vadjustment().value_changed.connect(mark_read); });
@ -100,7 +107,7 @@ public class ConversationViewer : Gtk.Box {
}
// Removes all displayed e-mails from the view.
public void clear(Geary.Folder? new_folder, Geary.AccountInformation? account_information) {
private void clear(Geary.Folder? new_folder, Geary.AccountInformation? account_information) {
// Remove all messages from DOM.
try {
foreach (WebKit.DOM.HTMLElement element in email_to_element.values) {
@ -145,7 +152,115 @@ public class ConversationViewer : Gtk.Box {
}
}
public void add_message(Geary.Email email) {
private void on_folder_selected(Geary.Folder? folder) {
if (folder == null) {
clear(null, null);
current_conversation = null;
}
}
private void on_conversations_selected(Gee.Set<Geary.Conversation>? conversations,
Geary.Folder? current_folder) {
cancel_load();
if (current_conversation != null) {
current_conversation.appended.disconnect(on_conversation_appended);
current_conversation.trimmed.disconnect(on_conversation_trimmed);
current_conversation.email_flags_changed.disconnect(update_flags);
}
if (conversations != null && conversations.size == 1 && current_folder != null) {
// Clear view before we yield, to make sure it happens.
clear(current_folder, current_folder.account.information);
web_view.scroll_reset();
GearyApplication.instance.controller.enable_message_buttons(false);
current_conversation = Geary.Collection.get_first(conversations);
select_conversation_async.begin(current_conversation, current_folder,
on_select_conversation_completed);
current_conversation.appended.connect(on_conversation_appended);
current_conversation.trimmed.connect(on_conversation_trimmed);
current_conversation.email_flags_changed.connect(update_flags);
} else if (conversations == null || conversations.size == 0) {
current_conversation = null;
}
}
private async void select_conversation_async(Geary.Conversation conversation,
Geary.Folder current_folder) throws Error {
Gee.Collection<Geary.Email> messages = conversation.get_emails(Geary.Conversation.Ordering.DATE_ASCENDING);
// Fetch full messages.
Gee.Collection<Geary.Email> messages_to_add = new Gee.HashSet<Geary.Email>();
foreach (Geary.Email email in messages)
messages_to_add.add(yield fetch_full_message_async(email));
// Add messages.
foreach (Geary.Email email in messages_to_add)
add_message(email);
unhide_last_email();
compress_emails();
}
private void on_select_conversation_completed(Object? source, AsyncResult result) {
try {
select_conversation_async.end(result);
GearyApplication.instance.controller.enable_message_buttons(true);
mark_read();
} catch (Error err) {
debug("Unable to select conversation: %s", err.message);
}
}
// Given an email, fetch the full version with all required fields.
private async Geary.Email fetch_full_message_async(Geary.Email email) throws Error {
Geary.Email.Field required_fields = ConversationViewer.REQUIRED_FIELDS |
Geary.ComposedEmail.REQUIRED_REPLY_FIELDS;
Geary.Email full_email;
if (email.id.get_folder_path() == null) {
full_email = yield current_folder.account.local_fetch_email_async(
email.id, required_fields, cancellable_fetch);
} else {
full_email = yield current_folder.fetch_email_async(email.id,
required_fields, Geary.Folder.ListFlags.NONE, cancellable_fetch);
}
return full_email;
}
// Cancels the current message load, if in progress.
private void cancel_load() {
Cancellable old_cancellable = cancellable_fetch;
cancellable_fetch = new Cancellable();
old_cancellable.cancel();
}
private void on_conversation_appended(Geary.Email email) {
on_conversation_appended_async.begin(email, on_conversation_appended_complete);
}
private async void on_conversation_appended_async(Geary.Email email) throws Error {
add_message(yield fetch_full_message_async(email));
}
private void on_conversation_appended_complete(Object? source, AsyncResult result) {
try {
on_conversation_appended_async.end(result);
} catch (Error err) {
debug("Unable to append email to conversation: %s", err.message);
}
}
private void on_conversation_trimmed(Geary.Email email) {
remove_message(email);
}
private void add_message(Geary.Email email) {
// Make sure the message container is showing and the multi-message counter hidden.
try {
web_view.show_element_by_id(MESSAGE_CONTAINER_ID);
@ -368,7 +483,7 @@ public class ConversationViewer : Gtk.Box {
}
}
public void unhide_last_email() {
private void unhide_last_email() {
WebKit.DOM.HTMLElement last_email = (WebKit.DOM.HTMLElement) web_view.container.get_last_child().previous_sibling;
if (last_email != null) {
WebKit.DOM.DOMTokenList class_list = last_email.get_class_list();
@ -380,7 +495,7 @@ public class ConversationViewer : Gtk.Box {
}
}
public void compress_emails() {
private void compress_emails() {
if (messages.size == 0)
return;
@ -435,7 +550,7 @@ public class ConversationViewer : Gtk.Box {
}
}
public void decompress_emails(WebKit.DOM.Element email_element) {
private void decompress_emails(WebKit.DOM.Element email_element) {
WebKit.DOM.Element iter_element = email_element;
try {
while ((iter_element != null) && iter_element.get_class_list().contains("compressed")) {
@ -517,7 +632,7 @@ public class ConversationViewer : Gtk.Box {
}
}
public void update_flags(Geary.Email email) {
private void update_flags(Geary.Email email) {
// Nothing to do if we aren't displaying this email.
if (!email_to_element.has_key(email.id)) {
return;
@ -1263,7 +1378,7 @@ public class ConversationViewer : Gtk.Box {
parent.append_child(signature_container);
}
public void remove_message(Geary.Email email) {
private void remove_message(Geary.Email email) {
if (!messages.contains(email))
return;
@ -1391,10 +1506,6 @@ public class ConversationViewer : Gtk.Box {
}
}
public void scroll_reset() {
web_view.scroll_reset();
}
private void on_hovering_over_link(string? title, string? url) {
// Copy the link the user is hovering over. Note that when the user mouses-out,
// this signal is called again with null for both parameters.
@ -1447,7 +1558,7 @@ public class ConversationViewer : Gtk.Box {
string temporary_uri = Filename.to_uri(temporary_filename, null);
Gtk.show_uri(web_view.get_screen(), temporary_uri, Gdk.CURRENT_TIME);
} catch (Error error) {
ErrorDialog dialog = new ErrorDialog(GearyApplication.instance.get_main_window(),
ErrorDialog dialog = new ErrorDialog(GearyApplication.instance.controller.main_window,
_("Failed to open default text editor."), error.message);
dialog.run();
}

View file

@ -38,9 +38,17 @@ public class Geary.ConversationMonitor : BaseObject {
convnum = next_convnum++;
this.owner = owner;
lowest_id = null;
owner.email_flags_changed.connect(on_email_flags_changed);
}
~ImplConversation() {
clear_owner();
}
public void clear_owner() {
if (owner != null)
owner.email_flags_changed.disconnect(on_email_flags_changed);
owner = null;
}
@ -108,6 +116,7 @@ public class Geary.ConversationMonitor : BaseObject {
message_ids.add_all(ancestors);
check_lowest_id(email.id);
notify_appended(email);
}
// Returns the removed Message-IDs
@ -132,6 +141,8 @@ public class Geary.ConversationMonitor : BaseObject {
foreach (Email e in emails.values)
check_lowest_id(e.id);
notify_trimmed(email);
return (removed_message_ids.size > 0) ? removed_message_ids : null;
}
@ -143,6 +154,11 @@ public class Geary.ConversationMonitor : BaseObject {
public string to_string() {
return "[#%d] (%d emails)".printf(convnum, emails.size);
}
private void on_email_flags_changed(Geary.Conversation conversation, Geary.Email email) {
if (conversation == this)
notify_email_flags_changed(email);
}
}
private class LocalSearchOperation : Nonblocking.BatchOperation {

View file

@ -11,6 +11,21 @@ public abstract class Geary.Conversation : BaseObject {
DATE_DESCENDING,
}
/**
* Fired when email has been added to this conversation.
*/
public signal void appended(Geary.Email email);
/**
* Fired when email has been trimmed from this conversation.
*/
public signal void trimmed(Geary.Email email);
/**
* Fired when the flags of an email in this conversation have changed.
*/
public signal void email_flags_changed(Geary.Email email);
protected Conversation() {
}
@ -142,5 +157,17 @@ public abstract class Geary.Conversation : BaseObject {
private bool is_missing_flag(Geary.NamedFlag flag) {
return check_flag(flag, false);
}
protected void notify_appended(Geary.Email email) {
appended(email);
}
protected void notify_trimmed(Geary.Email email) {
trimmed(email);
}
protected void notify_email_flags_changed(Geary.Email email) {
email_flags_changed(email);
}
}