more message deletion work; implement concept of 'trash' (though it will not be called that and have so such thing as a folder), handle 'current' flag cases to prevent dupes, add 'show deleted' and 'purge deleted' buttons. remaining: contextmenu purge and undelete of deleted items.

This commit is contained in:
alta88 2009-06-08 15:14:48 -06:00
Родитель 2abbeb4e9c
Коммит f41ae66e23
13 изменённых файлов: 242 добавлений и 76 удалений

Просмотреть файл

@ -111,6 +111,7 @@ let CollectionsView = {
Filters: {
unread: false,
deleted: false,
searchterms: null
},
@ -129,6 +130,12 @@ let CollectionsView = {
this.Filters["unread"] = document.getElementById("snowlUnreadButton").
checked ? true : false;
this.Filters["deleted"] = document.getElementById("snowlShowDeletedButton").
checked ? true : false;
if (this.Filters["deleted"])
document.getElementById("snowlPurgeDeletedButton").removeAttribute("disabled");
else
document.getElementById("snowlPurgeDeletedButton").setAttribute("disabled", true);
// Restore persisted view selection (need to build the menulist) or init.
let selIndex = parseInt(this._collectionsViewMenu.getAttribute("selectedindex"));
@ -370,14 +377,29 @@ this._log.info("onClick: START itemIds - " +this.itemIds.toSource());
},
onCommandUnreadButton: function(aEvent) {
// XXX Instead of rebuilding from scratch each time, when going from
// all to unread, simply hide the ones that are read (f.e. by setting a CSS
// class on read items and then using a CSS rule to hide them)?
// Unfortunately, css cannot be used to hide a treechildren row using
// properties and pseudo element selectors.
aEvent.target.checked = !aEvent.target.checked;
this.Filters["unread"] = aEvent.target.checked ? true : false;
gMessageViewWindow.SnowlMessageView.onFilter(this.Filters);
},
onCommandShowDeletedButton: function(aEvent) {
aEvent.target.checked = !aEvent.target.checked;
this.Filters["deleted"] = aEvent.target.checked ? true : false;
if (this.Filters["deleted"])
document.getElementById("snowlPurgeDeletedButton").removeAttribute("disabled");
else
document.getElementById("snowlPurgeDeletedButton").setAttribute("disabled", true);
gMessageViewWindow.SnowlMessageView.onFilter(this.Filters);
},
onCommandPurgeDeletedButton: function(aEvent) {
gMessageViewWindow.SnowlMessageView.onDeleteMessages();
},
_resetCollectionsView: true,
onPopupshowingCollectionsView: function(event) {
// Build dynamic Views list.
@ -809,6 +831,10 @@ this._log.info("removeAuthor: Removing author - " + query.queryName + " : " + se
let query, uri, rangeFirst = { }, rangeLast = { }, refreshFlag = false;
let numRanges = this._tree.view.selection.getRangeCount();
if (this.Filters["deleted"])
// Don't refresh if showing deleted for selected collection.
return refreshFlag;
for (let i = 0; i < numRanges && !refreshFlag; i++) {
this._tree.view.selection.getRangeAt(i, rangeFirst, rangeLast);
for (let index = rangeFirst.value; index <= rangeLast.value; index++) {

Просмотреть файл

@ -144,6 +144,17 @@
type="checkbox"
oncommand="ListSidebar.onToggleWrite(event)"
tooltiptext="&writeButton.tooltip;"/>
<toolbarbutton id="snowlShowDeletedButton"
class="tabbable"
type="checkbox"
autoCheck="false"
persist="checked"
oncommand="CollectionsView.onCommandShowDeletedButton(event)"
tooltiptext="&showDeletedButton.tooltip;"/>
<toolbarbutton id="snowlPurgeDeletedButton"
class="tabbable"
oncommand="CollectionsView.onCommandPurgeDeletedButton(event)"
tooltiptext="&purgeDeletedButton.tooltip;"/>
</toolbar>
</page>

Просмотреть файл

@ -34,10 +34,14 @@
*
* ***** END LICENSE BLOCK ***** */
treechildren::-moz-tree-cell-text(unread) {
#snowlView > treechildren::-moz-tree-cell-text(unread) {
font-weight: bold !important
}
#snowlView > treechildren::-moz-tree-cell-text(deleted) {
text-decoration: line-through;
}
/* XXX: these 2 rules were used by only the unread button, are they needed still?
/* Don't style the buttons natively because they look ugly on Linux.
* FIXME: test on other platforms and style conditionally as appropriate,

Просмотреть файл

@ -48,6 +48,7 @@ Cu.import("resource://snowl/modules/URI.js");
// modules that are Snowl-specific
Cu.import("resource://snowl/modules/collection.js");
Cu.import("resource://snowl/modules/constants.js");
Cu.import("resource://snowl/modules/datastore.js");
Cu.import("resource://snowl/modules/message.js");
Cu.import("resource://snowl/modules/service.js");
@ -149,11 +150,15 @@ let SnowlMessageView = {
getCellProperties: function (aRow, aColumn, aProperties) {
// We have to set this on each cell rather than on the row as a whole
// because the styling we apply to unread messages (bold text) has to be
// because the text styling we apply to unread/deleted messages has to be
// specified by the ::-moz-tree-cell-text pseudo-element, which inherits
// only the cell's properties.
if (!this._collection.messages[aRow].read)
aProperties.AppendElement(this._atomSvc.getAtom("unread"));
if (this._collection.messages[aRow].current == MESSAGE_NON_CURRENT_DELETED ||
this._collection.messages[aRow].current == MESSAGE_CURRENT_DELETED)
aProperties.AppendElement(this._atomSvc.getAtom("deleted"));
},
getColumnProperties: function(aColumnID, aColumn, aProperties) {},
@ -233,6 +238,15 @@ let SnowlMessageView = {
if (this.Filters["unread"])
filters.push({ expression: "read = 0", parameters: {} });
if (this.Filters["deleted"])
filters.push({ expression: "(current = " + MESSAGE_NON_CURRENT_DELETED + " OR" +
" current = " + MESSAGE_CURRENT_DELETED + ")",
parameters: {} });
else
filters.push({ expression: "(current = " + MESSAGE_NON_CURRENT + " OR" +
" current = " + MESSAGE_CURRENT + ")",
parameters: {} });
// FIXME: use a left join here once the SQLite bug breaking left joins to
// virtual tables has been fixed (i.e. after we upgrade to SQLite 3.5.7+).
if (this.Filters["searchterms"])
@ -619,8 +633,12 @@ this._log.info("_toggleRead: all? " + aAll);
// Create an array of messages and list rows to pass on.
let messages = [], selectedRows = [];
let rangeFirst = { }, rangeLast = { };
let numRanges = this._tree.view.selection.getRangeCount();
if (this.Filters["deleted"])
// Showing deleted messages, thus a purge is requested. Select the whole list.
this._tree.view.selection.selectAll();
let numRanges = this._tree.view.selection.getRangeCount();
for (let i = 0; i < numRanges; i++) {
this._tree.view.selection.getRangeAt(i, rangeFirst, rangeLast);
for (let index = rangeFirst.value; index <= rangeLast.value; index++) {
@ -639,7 +657,7 @@ this._log.info("_toggleRead: all? " + aAll);
_deleteMessages: function(aMessages, aRows) {
//this._log.info("_deleteMessages: START #ids - "+aMessages.length);
// Delete messages. Delete author if deleting author's only remaining message.
let message, messageID, authorID;
let message, messageID, current;
let messageIDs = [];
let refreshList = false, sessionHistoryEmpty = false;
@ -647,59 +665,25 @@ this._log.info("_toggleRead: all? " + aAll);
for (let i = 0; i < aMessages.length; ++i) {
message = aMessages[i];
messageID = message.id;
messageIDs.push(messageID)
authorID = message.authorID;
authorPlaceID = message.author.placeID;
messageIDs.push(messageID);
current = message.current;
if (!SnowlMessage.get(messageID))
//this._log.info("_deleteMessages: Delete messages NOTFOUND - "+messageID);
continue;
if (!refreshList && this.CollectionsView.isMessageForSelectedCollection(message))
if (!refreshList && (this.Filters["deleted"] ||
this.CollectionsView.isMessageForSelectedCollection(message)))
// Message being deleted in a selected collection? Don't repeat call if
// at least one is true.
// XXX: is this call worth doing, based on likely deletion usage.
// at least one is true. Also refresh if showing deleted list.
// XXX: is this call worth doing, based on likely deletion usage, or just
// always refresh..
refreshList = true;
SnowlDatastore.dbConnection.beginTransaction();
try {
// Delete messages
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM metadata " +
"WHERE messageID = " + messageID);
//this._log.info("_deleteMessages: Delete messages METADATA DONE");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM partsText " +
"WHERE docid IN " +
"(SELECT id FROM parts WHERE messageID = " + messageID + ")");
//this._log.info("_deleteMessages: Delete messages PARTSTEXT DONE");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM parts " +
"WHERE messageID = " + messageID);
//this._log.info("_deleteMessages: Delete messages PARTS DONE");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM messages " +
"WHERE id = " + messageID);
//this._log.info("_deleteMessages: Delete messages DONE");
if (!SnowlService.hasAuthorMessage(authorID)) {
// Delete people/identities; author's only message has been deleted.
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM people " +
"WHERE id IN " +
"(SELECT personID FROM identities WHERE id = " + authorID + ")");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM identities " +
"WHERE id = " + authorID);
// Finally, clean up Places bookmark by author's placeID. A collections
// tree rebuild is triggered by Places on removeItem of a visible item,
// triggering a select event. Need to bypass in onSelect.
this.CollectionsView.noSelect = true;
PlacesUtils.bookmarks.removeItem(authorPlaceID);
//this._log.info("_deleteMessages: Delete DONE authorID - "+authorID);
}
// PlacesUtils.history.removePage(URI(this.MESSAGE_URI + messageID));
//this._log.info("_deleteMessages: Delete DONE messageID - "+messageID);
SnowlDatastore.dbConnection.commitTransaction();
}
catch(ex) {
SnowlDatastore.dbConnection.rollbackTransaction();
throw ex;
}
if (current == MESSAGE_NON_CURRENT || current == MESSAGE_CURRENT)
SnowlMessage.markDeleted(message);
else
SnowlMessage.delete(message);
}
sessionHistoryEmpty = this._cleanSessionHistory(messageIDs);
@ -713,7 +697,7 @@ this._log.info("_toggleRead: all? " + aAll);
// Refresh list; if the currently deleted message is selected, then select
// the next message (same row post refresh) or prior message (if deleted
// message is last row). In a multiselection, this means the row
// of the first message in the selection.
// of the first message in the selection. Refresh deleted list if purged.
let currRow, rowCount, selIndex;
if (aRows) {
@ -750,13 +734,14 @@ this._log.info("_toggleRead: all? " + aAll);
// deletion. Return true if last message in history is deleted, else false.
// XXX: clean across all tabs' history, not just current tab?
//this._log.info("_cleanSessionHistory: messageIDs - "+aMessageIDs);
let shEntry, uri, msgUri, msgId;
let newCount = 0, restoreIndex = 0, historyChanged = false;
let newCount = 0, historyChanged = false;
let newHistory = [];
let sh = getBrowser().webNavigation.sessionHistory;
let currIndex = sh.index;
let restoreIndex = currIndex;
//this._log.info("_cleanSessionHistory: messageIDs:currIndex - "+aMessageIDs+" : "+currIndex);
for (i = 0; i < sh.count; i++){
shEntry = sh.getEntryAtIndex(i, false).QueryInterface(Ci.nsISHEntry);
@ -768,8 +753,8 @@ this._log.info("_toggleRead: all? " + aAll);
if (msgUri == this.MESSAGE_URI && aMessageIDs.indexOf(msgId) != -1) {
//this._log.info("_cleanSessionHistory: Delete from HISTORY - "+uri);
historyChanged = true;
if (currIndex == i)
restoreIndex = newCount > 0 ? --newCount : newCount;
if (i <= currIndex)
restoreIndex = restoreIndex <= 0 ? 0 : --restoreIndex;
continue;
}
//this._log.info("_cleanSessionHistory: Add to HISTORY - "+uri);
@ -794,8 +779,8 @@ this._log.info("_toggleRead: all? " + aAll);
sh.addEntry(shEntry, true);
})
sh.QueryInterface(Ci.nsIWebNavigation).gotoIndex(restoreIndex);
//this._log.info("_cleanSessionHistory: restoreIndex - "+restoreIndex);
sh.QueryInterface(Ci.nsIWebNavigation).gotoIndex(restoreIndex);
return false;
},

Просмотреть файл

@ -120,6 +120,10 @@ td {
padding-top: 6px;
}
#headerDeck[deleted] > #briefHeaderRow > .headerDataSubject > #subject{
text-decoration: line-through;
}
.fullHeaderRowSeparator {
border-bottom: 1px solid threedshadow;
margin: 0 10px;

Просмотреть файл

@ -135,6 +135,10 @@ var messageContent = {
appendChild(document.createTextNode(SnowlDateUtils._formatDate(message.timestamp)));
headerDeck.removeAttribute("notfound");
if (message.current == MESSAGE_NON_CURRENT_DELETED ||
message.current == MESSAGE_CURRENT_DELETED)
headerDeck.setAttribute("deleted", true);
}
else {
// Message no longer exists (removed source/author/message) but is in history.

Просмотреть файл

@ -42,13 +42,19 @@
#snowlToolbar > toolbarbutton > .toolbarbutton-icon {
-moz-margin-end: 0;
}
#snowlToolbar > toolbarbutton > .toolbarbutton-text {
margin: 0;
}
#snowlToolbar > toolbarbutton[disabled] {
opacity: .4;
}
#snowlSubscribeButton {
list-style-image: url("chrome://snowl/content/icons/add.png");
}
#snowlRefreshButton {
list-style-image: url("chrome://snowl/content/icons/arrow_refresh_small.png");
}
@ -61,6 +67,15 @@
#snowlUnreadButton {
list-style-image: url("chrome://snowl/content/icons/new.png");
}
#writeButton {
list-style-image: url("chrome://snowl/content/icons/email_add.png");
}
#snowlShowDeletedButton {
list-style-image: url("chrome://snowl/content/icons/bin_closed.png");
}
#snowlPurgeDeletedButton {
list-style-image: url("chrome://snowl/content/icons/bin_empty.png");
}

Просмотреть файл

@ -5,3 +5,5 @@
<!ENTITY unreadButton.tooltip "Only show unread messages.">
<!ENTITY listToolbarButton.tooltip "Toggle View toolbar.">
<!ENTITY writeButton.tooltip "Write a message.">
<!ENTITY showDeletedButton.tooltip "Show deleted messages for selected Collections.">
<!ENTITY purgeDeletedButton.tooltip "Purge all deleted messages from selected Collections.">

Просмотреть файл

@ -265,10 +265,11 @@ this._log.info("got " + groups.length + " groups");
sourceID: statement.row.sourceID,
subject: statement.row.subject,
authorID: statement.row.authorID,
link: statement.row.link,
timestamp: SnowlDateUtils.julianToJSDate(statement.row.timestamp),
_read: (statement.row.read ? true : false),
received: SnowlDateUtils.julianToJSDate(statement.row.received),
link: statement.row.link,
current: statement.row.current,
_read: (statement.row.read ? true : false),
content: content
});
@ -298,12 +299,13 @@ this._log.info("got " + groups.length + " groups");
let columns = [
"messages.id AS messageID",
"messages.sourceID",
"messages.authorID",
"messages.subject",
"messages.link",
"messages.authorID",
"messages.timestamp",
"messages.read",
"messages.received",
"messages.link",
"messages.current",
"messages.read",
"parts.id AS partID",
"parts.content",
"parts.mediaType",

Просмотреть файл

@ -40,7 +40,12 @@ const EXPORTED_SYMBOLS = ["TEXT_CONSTRUCT_TYPES",
"PART_TYPE_SUMMARY",
"XML_NS",
"XUL_NS",
"HTML_NS"];
"HTML_NS",
"MESSAGE_NON_CURRENT",
"MESSAGE_CURRENT",
"MESSAGE_NON_CURRENT_DELETED",
"MESSAGE_CURRENT_DELETED",
"MESSAGE_CURRENT_PENDING_PURGE"];
// Internet media type to nsIFeedTextConstruct::type mappings.
const TEXT_CONSTRUCT_TYPES = {
@ -60,6 +65,13 @@ const INTERNET_MEDIA_TYPES = {
const PART_TYPE_CONTENT = 1;
const PART_TYPE_SUMMARY = 2;
const XML_NS = "http://www.w3.org/XML/1998/namespace"
const XML_NS = "http://www.w3.org/XML/1998/namespace";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const HTML_NS = "http://www.w3.org/1999/xhtml";
// Message statuses
const MESSAGE_NON_CURRENT = 0;
const MESSAGE_CURRENT = 1;
const MESSAGE_NON_CURRENT_DELETED = 2;
const MESSAGE_CURRENT_DELETED = 3;
const MESSAGE_CURRENT_PENDING_PURGE = 4;

Просмотреть файл

@ -772,7 +772,9 @@ let SnowlDatastore = {
get _selectHasAuthorMessageStatement() {
let statement = this.createStatement(
"SELECT 1 FROM messages WHERE authorID = :authorID"
"SELECT 1 FROM messages" +
" WHERE authorID = :authorID AND" +
" current <> " + MESSAGE_CURRENT_PENDING_PURGE
);
this.__defineGetter__("_selectHasAuthorMessageStatement", function() { return statement });
return this._selectHasAuthorMessageStatement;

Просмотреть файл

@ -41,6 +41,9 @@ const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
// modules that come with Firefox
Cu.import("resource://gre/modules/utils.js"); // Places
// modules that are generic
Cu.import("resource://snowl/modules/URI.js");
@ -65,7 +68,7 @@ SnowlMessage.get = function(id) {
let message;
let statement = SnowlDatastore.createStatement(
"SELECT sourceID, subject, link, timestamp, read, received, authorID " +
"SELECT sourceID, subject, authorID, timestamp, received, link, current, read " +
"FROM messages WHERE messages.id = :id"
);
@ -77,10 +80,11 @@ SnowlMessage.get = function(id) {
sourceID: statement.row.sourceID,
subject: statement.row.subject,
authorID: statement.row.authorID,
link: statement.row.link,
timestamp: SnowlDateUtils.julianToJSDate(statement.row.timestamp),
_read: (statement.row.read ? true : false),
received: SnowlDateUtils.julianToJSDate(statement.row.received)
received: SnowlDateUtils.julianToJSDate(statement.row.received),
link: statement.row.link,
current: statement.row.current,
_read: (statement.row.read ? true : false)
});
}
}
@ -91,6 +95,87 @@ SnowlMessage.get = function(id) {
return message;
};
SnowlMessage.delete = function(aMessage) {
let message = aMessage;
let messageID = message.id;
let authorID = message.authorID;
let authorPlaceID = message.author.placeID;
let current = message.current;
SnowlDatastore.dbConnection.beginTransaction();
try {
// Delete messages
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM metadata " +
"WHERE messageID = " + messageID);
//this._log.info("_deleteMessages: Delete messages METADATA DONE");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM partsText " +
"WHERE docid IN " +
"(SELECT id FROM parts WHERE messageID = " + messageID + ")");
//this._log.info("_deleteMessages: Delete messages PARTSTEXT DONE");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM parts " +
"WHERE messageID = " + messageID);
//this._log.info("_deleteMessages: Delete messages PARTS DONE");
// If a message is current and marked deleted, need to keep the record so
// duplicates are not re added upon refresh. So we move to a pending purge
// state and delete the rest of the message.
if (current == MESSAGE_CURRENT_DELETED)
SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages " +
"SET current = " + MESSAGE_CURRENT_PENDING_PURGE +
" WHERE id = " + messageID);
else
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM messages " +
"WHERE id = " + messageID);
//this._log.info("_deleteMessages: Delete messages DONE");
if (!SnowlService.hasAuthorMessage(authorID)) {
// Delete people/identities; author's only message has been deleted.
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM people " +
"WHERE id IN " +
"(SELECT personID FROM identities WHERE id = " + authorID + ")");
SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM identities " +
"WHERE id = " + authorID);
// Finally, clean up Places bookmark by author's placeID. A collections
// tree rebuild is triggered by Places on removeItem of a visible item,
// triggering a select event. Need to bypass in onSelect.
SnowlMessage.prototype.CollectionsView.noSelect = true;
PlacesUtils.bookmarks.removeItem(authorPlaceID);
//this._log.info("_deleteMessages: Delete DONE authorID - "+authorID);
}
// PlacesUtils.history.removePage(URI(this.MESSAGE_URI + messageID));
//SnowlPlaces._log.info("_deleteMessages: Delete DONE messageID - "+messageID);
SnowlDatastore.dbConnection.commitTransaction();
}
catch(ex) {
SnowlDatastore.dbConnection.rollbackTransaction();
throw ex;
}
};
SnowlMessage.markDeleted = function(aMessage) {
let message = aMessage;
let messageID = message.id;
SnowlDatastore.dbConnection.beginTransaction();
try {
// Mark message deleted, make sure this caller checks for non delete status first.
SnowlDatastore.dbConnection.executeSimpleSQL(
"UPDATE messages SET current =" +
" (CASE WHEN current = " + MESSAGE_NON_CURRENT +
" THEN " + MESSAGE_NON_CURRENT_DELETED +
" WHEN current = " + MESSAGE_CURRENT +
" THEN " + MESSAGE_CURRENT_DELETED +
" END)" +
" WHERE id = " + messageID
);
SnowlDatastore.dbConnection.commitTransaction();
}
catch(ex) {
SnowlDatastore.dbConnection.rollbackTransaction();
throw ex;
}
};
SnowlMessage.prototype = {
id: null,
subject: null,
@ -287,6 +372,13 @@ SnowlMessage.prototype = {
get source() {
return SnowlService.sourcesByID[this.sourceID];
},
get CollectionsView() {
delete this._CollectionsView;
return this._CollectionsView = SnowlService.gBrowserWindow.document.
getElementById("sidebar").
contentWindow.CollectionsView;
}
};

Просмотреть файл

@ -486,19 +486,26 @@ this._log.info("persist placeID:sources.id - " + placeID + " : " + this.id);
/**
* Update the current flag for messages in a source, after a refresh.
* If message's current flag = 1 set to 0, then set current flag for messages
* in the current refresh list to 1.
* in the current refresh list to 1. Purge current and marked deleted
* placeholder message records if no longer current.
*
* @param aCurrentMessageIDs {array} messages table ids of the current list
*/
updateCurrentMessages: function(aCurrentMessageIDs) {
SnowlDatastore.dbConnection.executeSimpleSQL(
"UPDATE messages SET current = 0" +
" WHERE sourceID = " + this.id + " AND current = 1"
"UPDATE messages SET current = " + MESSAGE_NON_CURRENT +
" WHERE sourceID = " + this.id + " AND current = " + MESSAGE_CURRENT
);
SnowlDatastore.dbConnection.executeSimpleSQL(
"UPDATE messages SET current = 1" +
" WHERE sourceID = " + this.id + " AND id IN " +
"(" + aCurrentMessageIDs.join(", ") + ")"
"UPDATE messages SET current = " + MESSAGE_CURRENT +
" WHERE sourceID = " + this.id + " AND id IN" +
" (" + aCurrentMessageIDs.join(", ") + ") AND current = " + MESSAGE_NON_CURRENT
);
SnowlDatastore.dbConnection.executeSimpleSQL(
"DELETE FROM messages" +
" WHERE sourceID = " + this.id + " AND" +
" current = " + MESSAGE_CURRENT_PENDING_PURGE + " AND" +
" id NOT IN (" + aCurrentMessageIDs.join(", ") + ")"
);
}