diff options
Diffstat (limited to 'mailnews/base/src/nsMsgGroupView.cpp')
-rw-r--r-- | mailnews/base/src/nsMsgGroupView.cpp | 1024 |
1 files changed, 1024 insertions, 0 deletions
diff --git a/mailnews/base/src/nsMsgGroupView.cpp b/mailnews/base/src/nsMsgGroupView.cpp new file mode 100644 index 000000000..f3d9a2704 --- /dev/null +++ b/mailnews/base/src/nsMsgGroupView.cpp @@ -0,0 +1,1024 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgUtils.h" +#include "nsMsgGroupView.h" +#include "nsIMsgHdr.h" +#include "nsIMsgThread.h" +#include "nsIDBFolderInfo.h" +#include "nsIMsgSearchSession.h" +#include "nsMsgGroupThread.h" +#include "nsITreeColumns.h" +#include "nsMsgMessageFlags.h" +#include <plhash.h> +#include "mozilla/Attributes.h" + +#define MSGHDR_CACHE_LOOK_AHEAD_SIZE 25 // Allocate this more to avoid reallocation on new mail. +#define MSGHDR_CACHE_MAX_SIZE 8192 // Max msghdr cache entries. +#define MSGHDR_CACHE_DEFAULT_SIZE 100 + +nsMsgGroupView::nsMsgGroupView() +{ + m_dayChanged = false; + m_lastCurExplodedTime.tm_mday = 0; +} + +nsMsgGroupView::~nsMsgGroupView() +{ +} + +NS_IMETHODIMP nsMsgGroupView::Open(nsIMsgFolder *aFolder, nsMsgViewSortTypeValue aSortType, nsMsgViewSortOrderValue aSortOrder, nsMsgViewFlagsTypeValue aViewFlags, int32_t *aCount) +{ + nsresult rv = nsMsgDBView::Open(aFolder, aSortType, aSortOrder, aViewFlags, aCount); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr <nsIDBFolderInfo> dbFolderInfo; + PersistFolderInfo(getter_AddRefs(dbFolderInfo)); + + nsCOMPtr <nsISimpleEnumerator> headers; + rv = m_db->EnumerateMessages(getter_AddRefs(headers)); + NS_ENSURE_SUCCESS(rv, rv); + + return OpenWithHdrs(headers, aSortType, aSortOrder, aViewFlags, aCount); +} + +void nsMsgGroupView::InternalClose() +{ + m_groupsTable.Clear(); + // nothing to do if we're not grouped. + if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) + return; + + bool rcvDate = false; + + if (m_sortType == nsMsgViewSortType::byReceived) + rcvDate = true; + if (m_db && + ((m_sortType == nsMsgViewSortType::byDate) || + (m_sortType == nsMsgViewSortType::byReceived))) + { + nsCOMPtr <nsIDBFolderInfo> dbFolderInfo; + m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (dbFolderInfo) + { + uint32_t expandFlags = 0; + uint32_t num = GetSize(); + + for (uint32_t i = 0; i < num; i++) + { + if (m_flags[i] & MSG_VIEW_FLAG_ISTHREAD && ! (m_flags[i] & nsMsgMessageFlags::Elided)) + { + nsCOMPtr <nsIMsgDBHdr> msgHdr; + GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr)); + if (msgHdr) + { + uint32_t ageBucket; + nsresult rv = GetAgeBucketValue(msgHdr, &ageBucket, rcvDate); + if (NS_SUCCEEDED(rv)) + expandFlags |= 1 << ageBucket; + } + } + } + dbFolderInfo->SetUint32Property("dateGroupFlags", expandFlags); + } + } +} + +NS_IMETHODIMP nsMsgGroupView::Close() +{ + InternalClose(); + return nsMsgDBView::Close(); +} + +// Set rcvDate to true to get the Received: date instead of the Date: date. +nsresult nsMsgGroupView::GetAgeBucketValue(nsIMsgDBHdr *aMsgHdr, uint32_t * aAgeBucket, bool rcvDate) +{ + NS_ENSURE_ARG_POINTER(aMsgHdr); + NS_ENSURE_ARG_POINTER(aAgeBucket); + + PRTime dateOfMsg; + nsresult rv; + if (!rcvDate) + rv = aMsgHdr->GetDate(&dateOfMsg); + else + { + uint32_t rcvDateSecs; + rv = aMsgHdr->GetUint32Property("dateReceived", &rcvDateSecs); + Seconds2PRTime(rcvDateSecs, &dateOfMsg); + } + NS_ENSURE_SUCCESS(rv, rv); + + PRTime currentTime = PR_Now(); + PRExplodedTime currentExplodedTime; + PR_ExplodeTime(currentTime, PR_LocalTimeParameters, ¤tExplodedTime); + PRExplodedTime explodedMsgTime; + PR_ExplodeTime(dateOfMsg, PR_LocalTimeParameters, &explodedMsgTime); + + if (m_lastCurExplodedTime.tm_mday && + m_lastCurExplodedTime.tm_mday != currentExplodedTime.tm_mday) + m_dayChanged = true; // this will cause us to rebuild the view. + + m_lastCurExplodedTime = currentExplodedTime; + if (currentExplodedTime.tm_year == explodedMsgTime.tm_year && + currentExplodedTime.tm_month == explodedMsgTime.tm_month && + currentExplodedTime.tm_mday == explodedMsgTime.tm_mday) + { + // same day... + *aAgeBucket = 1; + } + // figure out how many days ago this msg arrived + else if (currentTime > dateOfMsg) + { + // setting the time variables to local time + int64_t GMTLocalTimeShift = currentExplodedTime.tm_params.tp_gmt_offset + + currentExplodedTime.tm_params.tp_dst_offset; + GMTLocalTimeShift *= PR_USEC_PER_SEC; + currentTime += GMTLocalTimeShift; + dateOfMsg += GMTLocalTimeShift; + + // the most recent midnight, counting from current time + int64_t mostRecentMidnight = currentTime - currentTime % PR_USEC_PER_DAY; + int64_t yesterday = mostRecentMidnight - PR_USEC_PER_DAY; + // most recent midnight minus 6 days + int64_t mostRecentWeek = mostRecentMidnight - (PR_USEC_PER_DAY * 6); + + // was the message sent yesterday? + if (dateOfMsg >= yesterday) // yes .... + *aAgeBucket = 2; + else if (dateOfMsg >= mostRecentWeek) + *aAgeBucket = 3; + else + { + int64_t lastTwoWeeks = mostRecentMidnight - PR_USEC_PER_DAY * 13; + *aAgeBucket = (dateOfMsg >= lastTwoWeeks) ? 4 : 5; + } + } + else + { + // All that remains is a future date. + *aAgeBucket = 6; + } + return NS_OK; +} + +nsresult nsMsgGroupView::HashHdr(nsIMsgDBHdr *msgHdr, nsString& aHashKey) +{ + nsCString cStringKey; + aHashKey.Truncate(); + nsresult rv = NS_OK; + bool rcvDate = false; + + switch (m_sortType) + { + case nsMsgViewSortType::bySubject: + (void) msgHdr->GetSubject(getter_Copies(cStringKey)); + CopyASCIItoUTF16(cStringKey, aHashKey); + break; + case nsMsgViewSortType::byAuthor: + rv = nsMsgDBView::FetchAuthor(msgHdr, aHashKey); + break; + case nsMsgViewSortType::byRecipient: + (void) msgHdr->GetRecipients(getter_Copies(cStringKey)); + CopyASCIItoUTF16(cStringKey, aHashKey); + break; + case nsMsgViewSortType::byAccount: + case nsMsgViewSortType::byTags: + { + nsCOMPtr <nsIMsgDatabase> dbToUse = m_db; + + if (!dbToUse) // probably search view + GetDBForViewIndex(0, getter_AddRefs(dbToUse)); + + rv = (m_sortType == nsMsgViewSortType::byAccount) + ? FetchAccount(msgHdr, aHashKey) + : FetchTags(msgHdr, aHashKey); + } + break; + case nsMsgViewSortType::byAttachments: + { + uint32_t flags; + msgHdr->GetFlags(&flags); + aHashKey.Assign(flags & nsMsgMessageFlags::Attachment ? '1' : '0'); + break; + } + case nsMsgViewSortType::byFlagged: + { + uint32_t flags; + msgHdr->GetFlags(&flags); + aHashKey.Assign(flags & nsMsgMessageFlags::Marked ? '1' : '0'); + break; + } + case nsMsgViewSortType::byPriority: + { + nsMsgPriorityValue priority; + msgHdr->GetPriority(&priority); + aHashKey.AppendInt(priority); + } + break; + case nsMsgViewSortType::byStatus: + { + uint32_t status = 0; + GetStatusSortValue(msgHdr, &status); + aHashKey.AppendInt(status); + } + break; + case nsMsgViewSortType::byReceived: + rcvDate = true; + MOZ_FALLTHROUGH; + case nsMsgViewSortType::byDate: + { + uint32_t ageBucket; + rv = GetAgeBucketValue(msgHdr, &ageBucket, rcvDate); + if (NS_SUCCEEDED(rv)) + aHashKey.AppendInt(ageBucket); + break; + } + case nsMsgViewSortType::byCustom: + { + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + if (colHandler) + { + bool isString; + colHandler->IsString(&isString); + if (isString) + rv = colHandler->GetSortStringForRow(msgHdr, aHashKey); + else + { + uint32_t intKey; + rv = colHandler->GetSortLongForRow(msgHdr, &intKey); + aHashKey.AppendInt(intKey); + } + } + break; + } + case nsMsgViewSortType::byCorrespondent: + if (IsOutgoingMsg(msgHdr)) + rv = FetchRecipients(msgHdr, aHashKey); + else + rv = FetchAuthor(msgHdr, aHashKey); + break; + default: + NS_ASSERTION(false, "no hash key for this type"); + rv = NS_ERROR_FAILURE; + } + return rv; +} + +nsMsgGroupThread *nsMsgGroupView::CreateGroupThread(nsIMsgDatabase *db) +{ + return new nsMsgGroupThread(db); +} + +nsMsgGroupThread *nsMsgGroupView::AddHdrToThread(nsIMsgDBHdr *msgHdr, bool *pNewThread) +{ + nsMsgKey msgKey; + uint32_t msgFlags; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFlags(&msgFlags); + nsString hashKey; + nsresult rv = HashHdr(msgHdr, hashKey); + if (NS_FAILED(rv)) + return nullptr; + +// if (m_sortType == nsMsgViewSortType::byDate) +// msgKey = ((nsPRUint32Key *) hashKey)->GetValue(); + nsCOMPtr<nsIMsgThread> msgThread; + m_groupsTable.Get(hashKey, getter_AddRefs(msgThread)); + bool newThread = !msgThread; + *pNewThread = newThread; + nsMsgViewIndex viewIndexOfThread; // index of first message in thread in view + nsMsgViewIndex threadInsertIndex; // index of newly added header in thread + + nsMsgGroupThread *foundThread = static_cast<nsMsgGroupThread *>(msgThread.get()); + if (foundThread) + { + // find the view index of the root node of the thread in the view + viewIndexOfThread = GetIndexOfFirstDisplayedKeyInThread(foundThread, + true); + if (viewIndexOfThread == nsMsgViewIndex_None) + { + // Something is wrong with the group table. Remove the old group and + // insert a new one. + m_groupsTable.Remove(hashKey); + foundThread = nullptr; + *pNewThread = newThread = true; + } + } + // If the thread does not already exist, create one + if (!foundThread) + { + foundThread = CreateGroupThread(m_db); + msgThread = do_QueryInterface(foundThread); + m_groupsTable.Put(hashKey, msgThread); + if (GroupViewUsesDummyRow()) + { + foundThread->m_dummy = true; + msgFlags |= MSG_VIEW_FLAG_DUMMY | MSG_VIEW_FLAG_HASCHILDREN; + } + + viewIndexOfThread = GetInsertIndex(msgHdr); + if (viewIndexOfThread == nsMsgViewIndex_None) + viewIndexOfThread = m_keys.Length(); + + // add the thread root node to the view + InsertMsgHdrAt(viewIndexOfThread, msgHdr, msgKey, + msgFlags | MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided, 0); + + // For dummy rows, Have the header serve as the dummy node (it will be added + // again for its actual content later.) + if (GroupViewUsesDummyRow()) + foundThread->InsertMsgHdrAt(0, msgHdr); + + // Calculate the (integer thread key); this really only needs to be done for + // the byDate case where the expanded state of the groups can be easily + // persisted and restored because of the bounded, consecutive value space + // occupied. We calculate an integer value in all cases mainly because + // it's the sanest choice available... + // (The thread key needs to be an integer, so parse hash keys that are + // stringified integers to real integers, and hash actual strings into + // integers.) + if ((m_sortType == nsMsgViewSortType::byAttachments) || + (m_sortType == nsMsgViewSortType::byFlagged) || + (m_sortType == nsMsgViewSortType::byPriority) || + (m_sortType == nsMsgViewSortType::byStatus) || + (m_sortType == nsMsgViewSortType::byReceived) || + (m_sortType == nsMsgViewSortType::byDate)) + foundThread->m_threadKey = + atoi(NS_LossyConvertUTF16toASCII(hashKey).get()); + else + foundThread->m_threadKey = (nsMsgKey) + PL_HashString(NS_LossyConvertUTF16toASCII(hashKey).get()); + } + // Add the message to the thread as an actual content-bearing header. + // (If we use dummy rows, it was already added to the thread during creation.) + threadInsertIndex = foundThread->AddChildFromGroupView(msgHdr, this); + // check if new hdr became thread root + if (!newThread && threadInsertIndex == 0) + { + // update the root node's header (in the view) to be the same as the root + // node in the thread. + SetMsgHdrAt(msgHdr, viewIndexOfThread, msgKey, + (msgFlags & ~(nsMsgMessageFlags::Elided)) | + // maintain elided flag and dummy flag + (m_flags[viewIndexOfThread] & (nsMsgMessageFlags::Elided + | MSG_VIEW_FLAG_DUMMY)) + // ensure thread and has-children flags are set + | MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN, 0); + // update the content-bearing copy in the thread to match. (the root and + // first nodes in the thread should always be the same header.) + // note: the guy who used to be the root will still exist. If our list of + // nodes was [A A], a new node B is introduced which sorts to be the first + // node, giving us [B A A], our copy makes that [B B A], and things are + // right in the world (since we want the first two headers to be the same + // since one is our dummy and one is real.) + if (GroupViewUsesDummyRow()) + foundThread->SetMsgHdrAt(1, msgHdr); // replace the old duplicate dummy header. + // we do not update the content-bearing copy in the view to match; we leave + // that up to OnNewHeader, which is the piece of code who gets to care + // about whether the thread's children are shown or not (elided) + } + + return foundThread; +} + +NS_IMETHODIMP nsMsgGroupView::OpenWithHdrs(nsISimpleEnumerator *aHeaders, nsMsgViewSortTypeValue aSortType, + nsMsgViewSortOrderValue aSortOrder, nsMsgViewFlagsTypeValue aViewFlags, + int32_t *aCount) +{ + nsresult rv = NS_OK; + + m_groupsTable.Clear(); + if (aSortType == nsMsgViewSortType::byThread || aSortType == nsMsgViewSortType::byId + || aSortType == nsMsgViewSortType::byNone || aSortType == nsMsgViewSortType::bySize) + return NS_ERROR_INVALID_ARG; + + m_sortType = aSortType; + m_sortOrder = aSortOrder; + m_viewFlags = aViewFlags | nsMsgViewFlagsType::kThreadedDisplay | nsMsgViewFlagsType::kGroupBySort; + SaveSortInfo(m_sortType, m_sortOrder); + + if (m_sortType == nsMsgViewSortType::byCustom) + { + // If the desired sort is a custom column and there is no handler found, + // it hasn't been registered yet; after the custom column observer is + // notified with MsgCreateDBView and registers the handler, it will come + // back and build the view. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + if (!colHandler) + return rv; + } + + bool hasMore; + nsCOMPtr <nsISupports> supports; + nsCOMPtr <nsIMsgDBHdr> msgHdr; + while (NS_SUCCEEDED(rv) && NS_SUCCEEDED(rv = aHeaders->HasMoreElements(&hasMore)) && hasMore) + { + rv = aHeaders->GetNext(getter_AddRefs(supports)); + if (NS_SUCCEEDED(rv) && supports) + { + bool notUsed; + msgHdr = do_QueryInterface(supports); + AddHdrToThread(msgHdr, ¬Used); + } + } + uint32_t expandFlags = 0; + bool expandAll = m_viewFlags & nsMsgViewFlagsType::kExpandAll; + uint32_t viewFlag = (m_sortType == nsMsgViewSortType::byDate) ? MSG_VIEW_FLAG_DUMMY : 0; + if (viewFlag && m_db) + { + nsCOMPtr <nsIDBFolderInfo> dbFolderInfo; + nsresult rv = m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + NS_ENSURE_SUCCESS(rv, rv); + if (dbFolderInfo) + dbFolderInfo->GetUint32Property("dateGroupFlags", 0, &expandFlags); + } + // go through the view updating the flags for threads with more than one message... + // and if grouped by date, expanding threads that were expanded before. + for (uint32_t viewIndex = 0; viewIndex < m_keys.Length(); viewIndex++) + { + nsCOMPtr <nsIMsgThread> thread; + GetThreadContainingIndex(viewIndex, getter_AddRefs(thread)); + if (thread) + { + uint32_t numChildren; + thread->GetNumChildren(&numChildren); + if (numChildren > 1 || viewFlag) + OrExtraFlag(viewIndex, viewFlag | MSG_VIEW_FLAG_HASCHILDREN); + if (expandAll || expandFlags) + { + nsMsgGroupThread *groupThread = static_cast<nsMsgGroupThread *>((nsIMsgThread *) thread); + if (expandAll || expandFlags & (1 << groupThread->m_threadKey)) + { + uint32_t numExpanded; + ExpandByIndex(viewIndex, &numExpanded); + viewIndex += numExpanded; + } + } + } + } + *aCount = m_keys.Length(); + return rv; +} + +// we wouldn't need this if we never instantiated this directly, +// but instead used nsMsgThreadedDBView with the grouping flag set. +// Or, we could get rid of the nsMsgThreadedDBView impl of this method. +NS_IMETHODIMP nsMsgGroupView::GetViewType(nsMsgViewTypeValue *aViewType) +{ + NS_ENSURE_ARG_POINTER(aViewType); + *aViewType = nsMsgViewType::eShowAllThreads; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgGroupView::CopyDBView(nsMsgDBView *aNewMsgDBView, nsIMessenger *aMessengerInstance, + nsIMsgWindow *aMsgWindow, nsIMsgDBViewCommandUpdater *aCmdUpdater) +{ + nsMsgDBView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater); + nsMsgGroupView* newMsgDBView = (nsMsgGroupView *) aNewMsgDBView; + + // If grouped, we need to clone the group thread hash table. + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) { + for (auto iter = m_groupsTable.Iter(); !iter.Done(); iter.Next()) { + newMsgDBView->m_groupsTable.Put(iter.Key(), iter.UserData()); + } + } + return NS_OK; +} + +// E.g., if the day has changed, we need to close and re-open the view. +// Or, if we're switching between grouping and threading in a cross-folder +// saved search. In that case, we needed to build an enumerator based on the +// old view type, and internally close the view based on its old type, but +// rebuild the new view based on the new view type. So we pass the new +// view flags to OpenWithHdrs. +nsresult nsMsgGroupView::RebuildView(nsMsgViewFlagsTypeValue newFlags) +{ + nsCOMPtr <nsISimpleEnumerator> headers; + if (NS_SUCCEEDED(GetMessageEnumerator(getter_AddRefs(headers)))) + { + int32_t count; + m_dayChanged = false; + AutoTArray<nsMsgKey, 1> preservedSelection; + nsMsgKey curSelectedKey; + SaveAndClearSelection(&curSelectedKey, preservedSelection); + InternalClose(); + int32_t oldSize = GetSize(); + // this is important, because the tree will ask us for our + // row count, which get determine from the number of keys. + m_keys.Clear(); + // be consistent + m_flags.Clear(); + m_levels.Clear(); + + // this needs to happen after we remove all the keys, since RowCountChanged() will call our GetRowCount() + if (mTree) + mTree->RowCountChanged(0, -oldSize); + SetSuppressChangeNotifications(true); + nsresult rv = OpenWithHdrs(headers, m_sortType, m_sortOrder, newFlags, &count); + SetSuppressChangeNotifications(false); + if (mTree) + mTree->RowCountChanged(0, GetSize()); + + NS_ENSURE_SUCCESS(rv,rv); + + // now, restore our desired selection + AutoTArray<nsMsgKey, 1> keyArray; + keyArray.AppendElement(curSelectedKey); + + return RestoreSelection(curSelectedKey, keyArray); + } + return NS_OK; +} + +nsresult nsMsgGroupView::OnNewHeader(nsIMsgDBHdr *newHdr, nsMsgKey aParentKey, bool ensureListed) +{ + if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) + return nsMsgDBView::OnNewHeader(newHdr, aParentKey, ensureListed); + + // check if we're adding a header, and the current day has changed. If it has, we're just going to + // close and re-open the view so things will be correctly categorized. + if (m_dayChanged) + return RebuildView(m_viewFlags); + + bool newThread; + nsMsgGroupThread *thread = AddHdrToThread(newHdr, &newThread); + if (thread) + { + // find the view index of (the root node of) the thread + nsMsgViewIndex threadIndex = ThreadIndexOfMsgHdr(newHdr); + // may need to fix thread counts + if (threadIndex != nsMsgViewIndex_None) + { + if (newThread) + { + // AddHdrToThread creates the header elided, so we need to un-elide it + // if we want it expanded. + if(m_viewFlags & nsMsgViewFlagsType::kExpandAll) + m_flags[threadIndex] &= ~nsMsgMessageFlags::Elided; + } + else + { + m_flags[threadIndex] |= MSG_VIEW_FLAG_HASCHILDREN + | MSG_VIEW_FLAG_ISTHREAD; + } + + int32_t numRowsToInvalidate = 1; + // if the thread is expanded (not elided), we should add the header to + // the view. + if (! (m_flags[threadIndex] & nsMsgMessageFlags::Elided)) + { + uint32_t msgIndexInThread = thread->FindMsgHdr(newHdr); + bool insertedAtThreadRoot = !msgIndexInThread; + // Add any new display node and potentially fix-up changes in the root. + // (If this is a new thread and we are not using a dummy row, the only + // node to display is the root node which has already been added by + // AddHdrToThread. And since there is just the one, no change in root + // could have occurred, so we have nothing to do.) + if (!newThread || GroupViewUsesDummyRow()) + { + // we never want to insert/update the root node, because + // AddHdrToThread has already done that for us (in all cases). + if (insertedAtThreadRoot) + msgIndexInThread++; + // If this header is the new parent of the thread... AND + // If we are not using a dummy row, this means we need to append our + // old node as the first child of the new root. + // (If we are using a dummy row, the old node's "content" node already + // exists (at position threadIndex + 1) and we need to insert the + // "content" copy of the new root node there, pushing our old + // "content" node down.) + // Example mini-diagrams, wrapping the to-add thing with () + // No dummy row; we had: [A], now we have [B], we want [B (A)]. + // Dummy row; we had: [A A], now we have [B A], we want [B (B) A]. + // (Coming into this we're adding 'B') + if (!newThread && insertedAtThreadRoot && !GroupViewUsesDummyRow()) + { + // grab a copy of the old root node ('A') from the thread so we can + // insert it. (offset msgIndexInThread=1 is the right thing; we are + // non-dummy.) + thread->GetChildHdrAt(msgIndexInThread, &newHdr); + } // nothing to do for dummy case, we're already inserting 'B'. + nsMsgKey msgKey; + uint32_t msgFlags; + newHdr->GetMessageKey(&msgKey); + newHdr->GetFlags(&msgFlags); + InsertMsgHdrAt(threadIndex + msgIndexInThread, newHdr, msgKey, + msgFlags, 1); + } + // the call to NoteChange() has to happen after we add the key + // as NoteChange() will call RowCountChanged() which will call our GetRowCount() + // (msgIndexInThread states. new thread: 0, old thread at root: 1) + if (newThread && GroupViewUsesDummyRow()) + NoteChange(threadIndex, 2, nsMsgViewNotificationCode::insertOrDelete); + else + NoteChange(threadIndex + msgIndexInThread, 1, + nsMsgViewNotificationCode::insertOrDelete); + numRowsToInvalidate = msgIndexInThread; + } + // we still need the addition notification for new threads when elided + else if (newThread) + { + NoteChange(threadIndex, 1, + nsMsgViewNotificationCode::insertOrDelete); + } + NoteChange(threadIndex, numRowsToInvalidate, nsMsgViewNotificationCode::changed); + } + } + // if thread is expanded, we need to add hdr to view... + return NS_OK; +} + +NS_IMETHODIMP nsMsgGroupView::OnHdrFlagsChanged(nsIMsgDBHdr *aHdrChanged, uint32_t aOldFlags, + uint32_t aNewFlags, nsIDBChangeListener *aInstigator) +{ + if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) + return nsMsgDBView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, + aInstigator); + + nsCOMPtr <nsIMsgThread> thread; + + // check if we're adding a header, and the current day has changed. If it has, we're just going to + // close and re-open the view so things will be correctly categorized. + if (m_dayChanged) + return RebuildView(m_viewFlags); + + nsresult rv = GetThreadContainingMsgHdr(aHdrChanged, getter_AddRefs(thread)); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t deltaFlags = (aOldFlags ^ aNewFlags); + if (deltaFlags & nsMsgMessageFlags::Read) + thread->MarkChildRead(aNewFlags & nsMsgMessageFlags::Read); + + return nsMsgDBView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, aInstigator); +} + +NS_IMETHODIMP nsMsgGroupView::OnHdrDeleted(nsIMsgDBHdr *aHdrDeleted, nsMsgKey aParentKey, int32_t aFlags, + nsIDBChangeListener *aInstigator) +{ + if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) + return nsMsgDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags, aInstigator); + + // check if we're adding a header, and the current day has changed. If it has, we're just going to + // close and re-open the view so things will be correctly categorized. + if (m_dayChanged) + return RebuildView(m_viewFlags); + + nsCOMPtr <nsIMsgThread> thread; + nsMsgKey keyDeleted; + aHdrDeleted->GetMessageKey(&keyDeleted); + + nsresult rv = GetThreadContainingMsgHdr(aHdrDeleted, getter_AddRefs(thread)); + NS_ENSURE_SUCCESS(rv, rv); + nsMsgViewIndex viewIndexOfThread = GetIndexOfFirstDisplayedKeyInThread( + thread, true); // yes to dummy node + thread->RemoveChildHdr(aHdrDeleted, nullptr); + + nsMsgGroupThread *groupThread = static_cast<nsMsgGroupThread *>((nsIMsgThread *) thread); + + bool rootDeleted = viewIndexOfThread != nsMsgKey_None && + m_keys[viewIndexOfThread] == keyDeleted; + rv = nsMsgDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags, aInstigator); + if (groupThread->m_dummy) + { + if (!groupThread->NumRealChildren()) + { + thread->RemoveChildAt(0); // get rid of dummy + if (viewIndexOfThread != nsMsgKey_None) + { + RemoveByIndex(viewIndexOfThread); + if (m_deletingRows) + mIndicesToNoteChange.AppendElement(viewIndexOfThread); + } + } + else if (rootDeleted) + { + // reflect new thread root into view.dummy row. + nsCOMPtr<nsIMsgDBHdr> hdr; + thread->GetChildHdrAt(0, getter_AddRefs(hdr)); + if (hdr) + { + nsMsgKey msgKey; + hdr->GetMessageKey(&msgKey); + SetMsgHdrAt(hdr, viewIndexOfThread, msgKey, m_flags[viewIndexOfThread], 0); + } + } + } + if (!groupThread->m_keys.Length()) + { + nsString hashKey; + rv = HashHdr(aHdrDeleted, hashKey); + if (NS_SUCCEEDED(rv)) + m_groupsTable.Remove(hashKey); + } + return rv; +} + +NS_IMETHODIMP nsMsgGroupView::GetRowProperties(int32_t aRow, nsAString& aProperties) +{ + if (!IsValidIndex(aRow)) + return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) + { + aProperties.AssignLiteral("dummy"); + return NS_OK; + } + + return nsMsgDBView::GetRowProperties(aRow, aProperties); +} + +NS_IMETHODIMP nsMsgGroupView::GetCellProperties(int32_t aRow, + nsITreeColumn *aCol, + nsAString& aProperties) +{ + if (!IsValidIndex(aRow)) + return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) + { + aProperties.AssignLiteral("dummy read"); + + if (!(m_flags[aRow] & nsMsgMessageFlags::Elided)) + return NS_OK; + + // Set unread property if a collapsed group thread has unread. + nsCOMPtr <nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + nsString hashKey; + rv = HashHdr(msgHdr, hashKey); + if (NS_FAILED(rv)) + return NS_OK; + + nsCOMPtr<nsIMsgThread> msgThread; + m_groupsTable.Get(hashKey, getter_AddRefs(msgThread)); + nsMsgGroupThread *groupThread = static_cast<nsMsgGroupThread *>(msgThread.get()); + if (!groupThread) + return NS_OK; + + uint32_t numUnrMsg = 0; + groupThread->GetNumUnreadChildren(&numUnrMsg); + if (numUnrMsg > 0) + aProperties.AppendLiteral(" hasUnread"); + + return NS_OK; + } + + return nsMsgDBView::GetCellProperties(aRow, aCol, aProperties); +} + +NS_IMETHODIMP nsMsgGroupView::CellTextForColumn(int32_t aRow, + const char16_t *aColumnName, + nsAString &aValue) { + if (!IsValidIndex(aRow)) + return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_flags[aRow] & MSG_VIEW_FLAG_DUMMY && aColumnName[0] != 'u') + { + nsCOMPtr <nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + nsString hashKey; + rv = HashHdr(msgHdr, hashKey); + if (NS_FAILED(rv)) + return NS_OK; + nsCOMPtr<nsIMsgThread> msgThread; + m_groupsTable.Get(hashKey, getter_AddRefs(msgThread)); + nsMsgGroupThread * groupThread = static_cast<nsMsgGroupThread *>(msgThread.get()); + if (aColumnName[0] == 's' && aColumnName[1] == 'u' ) + { + uint32_t flags; + bool rcvDate = false; + msgHdr->GetFlags(&flags); + aValue.Truncate(); + nsString tmp_str; + switch (m_sortType) + { + case nsMsgViewSortType::byReceived: + rcvDate = true; + MOZ_FALLTHROUGH; + case nsMsgViewSortType::byDate: + { + uint32_t ageBucket = 0; + GetAgeBucketValue(msgHdr, &ageBucket, rcvDate); + switch (ageBucket) + { + case 1: + if (m_kTodayString.IsEmpty()) + m_kTodayString.Adopt(GetString(u"today")); + aValue.Assign(m_kTodayString); + break; + case 2: + if (m_kYesterdayString.IsEmpty()) + m_kYesterdayString.Adopt(GetString(u"yesterday")); + aValue.Assign(m_kYesterdayString); + break; + case 3: + if (m_kLastWeekString.IsEmpty()) + m_kLastWeekString.Adopt(GetString(u"last7Days")); + aValue.Assign(m_kLastWeekString); + break; + case 4: + if (m_kTwoWeeksAgoString.IsEmpty()) + m_kTwoWeeksAgoString.Adopt(GetString(u"last14Days")); + aValue.Assign(m_kTwoWeeksAgoString); + break; + case 5: + if (m_kOldMailString.IsEmpty()) + m_kOldMailString.Adopt(GetString(u"older")); + aValue.Assign(m_kOldMailString); + break; + default: + // Future date, error/spoofed. + if (m_kFutureDateString.IsEmpty()) + m_kFutureDateString.Adopt(GetString(u"futureDate")); + aValue.Assign(m_kFutureDateString); + break; + } + break; + } + case nsMsgViewSortType::bySubject: + FetchSubject(msgHdr, m_flags[aRow], aValue); + break; + case nsMsgViewSortType::byAuthor: + FetchAuthor(msgHdr, aValue); + break; + case nsMsgViewSortType::byStatus: + rv = FetchStatus(m_flags[aRow], aValue); + if (aValue.IsEmpty()) { + tmp_str.Adopt(GetString(u"messagesWithNoStatus")); + aValue.Assign(tmp_str); + } + break; + case nsMsgViewSortType::byTags: + rv = FetchTags(msgHdr, aValue); + if (aValue.IsEmpty()) { + tmp_str.Adopt(GetString(u"untaggedMessages")); + aValue.Assign(tmp_str); + } + break; + case nsMsgViewSortType::byPriority: + FetchPriority(msgHdr, aValue); + if (aValue.IsEmpty()) { + tmp_str.Adopt(GetString(u"noPriority")); + aValue.Assign(tmp_str); + } + break; + case nsMsgViewSortType::byAccount: + FetchAccount(msgHdr, aValue); + break; + case nsMsgViewSortType::byRecipient: + FetchRecipients(msgHdr, aValue); + break; + case nsMsgViewSortType::byAttachments: + tmp_str.Adopt(GetString(flags & nsMsgMessageFlags::Attachment ? + u"attachments" : u"noAttachments")); + aValue.Assign(tmp_str); + break; + case nsMsgViewSortType::byFlagged: + tmp_str.Adopt(GetString(flags & nsMsgMessageFlags::Marked ? + u"groupFlagged" : u"notFlagged")); + aValue.Assign(tmp_str); + break; + // byLocation is a special case; we don't want to have duplicate + // all this logic in nsMsgSearchDBView, and its hash key is what we + // want anyways, so just copy it across. + case nsMsgViewSortType::byLocation: + case nsMsgViewSortType::byCorrespondent: + aValue = hashKey; + break; + case nsMsgViewSortType::byCustom: + { + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + if (colHandler) + { + bool isString; + colHandler->IsString(&isString); + if (isString) + rv = colHandler->GetSortStringForRow(msgHdr.get(), aValue); + else + { + uint32_t intKey; + rv = colHandler->GetSortLongForRow(msgHdr.get(), &intKey); + aValue.AppendInt(intKey); + } + } + if (aValue.IsEmpty()) + aValue.AssignLiteral("*"); + break; + } + + default: + NS_ASSERTION(false, "we don't sort by group for this type"); + break; + } + + if (groupThread) + { + // Get number of messages in group + nsAutoString formattedCountMsg; + uint32_t numMsg = groupThread->NumRealChildren(); + formattedCountMsg.AppendInt(numMsg); + + // Get number of unread messages + nsAutoString formattedCountUnrMsg; + uint32_t numUnrMsg = 0; + groupThread->GetNumUnreadChildren(&numUnrMsg); + formattedCountUnrMsg.AppendInt(numUnrMsg); + + // Add text to header + aValue.Append(NS_LITERAL_STRING(" (")); + if (numUnrMsg) + { + aValue.Append(formattedCountUnrMsg); + aValue.Append(NS_LITERAL_STRING("/")); + } + + aValue.Append(formattedCountMsg); + aValue.Append(NS_LITERAL_STRING(")")); + } + } + else if (aColumnName[0] == 't' && aColumnName[1] == 'o') + { + nsAutoString formattedCountString; + uint32_t numChildren = (groupThread) ? groupThread->NumRealChildren() : 0; + formattedCountString.AppendInt(numChildren); + aValue.Assign(formattedCountString); + } + return NS_OK; + } + return nsMsgDBView::CellTextForColumn(aRow, aColumnName, aValue); +} + +NS_IMETHODIMP nsMsgGroupView::LoadMessageByViewIndex(nsMsgViewIndex aViewIndex) +{ + if (!IsValidIndex(aViewIndex)) + return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_flags[aViewIndex] & MSG_VIEW_FLAG_DUMMY) + { + // if we used to have one item selected, and now we have more than one, we should clear the message pane. + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + nsCOMPtr <nsIMsgWindowCommands> windowCommands; + if (msgWindow && NS_SUCCEEDED(msgWindow->GetWindowCommands(getter_AddRefs(windowCommands))) && windowCommands) + windowCommands->ClearMsgPane(); + // since we are selecting a dummy row, we should also clear out m_currentlyDisplayedMsgUri + m_currentlyDisplayedMsgUri.Truncate(); + return NS_OK; + } + else + return nsMsgDBView::LoadMessageByViewIndex(aViewIndex); +} + +NS_IMETHODIMP nsMsgGroupView::GetThreadContainingMsgHdr(nsIMsgDBHdr *msgHdr, nsIMsgThread **pThread) +{ + if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) + return nsMsgDBView::GetThreadContainingMsgHdr(msgHdr, pThread); + + nsString hashKey; + nsresult rv = HashHdr(msgHdr, hashKey); + *pThread = nullptr; + if (NS_SUCCEEDED(rv)) + { + nsCOMPtr<nsIMsgThread> thread; + m_groupsTable.Get(hashKey, getter_AddRefs(thread)); + thread.swap(*pThread); + } + return (*pThread) ? NS_OK : NS_ERROR_FAILURE; +} + +int32_t nsMsgGroupView::FindLevelInThread(nsIMsgDBHdr *msgHdr, + nsMsgViewIndex startOfThread, nsMsgViewIndex viewIndex) +{ + if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) + return nsMsgDBView::FindLevelInThread(msgHdr, startOfThread, viewIndex); + return (startOfThread == viewIndex) ? 0 : 1; +} + +bool nsMsgGroupView::GroupViewUsesDummyRow() +{ + // Return true to always use a header row as root grouped parent row. + return true; +} + +NS_IMETHODIMP nsMsgGroupView::AddColumnHandler(const nsAString& column, + nsIMsgCustomColumnHandler* handler) +{ + nsMsgDBView::AddColumnHandler(column, handler); + + // If the sortType is byCustom and the desired custom column is the one just + // registered, build the view. + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort && + m_sortType == nsMsgViewSortType::byCustom) + { + nsAutoString curCustomColumn; + GetCurCustomColumn(curCustomColumn); + if (curCustomColumn == column) + RebuildView(m_viewFlags); + } + + return NS_OK; +} |