Mark messages as read when clicked. Closes #4476

This commit is contained in:
Eric Gregory 2011-12-07 18:46:05 -08:00
parent 2109708146
commit fe099d9fb9
16 changed files with 297 additions and 21 deletions

View file

@ -203,7 +203,7 @@ public class GearyController {
current_folder = folder;
yield current_folder.open_async(true, cancellable_folder);
yield current_folder.open_async(false, cancellable_folder);
current_conversations = new Geary.Conversations(current_folder,
MessageListStore.REQUIRED_FIELDS);
@ -351,6 +351,7 @@ public class GearyController {
private async void do_select_message(Geary.Conversation conversation, Cancellable?
cancellable = null) throws Error {
Gee.List<Geary.EmailIdentifier> messages = new Gee.ArrayList<Geary.EmailIdentifier>();
if (current_folder == null) {
debug("Conversation selected with no folder selected");
@ -366,7 +367,15 @@ public class GearyController {
break;
main_window.message_viewer.add_message(full_email);
if (full_email.properties.email_flags.is_unread())
messages.add(full_email.id);
}
// Mark as read.
if (messages.size > 0)
yield current_folder.mark_email_async(messages, Geary.EmailProperties.EmailFlags.NONE,
Geary.EmailProperties.EmailFlags.UNREAD, cancellable);
}
private void on_select_message_completed(Object? source, AsyncResult result) {

View file

@ -128,7 +128,7 @@ public class MessageListStore : Gtk.TreeStore {
// If it exists, return oldest unread message.
foreach (Geary.Email email in pool)
if (email.properties.is_unread())
if (email.properties.email_flags.is_unread())
return email;
// All e-mail was read, so return the newest one.

View file

@ -103,7 +103,7 @@ public class MessageViewer : Gtk.Viewport {
header.column_spacing = HEADER_COL_SPACING;
header.row_spacing = HEADER_ROW_SPACING;
if (email.properties.is_unread())
if (email.properties.email_flags.is_unread())
icon_area.add(new Gtk.Image.from_pixbuf(IconFactory.instance.unread));
int header_height = 0;

View file

@ -168,7 +168,7 @@ public abstract class Geary.Conversation : Object {
return false;
foreach (Geary.Email email in list) {
if (email.properties.is_unread())
if (email.properties.email_flags.is_unread())
return true;
}

View file

@ -5,9 +5,33 @@
*/
public abstract class Geary.EmailProperties : Object {
public EmailProperties() {
// Flags that can be set or cleared on a given e-mail.
public enum EmailFlags {
NONE = 0,
UNREAD = 1 << 0;
public inline bool is_all_set(EmailFlags required_flags) {
return (this & required_flags) == required_flags;
}
public inline EmailFlags set(EmailFlags flags) {
return (this | flags);
}
public inline EmailFlags clear(EmailFlags flags) {
return (this & ~(flags));
}
// Convenience method to check if the unread flag is set.
public inline bool is_unread() {
return is_all_set(UNREAD);
}
}
public abstract bool is_unread();
// Flags se on the email object.
public EmailFlags email_flags { get; protected set; default = EmailFlags.NONE; }
public EmailProperties() {
}
}

View file

@ -348,6 +348,15 @@ public interface Geary.Folder : Object {
public abstract async void remove_email_async(Geary.EmailIdentifier email_id,
Cancellable? cancellable = null) throws Error;
/**
* Adds or removes a flag from a list of messages.
*
* The Folder must be opened prior to attempting this operation.
*/
public abstract async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags
flags_to_remove, Cancellable? cancellable = null) throws Error;
/**
* check_span_specifiers() verifies that the span specifiers match the requirements set by
* list_email_async() and lazy_list_email_async(). If not, this method throws

View file

@ -26,10 +26,9 @@ public class Geary.Imap.EmailProperties : Geary.EmailProperties, Equalable {
flagged = flags.contains(MessageFlag.FLAGGED);
recent = flags.contains(MessageFlag.RECENT);
seen = flags.contains(MessageFlag.SEEN);
}
public override bool is_unread() {
return !flags.contains(MessageFlag.SEEN);
if (!seen)
email_flags = email_flags.set(Geary.EmailProperties.EmailFlags.UNREAD);
}
public bool equals(Equalable e) {

View file

@ -191,5 +191,23 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
throw new EngineError.READONLY("IMAP currently read-only");
}
public override async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags
flags_to_remove, Cancellable? cancellable = null) throws Error {
if (mailbox == null)
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
// Build an array of UIDs.
Geary.Imap.UID[] sparse_set = new Geary.Imap.UID[to_mark.size];
int i = 0;
foreach(Geary.EmailIdentifier id in to_mark) {
sparse_set[i] = ((Geary.Imap.EmailIdentifier) id).uid;
i++;
}
MessageSet message_set = new MessageSet.uid_sparse(sparse_set);
mailbox.mark_email_async(message_set, flags_to_add, flags_to_remove, cancellable);
}
}

View file

@ -94,7 +94,7 @@ public class Geary.Imap.StatusCommand : Command {
public StatusCommand(string mailbox, StatusDataType[] data_items) {
base (NAME);
add (new StringParameter(mailbox));
add(new StringParameter(mailbox));
assert(data_items.length > 0);
ListParameter data_item_list = new ListParameter(this);
@ -105,3 +105,24 @@ public class Geary.Imap.StatusCommand : Command {
}
}
public class Geary.Imap.StoreCommand : Command {
public const string NAME = "store";
public const string UID_NAME = "uid store";
public StoreCommand(MessageSet message_set, Gee.List<MessageFlag> flag_list, bool add_flag,
bool silent) {
base (message_set.is_uid ? UID_NAME : NAME);
add(message_set.to_parameter());
add(new StringParameter("%sflags%s".printf(add_flag ? "+" : "-", silent ? ".silent" : "")));
ListParameter list = new ListParameter(this);
foreach(MessageFlag flag in flag_list)
list.add(new StringParameter(flag.value));
add(list);
debug("command: %s", this.to_string());
}
}

View file

@ -96,6 +96,21 @@ public class Geary.Imap.MessageFlag : Geary.Imap.Flag {
public MessageFlag(string value) {
base (value);
}
// Converts a list of email flags to add and remove to a list of message
// flags to add and remove.
public static void from_email_flags(Geary.EmailProperties.EmailFlags email_flags_add,
Geary.EmailProperties.EmailFlags email_flags_remove, out Gee.List<MessageFlag> msg_flags_add,
out Gee.List<MessageFlag> msg_flags_remove) {
msg_flags_add = new Gee.ArrayList<MessageFlag>();
msg_flags_remove = new Gee.ArrayList<MessageFlag>();
if (email_flags_add.is_all_set(Geary.EmailProperties.EmailFlags.UNREAD))
msg_flags_remove.add(MessageFlag.SEEN);
if (email_flags_remove.is_all_set(Geary.EmailProperties.EmailFlags.UNREAD))
msg_flags_add.add(MessageFlag.SEEN);
}
}
public class Geary.Imap.MailboxAttribute : Geary.Imap.Flag {

View file

@ -84,11 +84,16 @@ public class Geary.Imap.MessageSet {
}
public MessageSet.sparse(int[] msg_nums) {
value = build_sparse_range(msg_nums);
value = build_sparse_range(msg_array_to_int64(msg_nums));
}
public MessageSet.uid_sparse(UID[] msg_uids) {
value = build_sparse_range(uid_array_to_int64(msg_uids));
is_uid = true;
}
public MessageSet.sparse_to_highest(int[] msg_nums) {
value = "%s:*".printf(build_sparse_range(msg_nums));
value = "%s:*".printf(build_sparse_range(msg_array_to_int64(msg_nums)));
}
public MessageSet.multirange(MessageSet[] msg_sets) {
@ -128,25 +133,42 @@ public class Geary.Imap.MessageSet {
is_uid = true;
}
// Builds sparse range of either UID values or message numbers.
// TODO: It would be more efficient to look for runs in the numbers and form the set specifier
// with them.
private static string build_sparse_range(int[] msg_nums) {
private static string build_sparse_range(int64[] msg_nums) {
assert(msg_nums.length > 0);
StringBuilder builder = new StringBuilder();
for (int ctr = 0; ctr < msg_nums.length; ctr++) {
int msg_num = msg_nums[ctr];
int64 msg_num = msg_nums[ctr];
assert(msg_num >= 0);
if (ctr < (msg_nums.length - 1))
builder.append_printf("%d,", msg_num);
builder.append_printf("%lld,", msg_num);
else
builder.append_printf("%d", msg_num);
builder.append_printf("%lld", msg_num);
}
return builder.str;
}
private static int64[] msg_array_to_int64(int[] msg_nums) {
int64[] ret = new int64[0];
foreach (int num in msg_nums)
ret += (int64) num;
return ret;
}
private static int64[] uid_array_to_int64(UID[] msg_uids) {
int64[] ret = new int64[0];
foreach (UID uid in msg_uids)
ret += uid.value;
return ret;
}
public Parameter to_parameter() {
// Message sets are not quoted, even if they use an atom-special character (this *might*
// be a Gmailism...)

View file

@ -89,10 +89,21 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
int plain_id = batch.add(new MailboxOperation(context, fetch_cmd));
int body_id = NonblockingBatch.INVALID_ID;
int preview_id = NonblockingBatch.INVALID_ID;
int preview_charset_id = NonblockingBatch.INVALID_ID;
int properties_id = NonblockingBatch.INVALID_ID;
if (fields.require(Geary.Email.Field.BODY)) {
// Fetch the body.
Gee.List<FetchBodyDataType> types = new Gee.ArrayList<FetchBodyDataType>();
types.add(new FetchBodyDataType.peek(
FetchBodyDataType.SectionPart.TEXT, null, -1, -1, null));
FetchCommand fetch_body = new FetchCommand(msg_set, null, types);
body_id = batch.add(new MailboxOperation(context, fetch_body));
}
if (fields.require(Geary.Email.Field.PREVIEW)) {
// Preview text.
FetchBodyDataType fetch_preview = new FetchBodyDataType.peek(FetchBodyDataType.SectionPart.NONE,
@ -155,6 +166,26 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
map.set(plain_res.msg_num, email);
}
// Process body results.
if (body_id != NonblockingBatch.INVALID_ID) {
MailboxOperation body_op = (MailboxOperation) batch.get_operation(body_id);
CommandResponse body_resp = (CommandResponse) batch.get_result(body_id);
if (body_resp.status_response.status != Status.OK) {
throw new ImapError.SERVER_ERROR("Server error for %s: %s",
body_op.cmd.to_string(), body_resp.to_string());
}
FetchResults[] body_results = FetchResults.decode(body_resp);
foreach (FetchResults body_res in body_results) {
Geary.Email? body_email = map.get(body_res.msg_num);
if (body_email == null)
continue;
body_email.set_message_body(new Geary.RFC822.Text(body_res.get_body_data().get(0)));
}
}
// Process properties results.
if (properties_id != NonblockingBatch.INVALID_ID) {
MailboxOperation properties_op = (MailboxOperation) batch.get_operation(properties_id);
@ -291,13 +322,10 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
break;
case Geary.Email.Field.BODY:
data_types_list.add(FetchDataType.RFC822_TEXT);
break;
case Geary.Email.Field.PROPERTIES:
case Geary.Email.Field.NONE:
case Geary.Email.Field.PREVIEW:
// not set (or, for previews and properties, fetched separately)
// not set (or, for body previews and properties, fetched separately)
break;
default:
@ -467,6 +495,61 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
if (fields.require(Geary.Email.Field.REFERENCES))
email.set_full_references(message_id, in_reply_to, references);
}
public async void mark_email_async(MessageSet to_mark, Geary.EmailProperties.EmailFlags
flags_to_add, Geary.EmailProperties.EmailFlags flags_to_remove,
Cancellable? cancellable = null) throws Error {
if (context.is_closed())
throw new ImapError.NOT_SELECTED("Mailbox %s closed", name);
Gee.List<MessageFlag> msg_flags_add = new Gee.ArrayList<MessageFlag>();
Gee.List<MessageFlag> msg_flags_remove = new Gee.ArrayList<MessageFlag>();
MessageFlag.from_email_flags(flags_to_add, flags_to_remove, out msg_flags_add,
out msg_flags_remove);
NonblockingBatch batch = new NonblockingBatch();
int add_flags_id = NonblockingBatch.INVALID_ID;
int remove_flags_id = NonblockingBatch.INVALID_ID;
if (msg_flags_add.size > 0)
add_flags_id = batch.add(new MailboxOperation(context, new StoreCommand(
to_mark, msg_flags_add, true, true)));
if (msg_flags_remove.size > 0)
remove_flags_id = batch.add(new MailboxOperation(context, new StoreCommand(
to_mark, msg_flags_remove, false, true)));
yield batch.execute_all_async(cancellable);
if (add_flags_id != NonblockingBatch.INVALID_ID) {
MailboxOperation add_op = (MailboxOperation) batch.get_operation(add_flags_id);
CommandResponse add_resp = (CommandResponse) batch.get_result(add_flags_id);
if (add_resp.status_response == null)
throw new ImapError.SERVER_ERROR("Server error. Command: %s No status response. %s",
add_op.cmd.to_string(), add_resp.to_string());
if (add_resp.status_response.status != Status.OK)
throw new ImapError.SERVER_ERROR("Server error. Command: %s Response: %s Error: %s",
add_op.cmd.to_string(), add_resp.to_string(),
add_resp.status_response.status.to_string());
}
if (remove_flags_id != NonblockingBatch.INVALID_ID) {
MailboxOperation remove_op = (MailboxOperation) batch.get_operation(remove_flags_id);
CommandResponse remove_resp = (CommandResponse) batch.get_result(remove_flags_id);
if (remove_resp.status_response == null)
throw new ImapError.SERVER_ERROR("Server error. Command: %s No status response. %s",
remove_op.cmd.to_string(), remove_resp.to_string());
if (remove_resp.status_response.status != Status.OK)
throw new ImapError.SERVER_ERROR("Server error. Command: %s Response: %s Error: %s",
remove_op.cmd.to_string(), remove_resp.to_string(),
remove_resp.status_response.status.to_string());
}
}
}
// A SelectedContext is a ReferenceSemantics object wrapping a ClientSession that is in a SELECTED

View file

@ -122,6 +122,10 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder {
public abstract async void remove_email_async(Geary.EmailIdentifier email_id, Cancellable? cancellable = null)
throws Error;
public abstract async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags
flags_to_remove, Cancellable? cancellable = null) throws Error;
public virtual string to_string() {
return get_path().to_string();
}

View file

@ -939,5 +939,15 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
debug("prefetched %d for %s", prefetch_count, to_string());
}
public override async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags
flags_to_remove, Cancellable? cancellable = null) throws Error {
if (!yield wait_for_remote_to_open())
throw new EngineError.SERVER_UNAVAILABLE("No connection to %s", remote.to_string());
yield remote_folder.mark_email_async(to_mark, flags_to_add, flags_to_remove, cancellable);
yield local_folder.mark_email_async(to_mark, flags_to_add, flags_to_remove, cancellable);
}
}

View file

@ -471,6 +471,52 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
notify_message_removed(id);
}
public override async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
Geary.EmailProperties.EmailFlags flags_to_add, Geary.EmailProperties.EmailFlags
flags_to_remove, Cancellable? cancellable = null) throws Error {
check_open();
Transaction transaction = yield db.begin_transaction_async("Folder.mark_email_async",
cancellable);
Gee.List<Geary.Imap.MessageFlag> msg_flags_add = new Gee.ArrayList<Geary.Imap.MessageFlag>();
Gee.List<Geary.Imap.MessageFlag> msg_flags_remove =
new Gee.ArrayList<Geary.Imap.MessageFlag>();
Geary.Imap.MessageFlag.from_email_flags(flags_to_add, flags_to_remove, out msg_flags_add,
out msg_flags_remove);
foreach (Geary.EmailIdentifier id in to_mark) {
MessageLocationRow? location_row = yield location_table.fetch_by_ordering_async(
transaction, folder_row.id, ((Geary.Imap.EmailIdentifier) id).uid.value, cancellable);
if (location_row == null) {
throw new EngineError.NOT_FOUND("No message with ID %s in folder %s", id.to_string(),
to_string());
}
ImapMessagePropertiesRow? row = yield imap_message_properties_table.fetch_async(
transaction, location_row.id, cancellable);
if (row == null) {
warning("Message not found in database: %lld", location_row.id);
continue;
}
// Create new set of message flags with the appropriate flags added and/or removed.
Gee.HashSet<Geary.Imap.MessageFlag> mutable_copy =
new Gee.HashSet<Geary.Imap.MessageFlag>();
mutable_copy.add_all(Geary.Imap.MessageFlags.deserialize(row.flags).get_all());
mutable_copy.remove_all(msg_flags_remove);
mutable_copy.add_all(msg_flags_add);
Geary.Imap.MessageFlags new_flags = new Geary.Imap.MessageFlags(mutable_copy);
yield imap_message_properties_table.update_flags_async(transaction, location_row.id,
new_flags.serialize(), cancellable);
}
yield transaction.commit_async(cancellable);
}
public async bool is_email_present_async(Geary.EmailIdentifier id, out Geary.Email.Field available_fields,
Cancellable? cancellable = null) throws Error {
check_open();

View file

@ -76,6 +76,22 @@ public class Geary.Sqlite.ImapMessagePropertiesTable : Geary.Sqlite.Table {
yield release_lock_async(transaction, locked, cancellable);
}
public async void update_flags_async(Transaction? transaction, int64 message_id, string? flags,
Cancellable? cancellable) throws Error {
Transaction locked = yield obtain_lock_async(transaction,
"ImapMessagePropertiesTable.update_flags_async", cancellable);
SQLHeavy.Query query = locked.prepare(
"UPDATE ImapMessagePropertiesTable SET flags = ? WHERE message_id = ?");
query.bind_string(0, flags);
query.bind_int64(1, message_id);
yield query.execute_async(cancellable);
locked.set_commit_required();
yield release_lock_async(transaction, locked, cancellable);
}
public async Gee.List<int64?>? search_for_duplicates_async(Transaction? transaction, string? internaldate,
long rfc822_size, Cancellable? cancellable) throws Error {
bool has_internaldate = !String.is_empty(internaldate);