/* -*- 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 "nsMsgThreadedDBView.h" #include "nsIMsgHdr.h" #include "nsIMsgThread.h" #include "nsIDBFolderInfo.h" #include "nsIMsgSearchSession.h" #include "nsMsgMessageFlags.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 nsMsgThreadedDBView::nsMsgThreadedDBView() { /* member initializers and constructor code */ m_havePrevView = false; } nsMsgThreadedDBView::~nsMsgThreadedDBView() { /* destructor code */ } NS_IMETHODIMP nsMsgThreadedDBView::Open(nsIMsgFolder *folder, nsMsgViewSortTypeValue sortType, nsMsgViewSortOrderValue sortOrder, nsMsgViewFlagsTypeValue viewFlags, int32_t *pCount) { nsresult rv = nsMsgDBView::Open(folder, sortType, sortOrder, viewFlags, pCount); NS_ENSURE_SUCCESS(rv, rv); if (!m_db) return NS_ERROR_NULL_POINTER; // Preset msg hdr cache size for performance reason. int32_t totalMessages, unreadMessages; nsCOMPtr dbFolderInfo; PersistFolderInfo(getter_AddRefs(dbFolderInfo)); NS_ENSURE_SUCCESS(rv, rv); // save off sort type and order, view type and flags dbFolderInfo->GetNumUnreadMessages(&unreadMessages); dbFolderInfo->GetNumMessages(&totalMessages); if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) { // Set unread msg size + extra entries to avoid reallocation on new mail. totalMessages = (uint32_t)unreadMessages+MSGHDR_CACHE_LOOK_AHEAD_SIZE; } else { if (totalMessages > MSGHDR_CACHE_MAX_SIZE) totalMessages = MSGHDR_CACHE_MAX_SIZE; // use max default else if (totalMessages > 0) totalMessages += MSGHDR_CACHE_LOOK_AHEAD_SIZE;// allocate extra entries to avoid reallocation on new mail. } // if total messages is 0, then we probably don't have any idea how many headers are in the db // so we have no business setting the cache size. if (totalMessages > 0) m_db->SetMsgHdrCacheSize((uint32_t)totalMessages); if (pCount) *pCount = 0; rv = InitThreadedView(pCount); // this is a hack, but we're trying to find a way to correct // incorrect total and unread msg counts w/o paying a big // performance penalty. So, if we're not threaded, just add // up the total and unread messages in the view and see if that // matches what the db totals say. Except ignored threads are // going to throw us off...hmm. Unless we just look at the // unread counts which is what mostly tweaks people anyway... int32_t unreadMsgsInView = 0; if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { for (uint32_t i = m_flags.Length(); i > 0; ) { if (!(m_flags[--i] & nsMsgMessageFlags::Read)) ++unreadMsgsInView; } if (unreadMessages != unreadMsgsInView) m_db->SyncCounts(); } m_db->SetMsgHdrCacheSize(MSGHDR_CACHE_DEFAULT_SIZE); return rv; } NS_IMETHODIMP nsMsgThreadedDBView::Close() { return nsMsgDBView::Close(); } nsresult nsMsgThreadedDBView::InitThreadedView(int32_t *pCount) { nsresult rv; m_keys.Clear(); m_flags.Clear(); m_levels.Clear(); m_prevKeys.Clear(); m_prevFlags.Clear(); m_prevLevels.Clear(); m_havePrevView = false; nsresult getSortrv = NS_OK; // ### TODO m_db->GetSortInfo(&sortType, &sortOrder); // list all the ids into m_keys. nsMsgKey startMsg = 0; do { const int32_t kIdChunkSize = 400; int32_t numListed = 0; nsMsgKey idArray[kIdChunkSize]; int32_t flagArray[kIdChunkSize]; char levelArray[kIdChunkSize]; rv = ListThreadIds(&startMsg, (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) != 0, idArray, flagArray, levelArray, kIdChunkSize, &numListed, nullptr); if (NS_SUCCEEDED(rv)) { int32_t numAdded = AddKeys(idArray, flagArray, levelArray, m_sortType, numListed); if (pCount) *pCount += numAdded; } } while (NS_SUCCEEDED(rv) && startMsg != nsMsgKey_None); if (NS_SUCCEEDED(getSortrv)) { rv = InitSort(m_sortType, m_sortOrder); SaveSortInfo(m_sortType, m_sortOrder); } return rv; } nsresult nsMsgThreadedDBView::SortThreads(nsMsgViewSortTypeValue sortType, nsMsgViewSortOrderValue sortOrder) { NS_PRECONDITION(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay, "trying to sort unthreaded threads"); uint32_t numThreads = 0; // the idea here is that copy the current view, then build up an m_keys and m_flags array of just the top level // messages in the view, and then call nsMsgDBView::Sort(sortType, sortOrder). // Then, we expand the threads in the result array that were expanded in the original view (perhaps by copying // from the original view, but more likely just be calling expand). for (uint32_t i = 0; i < m_keys.Length(); i++) { if (m_flags[i] & MSG_VIEW_FLAG_ISTHREAD) { if (numThreads < i) { m_keys[numThreads] = m_keys[i]; m_flags[numThreads] = m_flags[i]; } m_levels[numThreads] = 0; numThreads++; } } m_keys.SetLength(numThreads); m_flags.SetLength(numThreads); m_levels.SetLength(numThreads); //m_viewFlags &= ~nsMsgViewFlagsType::kThreadedDisplay; m_sortType = nsMsgViewSortType::byNone; // sort from scratch nsMsgDBView::Sort(sortType, sortOrder); m_viewFlags |= nsMsgViewFlagsType::kThreadedDisplay; SetSuppressChangeNotifications(true); // Loop through the original array, for each thread that's expanded, find it in the new array // and expand the thread. We have to update MSG_VIEW_FLAG_HAS_CHILDREN because // we may be going from a flat sort, which doesn't maintain that flag, // to a threaded sort, which requires that flag. for (uint32_t j = 0; j < m_keys.Length(); j++) { uint32_t flags = m_flags[j]; if ((flags & (MSG_VIEW_FLAG_HASCHILDREN | nsMsgMessageFlags::Elided)) == MSG_VIEW_FLAG_HASCHILDREN) { uint32_t numExpanded; m_flags[j] = flags | nsMsgMessageFlags::Elided; ExpandByIndex(j, &numExpanded); j += numExpanded; if (numExpanded > 0) m_flags[j - numExpanded] = flags | MSG_VIEW_FLAG_HASCHILDREN; } else if (flags & MSG_VIEW_FLAG_ISTHREAD && ! (flags & MSG_VIEW_FLAG_HASCHILDREN)) { nsCOMPtr msgHdr; nsCOMPtr pThread; m_db->GetMsgHdrForKey(m_keys[j], getter_AddRefs(msgHdr)); if (msgHdr) { m_db->GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(pThread)); if (pThread) { uint32_t numChildren; pThread->GetNumChildren(&numChildren); if (numChildren > 1) m_flags[j] = flags | MSG_VIEW_FLAG_HASCHILDREN | nsMsgMessageFlags::Elided; } } } } SetSuppressChangeNotifications(false); return NS_OK; } int32_t nsMsgThreadedDBView::AddKeys(nsMsgKey *pKeys, int32_t *pFlags, const char *pLevels, nsMsgViewSortTypeValue sortType, int32_t numKeysToAdd) { int32_t numAdded = 0; // Allocate enough space first to avoid memory allocation/deallocation. m_keys.SetCapacity(m_keys.Length() + numKeysToAdd); m_flags.SetCapacity(m_flags.Length() + numKeysToAdd); m_levels.SetCapacity(m_levels.Length() + numKeysToAdd); for (int32_t i = 0; i < numKeysToAdd; i++) { int32_t threadFlag = pFlags[i]; int32_t flag = threadFlag; // skip ignored threads. if ((threadFlag & nsMsgMessageFlags::Ignored) && !(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) continue; // skip ignored subthreads nsCOMPtr msgHdr; m_db->GetMsgHdrForKey(pKeys[i], getter_AddRefs(msgHdr)); if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) { bool killed; msgHdr->GetIsKilled(&killed); if (killed) continue; } // by default, make threads collapsed, unless we're in only viewing new msgs if (flag & MSG_VIEW_FLAG_HASCHILDREN) flag |= nsMsgMessageFlags::Elided; // should this be persistent? Doesn't seem to need to be. flag |= MSG_VIEW_FLAG_ISTHREAD; m_keys.AppendElement(pKeys[i]); m_flags.AppendElement(flag); m_levels.AppendElement(pLevels[i]); numAdded++; // we expand as we build the view, which allows us to insert at the end of the key array, // instead of the middle, and is much faster. if ((!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) || m_viewFlags & nsMsgViewFlagsType::kExpandAll) && flag & nsMsgMessageFlags::Elided) ExpandByIndex(m_keys.Length() - 1, NULL); } return numAdded; } NS_IMETHODIMP nsMsgThreadedDBView::Sort(nsMsgViewSortTypeValue sortType, nsMsgViewSortOrderValue sortOrder) { nsresult rv; int32_t rowCountBeforeSort = GetSize(); if (!rowCountBeforeSort) { // still need to setup our flags even when no articles - bug 98183. m_sortType = sortType; if (sortType == nsMsgViewSortType::byThread && ! (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) SetViewFlags(m_viewFlags | nsMsgViewFlagsType::kThreadedDisplay); SaveSortInfo(sortType, sortOrder); return NS_OK; } if (!m_checkedCustomColumns && CustomColumnsInSortAndNotRegistered()) return NS_OK; // sort threads by sort order bool sortThreads = m_viewFlags & (nsMsgViewFlagsType::kThreadedDisplay | nsMsgViewFlagsType::kGroupBySort); // if sort type is by thread, and we're already threaded, change sort type to byId if (sortType == nsMsgViewSortType::byThread && (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) != 0) sortType = nsMsgViewSortType::byId; nsMsgKey preservedKey; AutoTArray preservedSelection; SaveAndClearSelection(&preservedKey, preservedSelection); // if the client wants us to forget our cached id arrays, they // should build a new view. If this isn't good enough, we // need a method to do that. if (sortType != m_sortType || !m_sortValid || sortThreads) { SaveSortInfo(sortType, sortOrder); if (sortType == nsMsgViewSortType::byThread) { m_sortType = sortType; m_viewFlags |= nsMsgViewFlagsType::kThreadedDisplay; m_viewFlags &= ~nsMsgViewFlagsType::kGroupBySort; if ( m_havePrevView) { // restore saved id array and flags array m_keys = m_prevKeys; m_flags = m_prevFlags; m_levels = m_prevLevels; m_sortValid = true; // the sort may have changed the number of rows // before we restore the selection, tell the tree // do this before we call restore selection // this is safe when there is no selection. rv = AdjustRowCount(rowCountBeforeSort, GetSize()); RestoreSelection(preservedKey, preservedSelection); if (mTree) mTree->Invalidate(); return NS_OK; } else { // set sort info in anticipation of what Init will do. InitThreadedView(nullptr); // build up thread list. if (sortOrder != nsMsgViewSortOrder::ascending) Sort(sortType, sortOrder); // the sort may have changed the number of rows // before we update the selection, tell the tree // do this before we call restore selection // this is safe when there is no selection. rv = AdjustRowCount(rowCountBeforeSort, GetSize()); RestoreSelection(preservedKey, preservedSelection); if (mTree) mTree->Invalidate(); return NS_OK; } } else if (sortType != nsMsgViewSortType::byThread && (m_sortType == nsMsgViewSortType::byThread || sortThreads)/* && !m_havePrevView*/) { if (sortThreads) { SortThreads(sortType, sortOrder); sortType = nsMsgViewSortType::byThread; // hack so base class won't do anything } else { // going from SortByThread to non-thread sort - must build new key, level,and flags arrays m_prevKeys = m_keys; m_prevFlags = m_flags; m_prevLevels = m_levels; // do this before we sort, so that we'll use the cheap method // of expanding. m_viewFlags &= ~(nsMsgViewFlagsType::kThreadedDisplay | nsMsgViewFlagsType::kGroupBySort); ExpandAll(); // m_idArray.RemoveAll(); // m_flags.Clear(); m_havePrevView = true; } } } else if (m_sortOrder != sortOrder)// check for toggling the sort { nsMsgDBView::Sort(sortType, sortOrder); } if (!sortThreads) { // call the base class in case we're not sorting by thread rv = nsMsgDBView::Sort(sortType, sortOrder); SaveSortInfo(sortType, sortOrder); } // the sort may have changed the number of rows // before we restore the selection, tell the tree // do this before we call restore selection // this is safe when there is no selection. rv = AdjustRowCount(rowCountBeforeSort, GetSize()); RestoreSelection(preservedKey, preservedSelection); if (mTree) mTree->Invalidate(); NS_ENSURE_SUCCESS(rv,rv); return NS_OK; } // list the ids of the top-level thread ids starting at id == startMsg. This actually returns // the ids of the first message in each thread. nsresult nsMsgThreadedDBView::ListThreadIds(nsMsgKey *startMsg, bool unreadOnly, nsMsgKey *pOutput, int32_t *pFlags, char *pLevels, int32_t numToList, int32_t *pNumListed, int32_t *pTotalHeaders) { nsresult rv = NS_OK; // N.B..don't ret before assigning numListed to *pNumListed int32_t numListed = 0; if (*startMsg > 0) { NS_ASSERTION(m_threadEnumerator != nullptr, "where's our iterator?"); // for now, we'll just have to rely on the caller leaving // the iterator in the right place. } else { NS_ASSERTION(m_db, "no db"); if (!m_db) return NS_ERROR_UNEXPECTED; rv = m_db->EnumerateThreads(getter_AddRefs(m_threadEnumerator)); NS_ENSURE_SUCCESS(rv, rv); } bool hasMore = false; nsCOMPtr threadHdr ; int32_t threadsRemoved = 0; while (numListed < numToList && NS_SUCCEEDED(rv = m_threadEnumerator->HasMoreElements(&hasMore)) && hasMore) { nsCOMPtr supports; rv = m_threadEnumerator->GetNext(getter_AddRefs(supports)); if (NS_FAILED(rv)) { threadHdr = nullptr; break; } threadHdr = do_QueryInterface(supports); if (!threadHdr) break; nsCOMPtr msgHdr; uint32_t numChildren; if (unreadOnly) threadHdr->GetNumUnreadChildren(&numChildren); else threadHdr->GetNumChildren(&numChildren); uint32_t threadFlags; threadHdr->GetFlags(&threadFlags); if (numChildren != 0) // not empty thread { int32_t unusedRootIndex; if (pTotalHeaders) *pTotalHeaders += numChildren; if (unreadOnly) rv = threadHdr->GetFirstUnreadChild(getter_AddRefs(msgHdr)); else rv = threadHdr->GetRootHdr(&unusedRootIndex, getter_AddRefs(msgHdr)); if (NS_SUCCEEDED(rv) && msgHdr != nullptr && WantsThisThread(threadHdr)) { uint32_t msgFlags; uint32_t newMsgFlags; nsMsgKey msgKey; msgHdr->GetMessageKey(&msgKey); msgHdr->GetFlags(&msgFlags); // turn off high byte of msg flags - used for view flags. msgFlags &= ~MSG_VIEW_FLAGS; pOutput[numListed] = msgKey; pLevels[numListed] = 0; // turn off these flags on msg hdr - they belong in thread msgHdr->AndFlags(~(nsMsgMessageFlags::Watched), &newMsgFlags); AdjustReadFlag(msgHdr, &msgFlags); // try adding in MSG_VIEW_FLAG_ISTHREAD flag for unreadonly view. pFlags[numListed] = msgFlags | MSG_VIEW_FLAG_ISTHREAD | threadFlags; if (numChildren > 1) pFlags[numListed] |= MSG_VIEW_FLAG_HASCHILDREN; numListed++; } else NS_ASSERTION(NS_SUCCEEDED(rv) && msgHdr, "couldn't get header for some reason"); } else if (threadsRemoved < 10 && !(threadFlags & (nsMsgMessageFlags::Watched | nsMsgMessageFlags::Ignored))) { // ### remove thread. threadsRemoved++; // don't want to remove all empty threads first time // around as it will choke preformance for upgrade. #ifdef DEBUG_bienvenu printf("removing empty non-ignored non-watched thread\n"); #endif } } if (hasMore && threadHdr) { threadHdr->GetThreadKey(startMsg); } else { *startMsg = nsMsgKey_None; nsCOMPtr dbListener = do_QueryInterface(m_threadEnumerator); // this is needed to make the thread enumerator release its reference to the db. if (dbListener) dbListener->OnAnnouncerGoingAway(nullptr); m_threadEnumerator = nullptr; } *pNumListed = numListed; return rv; } void nsMsgThreadedDBView::OnExtraFlagChanged(nsMsgViewIndex index, uint32_t extraFlag) { if (IsValidIndex(index)) { if (m_havePrevView) { nsMsgKey keyChanged = m_keys[index]; nsMsgViewIndex prevViewIndex = m_prevKeys.IndexOf(keyChanged); if (prevViewIndex != nsMsgViewIndex_None) { uint32_t prevFlag = m_prevFlags[prevViewIndex]; // don't want to change the elided bit, or has children or is thread if (prevFlag & nsMsgMessageFlags::Elided) extraFlag |= nsMsgMessageFlags::Elided; else extraFlag &= ~nsMsgMessageFlags::Elided; if (prevFlag & MSG_VIEW_FLAG_ISTHREAD) extraFlag |= MSG_VIEW_FLAG_ISTHREAD; else extraFlag &= ~MSG_VIEW_FLAG_ISTHREAD; if (prevFlag & MSG_VIEW_FLAG_HASCHILDREN) extraFlag |= MSG_VIEW_FLAG_HASCHILDREN; else extraFlag &= ~MSG_VIEW_FLAG_HASCHILDREN; m_prevFlags[prevViewIndex] = extraFlag; // will this be right? } } } // we don't really know what's changed, but to be on the safe side, set the sort invalid // so that reverse sort will pick it up. if (m_sortType == nsMsgViewSortType::byStatus || m_sortType == nsMsgViewSortType::byFlagged || m_sortType == nsMsgViewSortType::byUnread || m_sortType == nsMsgViewSortType::byPriority) m_sortValid = false; } void nsMsgThreadedDBView::OnHeaderAddedOrDeleted() { ClearPrevIdArray(); } void nsMsgThreadedDBView::ClearPrevIdArray() { m_prevKeys.Clear(); m_prevLevels.Clear(); m_prevFlags.Clear(); m_havePrevView = false; } nsresult nsMsgThreadedDBView::InitSort(nsMsgViewSortTypeValue sortType, nsMsgViewSortOrderValue sortOrder) { if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) return NS_OK; // nothing to do. if (sortType == nsMsgViewSortType::byThread) { nsMsgDBView::Sort(nsMsgViewSortType::byId, sortOrder); // sort top level threads by id. m_sortType = nsMsgViewSortType::byThread; m_viewFlags |= nsMsgViewFlagsType::kThreadedDisplay; m_viewFlags &= ~nsMsgViewFlagsType::kGroupBySort; SetViewFlags(m_viewFlags); // persist the view flags. // m_db->SetSortInfo(m_sortType, sortOrder); } // else // m_viewFlags &= ~nsMsgViewFlagsType::kThreadedDisplay; // by default, the unread only view should have all threads expanded. if ((m_viewFlags & (nsMsgViewFlagsType::kUnreadOnly|nsMsgViewFlagsType::kExpandAll)) && (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) ExpandAll(); if (! (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) ExpandAll(); // for now, expand all and do a flat sort. Sort(sortType, sortOrder); if (sortType != nsMsgViewSortType::byThread) // forget prev view, since it has everything expanded. ClearPrevIdArray(); return NS_OK; } nsresult nsMsgThreadedDBView::OnNewHeader(nsIMsgDBHdr *newHdr, nsMsgKey aParentKey, bool ensureListed) { if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) return nsMsgGroupView::OnNewHeader(newHdr, aParentKey, ensureListed); NS_ENSURE_TRUE(newHdr, NS_MSG_MESSAGE_NOT_FOUND); nsMsgKey newKey; newHdr->GetMessageKey(&newKey); // views can override this behaviour, which is to append to view. // This is the mail behaviour, but threaded views want // to insert in order... uint32_t msgFlags; newHdr->GetFlags(&msgFlags); if ((m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) && !ensureListed && (msgFlags & nsMsgMessageFlags::Read)) return NS_OK; // Currently, we only add the header in a threaded view if it's a thread. // We used to check if this was the first header in the thread, but that's // a bit harder in the unreadOnly view. But we'll catch it below. // if not threaded display just add it to the view. if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) return AddHdr(newHdr); // need to find the thread we added this to so we can change the hasnew flag // added message to existing thread, but not to view // Fix flags on thread header. int32_t threadCount; uint32_t threadFlags; bool moveThread = false; nsMsgViewIndex threadIndex = ThreadIndexOfMsg(newKey, nsMsgViewIndex_None, &threadCount, &threadFlags); bool threadRootIsDisplayed = false; nsCOMPtr threadHdr; m_db->GetThreadContainingMsgHdr(newHdr, getter_AddRefs(threadHdr)); if (threadHdr && m_sortType == nsMsgViewSortType::byDate) { uint32_t newestMsgInThread = 0, msgDate = 0; threadHdr->GetNewestMsgDate(&newestMsgInThread); newHdr->GetDateInSeconds(&msgDate); moveThread = (msgDate == newestMsgInThread); } if (threadIndex != nsMsgViewIndex_None) { threadRootIsDisplayed = (m_currentlyDisplayedViewIndex == threadIndex); uint32_t flags = m_flags[threadIndex]; if (!(flags & MSG_VIEW_FLAG_HASCHILDREN)) { flags |= MSG_VIEW_FLAG_HASCHILDREN | MSG_VIEW_FLAG_ISTHREAD; if (!(m_viewFlags & nsMsgViewFlagsType::kUnreadOnly)) flags |= nsMsgMessageFlags::Elided; m_flags[threadIndex] = flags; } if (!(flags & nsMsgMessageFlags::Elided)) { // thread is expanded // insert child into thread // levels of other hdrs may have changed! uint32_t newFlags = msgFlags; int32_t level = 0; nsMsgViewIndex insertIndex = threadIndex; if (aParentKey == nsMsgKey_None) { newFlags |= MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN; } else { nsMsgViewIndex parentIndex = FindParentInThread(aParentKey, threadIndex); level = m_levels[parentIndex] + 1; insertIndex = GetInsertInfoForNewHdr(newHdr, parentIndex, level); } InsertMsgHdrAt(insertIndex, newHdr, newKey, newFlags, level); // the call to NoteChange() has to happen after we add the key // as NoteChange() will call RowCountChanged() which will call our GetRowCount() NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete); if (aParentKey == nsMsgKey_None) { // this header is the new king! try collapsing the existing thread, // removing it, installing this header as king, and expanding it. CollapseByIndex(threadIndex, nullptr); // call base class, so child won't get promoted. // nsMsgDBView::RemoveByIndex(threadIndex); ExpandByIndex(threadIndex, nullptr); } } else if (aParentKey == nsMsgKey_None) { // if we have a collapsed thread which just got a new // top of thread, change the keys array. m_keys[threadIndex] = newKey; } // If this message is new, the thread is collapsed, it is the // root and it was displayed, expand it so that the user does // not find that their message has magically turned into a summary. if (msgFlags & nsMsgMessageFlags::New && m_flags[threadIndex] & nsMsgMessageFlags::Elided && threadRootIsDisplayed) ExpandByIndex(threadIndex, nullptr); if (moveThread) MoveThreadAt(threadIndex); else // note change, to update the parent thread's unread and total counts NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed); } else if (threadHdr) // adding msg to thread that's not in view. AddMsgToThreadNotInView(threadHdr, newHdr, ensureListed); return NS_OK; } NS_IMETHODIMP nsMsgThreadedDBView::OnParentChanged (nsMsgKey aKeyChanged, nsMsgKey oldParent, nsMsgKey newParent, nsIDBChangeListener *aInstigator) { // we need to adjust the level of the hdr whose parent changed, and invalidate that row, // iff we're in threaded mode. #if 0 // This code never runs due to the if (false) and Clang complains about it // so it is ifdefed out for now. if (false && m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { nsMsgViewIndex childIndex = FindViewIndex(aKeyChanged); if (childIndex != nsMsgViewIndex_None) { nsMsgViewIndex parentIndex = FindViewIndex(newParent); int32_t newParentLevel = (parentIndex == nsMsgViewIndex_None) ? -1 : m_levels[parentIndex]; nsMsgViewIndex oldParentIndex = FindViewIndex(oldParent); int32_t oldParentLevel = (oldParentIndex != nsMsgViewIndex_None || newParent == nsMsgKey_None) ? m_levels[oldParentIndex] : -1 ; int32_t levelChanged = m_levels[childIndex]; int32_t parentDelta = oldParentLevel - newParentLevel; m_levels[childIndex] = (newParent == nsMsgKey_None) ? 0 : newParentLevel + 1; if (parentDelta > 0) { for (nsMsgViewIndex viewIndex = childIndex + 1; viewIndex < GetSize() && m_levels[viewIndex] > levelChanged; viewIndex++) { m_levels[viewIndex] = m_levels[viewIndex] - parentDelta; NoteChange(viewIndex, 1, nsMsgViewNotificationCode::changed); } } NoteChange(childIndex, 1, nsMsgViewNotificationCode::changed); } } #endif return NS_OK; } nsMsgViewIndex nsMsgThreadedDBView::GetInsertInfoForNewHdr(nsIMsgDBHdr *newHdr, nsMsgViewIndex parentIndex, int32_t targetLevel) { uint32_t viewSize = GetSize(); while (++parentIndex < viewSize) { // loop until we find a message at a level less than or equal to the parent level if (m_levels[parentIndex] < targetLevel) break; } return parentIndex; } // This method removes the thread at threadIndex from the view // and puts it back in its new position, determined by the sort order. // And, if the selection is affected, save and restore the selection. void nsMsgThreadedDBView::MoveThreadAt(nsMsgViewIndex threadIndex) { // we need to check if the thread is collapsed or not... // We want to turn off tree notifications so that we don't // reload the current message. // We also need to invalidate the range between where the thread was // and where it ended up. bool changesDisabled = mSuppressChangeNotification; if (!changesDisabled) SetSuppressChangeNotifications(true); nsCOMPtr threadHdr; GetMsgHdrForViewIndex(threadIndex, getter_AddRefs(threadHdr)); int32_t childCount = 0; nsMsgKey preservedKey; AutoTArray preservedSelection; int32_t selectionCount; int32_t currentIndex; bool hasSelection = mTree && mTreeSelection && ((NS_SUCCEEDED(mTreeSelection->GetCurrentIndex(¤tIndex)) && currentIndex >= 0 && (uint32_t)currentIndex < GetSize()) || (NS_SUCCEEDED(mTreeSelection->GetRangeCount(&selectionCount)) && selectionCount > 0)); if (hasSelection) SaveAndClearSelection(&preservedKey, preservedSelection); uint32_t saveFlags = m_flags[threadIndex]; bool threadIsExpanded = !(saveFlags & nsMsgMessageFlags::Elided); if (threadIsExpanded) { ExpansionDelta(threadIndex, &childCount); childCount = -childCount; } nsTArray threadKeys; nsTArray threadFlags; nsTArray threadLevels; if (threadIsExpanded) { threadKeys.SetCapacity(childCount); threadFlags.SetCapacity(childCount); threadLevels.SetCapacity(childCount); for (nsMsgViewIndex index = threadIndex + 1; index < GetSize() && m_levels[index]; index++) { threadKeys.AppendElement(m_keys[index]); threadFlags.AppendElement(m_flags[index]); threadLevels.AppendElement(m_levels[index]); } uint32_t collapseCount; CollapseByIndex(threadIndex, &collapseCount); } nsMsgDBView::RemoveByIndex(threadIndex); nsMsgViewIndex newIndex = nsMsgViewIndex_None; AddHdr(threadHdr, &newIndex); // AddHdr doesn't always set newIndex, and getting it to do so // is going to require some refactoring. if (newIndex == nsMsgViewIndex_None) newIndex = FindHdr(threadHdr); if (threadIsExpanded) { m_keys.InsertElementsAt(newIndex + 1, threadKeys); m_flags.InsertElementsAt(newIndex + 1, threadFlags); m_levels.InsertElementsAt(newIndex + 1, threadLevels); } if (newIndex == nsMsgViewIndex_None) { NS_WARNING("newIndex=-1 in MoveThreadAt"); newIndex = 0; } m_flags[newIndex] = saveFlags; // unfreeze selection. if (hasSelection) RestoreSelection(preservedKey, preservedSelection); if (!changesDisabled) SetSuppressChangeNotifications(false); nsMsgViewIndex lowIndex = threadIndex < newIndex ? threadIndex : newIndex; nsMsgViewIndex highIndex = lowIndex == threadIndex ? newIndex : threadIndex; NoteChange(lowIndex, highIndex - lowIndex + childCount + 1, nsMsgViewNotificationCode::changed); } nsresult nsMsgThreadedDBView::AddMsgToThreadNotInView(nsIMsgThread *threadHdr, nsIMsgDBHdr *msgHdr, bool ensureListed) { nsresult rv = NS_OK; uint32_t threadFlags; threadHdr->GetFlags(&threadFlags); if (!(threadFlags & nsMsgMessageFlags::Ignored)) { bool msgKilled; msgHdr->GetIsKilled(&msgKilled); if (!msgKilled) rv = nsMsgDBView::AddHdr(msgHdr); } return rv; } // This method just removes the specified line from the view. It does // NOT delete it from the database. nsresult nsMsgThreadedDBView::RemoveByIndex(nsMsgViewIndex index) { nsresult rv = NS_OK; int32_t flags; if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; OnHeaderAddedOrDeleted(); flags = m_flags[index]; if (! (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) return nsMsgDBView::RemoveByIndex(index); nsCOMPtr threadHdr; GetThreadContainingIndex(index, getter_AddRefs(threadHdr)); NS_ENSURE_SUCCESS(rv, rv); uint32_t numThreadChildren = 0; // If we can't get a thread, it's already deleted and thus has 0 children. if (threadHdr) threadHdr->GetNumChildren(&numThreadChildren); // Check if we're the top level msg in the thread, and we're not collapsed. if ((flags & MSG_VIEW_FLAG_ISTHREAD) && !(flags & nsMsgMessageFlags::Elided) && (flags & MSG_VIEW_FLAG_HASCHILDREN)) { // Fix flags on thread header - newly promoted message should have // flags set correctly. if (threadHdr) { nsMsgDBView::RemoveByIndex(index); nsCOMPtr nextThreadHdr; // Above RemoveByIndex may now make index out of bounds. if (IsValidIndex(index) && numThreadChildren > 0) { // unreadOnly nsCOMPtr msgHdr; rv = threadHdr->GetChildHdrAt(0, getter_AddRefs(msgHdr)); if (msgHdr != nullptr) { uint32_t flag = 0; msgHdr->GetFlags(&flag); if (numThreadChildren > 1) flag |= MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN; m_flags[index] = flag; m_levels[index] = 0; } } } return rv; } else if (!(flags & MSG_VIEW_FLAG_ISTHREAD)) { // We're not deleting the top level msg, but top level msg might be the // only msg in thread now. if (threadHdr && numThreadChildren == 1) { nsMsgKey msgKey; rv = threadHdr->GetChildKeyAt(0, &msgKey); if (NS_SUCCEEDED(rv)) { nsMsgViewIndex threadIndex = FindViewIndex(msgKey); if (IsValidIndex(threadIndex)) { uint32_t flags = m_flags[threadIndex]; flags &= ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided | MSG_VIEW_FLAG_HASCHILDREN); m_flags[threadIndex] = flags; NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed); } } } return nsMsgDBView::RemoveByIndex(index); } // Deleting collapsed thread header is special case. Child will be promoted, // so just tell FE that line changed, not that it was deleted. // Header has aleady been deleted from thread. if (threadHdr && numThreadChildren > 0) { // change the id array and flags array to reflect the child header. // If we're not deleting the header, we want the second header, // Otherwise, the first one (which just got promoted). nsCOMPtr msgHdr; rv = threadHdr->GetChildHdrAt(0, getter_AddRefs(msgHdr)); if (msgHdr != nullptr) { msgHdr->GetMessageKey(&m_keys[index]); uint32_t flag = 0; msgHdr->GetFlags(&flag); flag |= MSG_VIEW_FLAG_ISTHREAD; // if only hdr in thread (with one about to be deleted) if (numThreadChildren == 1) { // adjust flags. flag &= ~MSG_VIEW_FLAG_HASCHILDREN; flag &= ~nsMsgMessageFlags::Elided; // tell FE that thread header needs to be repainted. NoteChange(index, 1, nsMsgViewNotificationCode::changed); } else { flag |= MSG_VIEW_FLAG_HASCHILDREN; flag |= nsMsgMessageFlags::Elided; } m_flags[index] = flag; mIndicesToNoteChange.RemoveElement(index); } else NS_ASSERTION(false, "couldn't find thread child"); NoteChange(index, 1, nsMsgViewNotificationCode::changed); } else { // we may have deleted a whole, collapsed thread - if so, // ensure that the current index will be noted as changed. if (!mIndicesToNoteChange.Contains(index)) mIndicesToNoteChange.AppendElement(index); rv = nsMsgDBView::RemoveByIndex(index); } return rv; } NS_IMETHODIMP nsMsgThreadedDBView::GetViewType(nsMsgViewTypeValue *aViewType) { NS_ENSURE_ARG_POINTER(aViewType); *aViewType = nsMsgViewType::eShowAllThreads; return NS_OK; } NS_IMETHODIMP nsMsgThreadedDBView::CloneDBView(nsIMessenger *aMessengerInstance, nsIMsgWindow *aMsgWindow, nsIMsgDBViewCommandUpdater *aCmdUpdater, nsIMsgDBView **_retval) { nsMsgThreadedDBView* newMsgDBView = new nsMsgThreadedDBView(); if (!newMsgDBView) return NS_ERROR_OUT_OF_MEMORY; nsresult rv = CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater); NS_ENSURE_SUCCESS(rv,rv); NS_IF_ADDREF(*_retval = newMsgDBView); return NS_OK; }