LCOV - code coverage report
Current view: top level - src/operations - LisvelLoadNewsOperation.cpp (source / functions) Coverage Total Hit
Test: coverage.info.cleaned Lines: 8.8 % 228 20
Test Date: 2026-03-23 10:19:47 Functions: 28.6 % 7 2

            Line data    Source code
       1              : #include "LisvelLoadNewsOperation.h"
       2              : #include "src/models/NewsList.h"
       3              : #include "../utilities/ErrorHandling.h"
       4              : #include "../utilities/FangLogging.h"
       5              : 
       6              : 
       7            8 : LisvelLoadNewsOperation::LisvelLoadNewsOperation(OperationManager *parent, LisvelFeedItem *feedItem, LoadMode mode, int loadLimit, bool prependOnInit) :
       8              :     LoadNewsOperation(parent, feedItem, mode, loadLimit),
       9            8 :     lisvelNews(feedItem),
      10            8 :     prependOnInit(prependOnInit)
      11              : {
      12            8 : }
      13              : 
      14            0 : void LisvelLoadNewsOperation::execute()
      15              : {
      16            0 :     qCDebug(logOperation) << "LisvelLoadNewsOperation::execute load for feed: " << lisvelNews->getDbID();
      17              : 
      18              :     // Ensure lists are empty just in case the operation object gets reused somehow.
      19            0 :     if (!listAppend.isEmpty() || !listPrepend.isEmpty()) {
      20            0 :         qCWarning(logOperation) << "LisvelLoadNewsOperation: Lists not empty at start!"
      21            0 :                                 << "listAppend:" << listAppend.size()
      22            0 :                                 << "listPrepend:" << listPrepend.size();
      23            0 :         listAppend.clear();
      24            0 :         listPrepend.clear();
      25              :     }
      26              : 
      27              :     // For an initial load, make sure the feed isn't populated yet.
      28            0 :     if (getMode() == LoadNewsOperation::Initial) {
      29            0 :         FANG_CHECK(feedItem->getNewsList() != nullptr || feedItem->getNewsList()->isEmpty(),
      30              :                    "LisvelLoadNewsOperation: Initial load on populated feed");
      31              :         // Clear the list for a fresh start if re-using an existing LisvelFeeditem.
      32            0 :         feedItem->getNewsList()->clear();
      33              :     }
      34              : 
      35              :     // DB query/ies.
      36            0 :     bool dbResult = true;
      37            0 :     switch (getMode()) {
      38            0 :     case Initial:
      39              :     {
      40            0 :         dbResult &= doAppend();  // First load everything after bookmark.
      41              : 
      42            0 :         if (prependOnInit) {
      43            0 :             dbResult &= doPrepend(); // Now the stuff on top
      44              :         }
      45              : 
      46            0 :         break;
      47              :     }
      48              : 
      49            0 :     case Append:
      50              :     {
      51            0 :         dbResult &= doAppend();
      52              : 
      53            0 :         break;
      54              :     }
      55              : 
      56            0 :     case Prepend:
      57              :     {
      58            0 :         dbResult &= doPrepend();
      59              : 
      60            0 :         break;
      61              :     }
      62              : 
      63            0 :     default:
      64            0 :         FANG_UNREACHABLE("Invalid LoadMode in LisvelLoadNewsOperation");
      65              :         break;
      66              :     }
      67              : 
      68              :     // Check if we done goofed.
      69            0 :     if (!dbResult) {
      70            0 :         reportError("DB error in LisvelLoadNewsOperation");
      71              : 
      72            0 :         return;
      73              :     }
      74              : 
      75              : 
      76              :     // For initial load, save the bookmark ID before modifying the lists.
      77              :     // We'll set the bookmark AFTER items are added to NewsList so the
      78              :     // NewsPosition can be properly captured.
      79            0 :     qint64 initialBookmarkID = -1;
      80            0 :     if (getMode() == Initial && !listPrepend.isEmpty()) {
      81            0 :         initialBookmarkID = listPrepend.first()->getDbID();
      82              : 
      83              :         // As an optimization, we only want to present *one* list -- an append list.
      84              :         // So we rewind our prepend list on top of it, then delete the prepend list.
      85            0 :         for (NewsItem* newsItem: std::as_const(listPrepend)) {
      86            0 :             listAppend.prepend(newsItem);
      87              :         }
      88            0 :         listPrepend.clear();
      89              :     }
      90              : 
      91              :     // Remove any duplicate IDs within the lists. (Note: should not be necessary.)
      92            0 :     QSet<qint64> seenIds;
      93            0 :     QList<NewsItem*> duplicatesToDelete;
      94              : 
      95              :     // Rmove duplicates from listAppend.
      96            0 :     for (int i = listAppend.size() - 1; i >= 0; --i) {
      97            0 :         NewsItem* item = listAppend.at(i);
      98            0 :         if (seenIds.contains(item->getDbID())) {
      99            0 :             qCCritical(logOperation) << "LisvelLoadNewsOperation: DUPLICATE ID" << item->getDbID()
     100            0 :                                      << "found in listAppend at index" << i << "- removing duplicate.";
     101            0 :             duplicatesToDelete.append(item);
     102            0 :             listAppend.removeAt(i);
     103              :         } else {
     104            0 :             seenIds.insert(item->getDbID());
     105              :         }
     106              :     }
     107              : 
     108              :     // Remove duplicates from listPrepend.
     109            0 :     for (int i = listPrepend.size() - 1; i >= 0; --i) {
     110            0 :         NewsItem* item = listPrepend.at(i);
     111            0 :         if (seenIds.contains(item->getDbID())) {
     112            0 :             qCCritical(logOperation) << "LisvelLoadNewsOperation: DUPLICATE ID" << item->getDbID()
     113            0 :                                      << "found in listPrepend at index" << i << "- removing duplicate.";
     114            0 :             duplicatesToDelete.append(item);
     115            0 :             listPrepend.removeAt(i);
     116              :         } else {
     117            0 :             seenIds.insert(item->getDbID());
     118              :         }
     119              :     }
     120              : 
     121              :     // Delete the duplicate NewsItem objects to prevent memory leaks.
     122            0 :     for (NewsItem* dup : duplicatesToDelete) {
     123            0 :         dup->deleteLater();
     124              :     }
     125              : 
     126              :     // Append/prepend items from our lists.
     127              :     // Note: listAppend/listPrepend may contain items that are already in NewsList
     128              :     // (paged from memory in step 1). We only call append/prepend for items NOT
     129              :     // already in the list. Paged items are already in NewsList (just outside the
     130              :     // display window) - pageUp/pageDown already expanded the window to include them.
     131            0 :     qCDebug(logOperation) << "LisvelLoadNewsOperation: Processing" << listAppend.size()
     132            0 :                           << "items to append," << listPrepend.size() << "items to prepend";
     133              : 
     134            0 :     int appendedCount = 0;
     135            0 :     int skippedAppend = 0;
     136            0 :     QList<NewsItem*> itemsToSendToJS;  // Track items that should actually be sent to JS
     137              : 
     138            0 :     if (!listAppend.isEmpty()) {
     139            0 :         for (NewsItem* newsItem: std::as_const(listAppend)) {
     140              :             // Only add items not already in NewsList (i.e., items from DB queries).
     141              :             // Items paged from memory are already in NewsList.
     142            0 :             if (!feedItem->getNewsList()->containsID(newsItem->getDbID())) {
     143            0 :                 feedItem->getNewsList()->append(newsItem);
     144            0 :                 appendedCount++;
     145            0 :                 itemsToSendToJS.append(newsItem);  // New items should be sent to JS
     146              :             } else {
     147              :                 // Item already in NewsList. Check to make sure we didn't load it twice..
     148            0 :                 qsizetype fullIdx = feedItem->getNewsList()->fullIndexForItemID(newsItem->getDbID());
     149            0 :                 qCDebug(logOperation) << "Skipped append for id=" << newsItem->getDbID()
     150            0 :                                       << "already at fullIndex=" << fullIdx
     151            0 :                                       << "(paged item will still be sent to JS)";
     152            0 :                 skippedAppend++;
     153              :                 // Paged items should still be sent to JS since they were shrunk from the display
     154            0 :                 itemsToSendToJS.append(newsItem);
     155              :             }
     156              :         }
     157              :     }
     158              : 
     159              :     // Replace listAppend with only the items that should be sent to JS.
     160            0 :     listAppend = itemsToSendToJS;
     161              : 
     162            0 :     int prependedCount = 0;
     163            0 :     int skippedPrepend = 0;
     164            0 :     if (!listPrepend.isEmpty()) {
     165            0 :         for (NewsItem* newsItem: std::as_const(listPrepend)) {
     166              :             // Only add items not already in NewsList.
     167            0 :             if (!feedItem->getNewsList()->containsID(newsItem->getDbID())) {
     168            0 :                 feedItem->getNewsList()->prepend(newsItem);
     169            0 :                 prependedCount++;
     170              :             } else {
     171            0 :                 skippedPrepend++;
     172              :             }
     173              :         }
     174              :     }
     175              : 
     176              :     // Log final state
     177            0 :     qCDebug(logOperation) << "LisvelLoadNewsOperation: Added" << appendedCount << "appended,"
     178            0 :                           << prependedCount << "prepended. Skipped" << skippedAppend + skippedPrepend
     179            0 :                           << "already in list. NewsList now has"
     180            0 :                           << feedItem->getNewsList()->size() << "items in display window,"
     181            0 :                           << feedItem->getNewsList()->fullSize() << "total";
     182              : 
     183              :     // Set the bookmark, as by now the bookmarked item should be in the NewsList
     184              :     // with its correct position.
     185            0 :     if (initialBookmarkID >= 0) {
     186            0 :         feedItem->setBookmark(initialBookmarkID);
     187              :     }
     188              : 
     189              :     // Set the first possible ID for that top bookmark display action.
     190            0 :     lisvelNews->setFirstNewsID(getFirstNewsID());
     191              : }
     192              : 
     193            0 : bool LisvelLoadNewsOperation::doPrepend()
     194              : {
     195              :     // Remaining items to load.
     196            0 :     int remainingLoadLimit = getLoadLimit();
     197            0 :     NewsList* newsList = lisvelNews->getNewsList();
     198              : 
     199              :     // Log current list state for debugging
     200            0 :     if (!newsList->isEmpty()) {
     201            0 :         NewsItem* first = newsList->first();
     202            0 :         qCDebug(logOperation) << "doPrepend: First item in display window: id=" << first->getDbID()
     203            0 :                               << "timestamp=" << first->getTimestamp().toString(Qt::ISODate)
     204            0 :                               << "displayStart=" << newsList->getDisplayStart()
     205            0 :                               << "fullSize=" << newsList->fullSize();
     206              :     }
     207              : 
     208              :     //
     209              :     // STEP ONE: Page up through items already in memory (before the display window).
     210              :     //
     211            0 :     if (newsList->canPageUp()) {
     212              :         // Record current start before paging so we can add the newly-visible items to listPrepend.
     213            0 :         qsizetype oldDisplayStart = newsList->getDisplayStart();
     214              : 
     215            0 :         qsizetype paged = newsList->pageUp(remainingLoadLimit);
     216            0 :         qCDebug(logOperation) << "doPrepend step 1: Paged up" << paged << "items from memory";
     217            0 :         remainingLoadLimit -= paged;
     218              : 
     219              :         // Add the newly-visible items to listPrepend so they get sent to JS.
     220              :         // Items need to be in DESC order (newest first among the paged items) to match DB query order.
     221            0 :         for (qsizetype i = oldDisplayStart - 1; i >= newsList->getDisplayStart(); --i) {
     222            0 :             NewsItem* item = newsList->fullAt(i);
     223            0 :             if (item) {
     224            0 :                 listPrepend.append(item);
     225              :             } else {
     226            0 :                 qCWarning(logOperation) << "doPrepend: Unexpected null item at index" << i;
     227              :             }
     228              :         }
     229              :     }
     230              : 
     231              :     //
     232              :     // STEP TWO: Load new items from DB if we still need more.
     233              :     //
     234            0 :     QString prependQuery = prependNewQueryString();
     235            0 :     if (remainingLoadLimit > 0 && !prependQuery.isEmpty()) {
     236            0 :         QSqlQuery query(db());
     237            0 :         query.prepare(prependQuery);
     238            0 :         query.bindValue(":load_limit", remainingLoadLimit);
     239            0 :         bindQueryParameters(query);
     240              : 
     241            0 :         if (!query.exec()) {
     242            0 :             qCDebug(logOperation) << "Could not load news! (lisvel prepend step 2)";
     243            0 :             qCDebug(logOperation) << query.lastError();
     244              : 
     245            0 :             return false;
     246              :         }
     247              : 
     248              :         // Extract the query into our news list.
     249            0 :         int beforeCount = listPrepend.size();
     250            0 :         queryToNewsList(query, &listPrepend);
     251              : 
     252              :         // Log the prepended items to debug ordering issues
     253            0 :         qCDebug(logOperation) << "Prepend loaded" << (listPrepend.size() - beforeCount) << "items from DB";
     254            0 :         for (int i = beforeCount; i < listPrepend.size(); i++) {
     255            0 :             NewsItem* item = listPrepend.at(i);
     256            0 :             qCDebug(logOperation) << "  [" << i << "] id=" << item->getDbID()
     257            0 :                                   << "timestamp=" << item->getTimestamp().toString(Qt::ISODate)
     258            0 :                                   << "feed=" << item->getFeedId();
     259              :         }
     260            0 :     }
     261              : 
     262            0 :     return true;
     263            0 : }
     264              : 
     265            0 : bool LisvelLoadNewsOperation::doAppend()
     266              : {
     267              :     // Remaining items to load.
     268            0 :     int remainingLoadLimit = getLoadLimit();
     269            0 :     NewsList* newsList = lisvelNews->getNewsList();
     270              : 
     271              :     // Log state before doAppend for debugging duplicate issues
     272            0 :     qCDebug(logOperation) << "doAppend: Starting with displayWindow ["
     273            0 :                           << newsList->getDisplayStart() << "," << newsList->getDisplayEnd() << ")"
     274            0 :                           << "fullSize=" << newsList->fullSize()
     275            0 :                           << "canPageDown=" << newsList->canPageDown();
     276              : 
     277              :     //
     278              :     // STEP ONE: Page down through items already in memory (after the display window).
     279              :     // This handles items that were previously loaded, then removed when the window was
     280              :     // adjusted, and now need to be loaded again.
     281              :     //
     282              :     // Should only trigger if items were shrunk from the bottom/end of the list.
     283            0 :     if (newsList->canPageDown()) {
     284              :         // Record current end before paging so we can add the newly-visible items to listAppend.
     285            0 :         qsizetype oldDisplayEnd = newsList->getDisplayEnd();
     286            0 :         qsizetype fullSize = newsList->fullSize();
     287              : 
     288              :         // Warning: This suggests items were shrunk from the bottom at some point.
     289              :         // This typically happens during prepend mode (scrolling up), not append mode.
     290            0 :         qCWarning(logOperation) << "doAppend step 1: canPageDown=true while in Append mode!"
     291            0 :                                 << "displayEnd=" << oldDisplayEnd << "fullSize=" << fullSize
     292            0 :                                 << "- suggests previous removeNewsBottom occurred";
     293              : 
     294            0 :         qsizetype paged = newsList->pageDown(remainingLoadLimit);
     295            0 :         qCDebug(logOperation) << "doAppend step 1: Paged down" << paged << "items from memory"
     296            0 :                               << "oldDisplayEnd=" << oldDisplayEnd
     297            0 :                               << "newDisplayEnd=" << newsList->getDisplayEnd();
     298            0 :         remainingLoadLimit -= paged;
     299              : 
     300              :         // Add the newly-visible items to listAppend so they get sent to JS.
     301              :         // NOTE: These items are ALREADY in NewsList but were outside the display window.
     302              :         // They were previously sent to JS and then shrunk (via removeAndDelete from the END).
     303            0 :         for (qsizetype i = oldDisplayEnd; i < newsList->getDisplayEnd(); ++i) {
     304            0 :             NewsItem* item = newsList->fullAt(i);
     305            0 :             if (item) {
     306            0 :                 qCDebug(logOperation) << "doAppend step 1: Re-sending paged item id=" << item->getDbID()
     307            0 :                                       << "at fullIndex=" << i
     308            0 :                                       << "timestamp=" << item->getTimestamp().toString(Qt::ISODate);
     309            0 :                 listAppend.append(item);
     310              :             } else {
     311            0 :                 qCWarning(logOperation) << "doAppend: Unexpected null item at index" << i;
     312              :             }
     313              :         }
     314              :     }
     315              : 
     316              :     // Get the last item for position-based queries
     317            0 :     NewsItem* lastItem = newsList->isEmpty() ? nullptr : newsList->last();
     318              : 
     319              :     //
     320              :     // STEP TWO: Load new (unread) items from DB.
     321              :     //
     322            0 :     if (remainingLoadLimit > 0) {
     323            0 :         QSqlQuery query(db());
     324            0 :         query.prepare(appendNewQueryString());
     325            0 :         query.bindValue(":load_limit", remainingLoadLimit);
     326            0 :         bindQueryParameters(query);
     327              : 
     328            0 :         if (!query.exec()) {
     329            0 :             qCDebug(logOperation) << "Could not load news! (lisvel append step 2)";
     330            0 :             qCDebug(logOperation) << query.lastError();
     331              : 
     332            0 :             return false;
     333              :         }
     334              : 
     335              :         // Extract the query into our news list.
     336            0 :         int beforeCount = listAppend.size();
     337            0 :         queryToNewsList(query, &listAppend);
     338            0 :         remainingLoadLimit -= (listAppend.size() - beforeCount);
     339              : 
     340            0 :         qCDebug(logOperation) << "doAppend step 2: Loaded" << (listAppend.size() - beforeCount) << "unread items from DB";
     341            0 :     }
     342              : 
     343              :     //
     344              :     // STEP THREE: If we still need items and have a position reference,
     345              :     // try loading items that come chronologically after the last item.
     346              :     // This handles loading read items that we haven't seen yet.
     347              :     //
     348            0 :     if (remainingLoadLimit > 0 && lastItem != nullptr) {
     349            0 :         QString fallbackQuery = appendAfterPositionQueryString();
     350            0 :         if (!fallbackQuery.isEmpty()) {
     351            0 :             QSqlQuery query(db());
     352            0 :             query.prepare(fallbackQuery);
     353            0 :             query.bindValue(":load_limit", remainingLoadLimit);
     354            0 :             query.bindValue(":last_timestamp", lastItem->getTimestamp());
     355            0 :             query.bindValue(":last_id", lastItem->getDbID());
     356            0 :             bindQueryParameters(query);
     357              : 
     358            0 :             if (query.exec()) {
     359            0 :                 int beforeCount = listAppend.size();
     360            0 :                 queryToNewsList(query, &listAppend);
     361            0 :                 qCDebug(logOperation) << "doAppend step 3 (fallback): Loaded" << (listAppend.size() - beforeCount)
     362            0 :                                       << "items after position" << lastItem->getTimestamp().toString(Qt::ISODate);
     363              :             } else {
     364            0 :                 qCDebug(logOperation) << "doAppend fallback query failed:" << query.lastError();
     365              :             }
     366            0 :         }
     367            0 :     }
     368              : 
     369            0 :     return true;
     370              : }
     371              : 
     372            0 : void LisvelLoadNewsOperation::bindQueryParameters(QSqlQuery& query)
     373              : {
     374              :     Q_UNUSED(query);
     375            0 : }
     376              : 
     377            0 : QString LisvelLoadNewsOperation::appendAfterPositionQueryString()
     378              : {
     379              :     // Default: no fallback query. Subclasses can override.
     380            0 :     return QString();
     381              : }
     382              : 
     383            7 : QString LisvelLoadNewsOperation::getLoadedIDString()
     384              : {
     385              :     // Get all IDs from the full NewsList to prevent loading duplicates.
     386              :     // positionAt() works even for unloaded items.
     387            7 :     NewsList* newsList = lisvelNews->getNewsList();
     388              : 
     389            7 :     QSet<qint64> ids;
     390              : 
     391              :     // Add IDs from the NewsList.
     392            7 :     qsizetype newsListCount = 0;
     393            7 :     for (qsizetype i = 0; i < newsList->fullSize(); ++i) {
     394            0 :         ids.insert(newsList->positionAt(i).id());
     395            0 :         newsListCount++;
     396              :     }
     397              : 
     398              :     // Also add IDs from items already loaded in this operation (listAppend/listPrepend).
     399              :     // This prevents the same item from being loaded by multiple steps within doAppend/doPrepend.
     400            7 :     qsizetype listAppendCount = 0;
     401            7 :     for (NewsItem* item : std::as_const(listAppend)) {
     402            0 :         if (!ids.contains(item->getDbID())) {
     403            0 :             listAppendCount++;  // Count only IDs not already in NewsList
     404              :         }
     405            0 :         ids.insert(item->getDbID());
     406              :     }
     407            7 :     qsizetype listPrependCount = 0;
     408            7 :     for (NewsItem* item : std::as_const(listPrepend)) {
     409            0 :         if (!ids.contains(item->getDbID())) {
     410            0 :             listPrependCount++;  // Count only IDs not already counted
     411              :         }
     412            0 :         ids.insert(item->getDbID());
     413              :     }
     414              : 
     415           14 :     qCDebug(logOperation) << "getLoadedIDString: Excluding" << ids.size() << "IDs"
     416            7 :                           << "(NewsList:" << newsListCount
     417            7 :                           << "listAppend:" << listAppendCount
     418            7 :                           << "listPrepend:" << listPrependCount << ")";
     419              : 
     420            7 :     if (ids.isEmpty()) {
     421            7 :         return "-1";  // Return invalid ID to avoid SQL syntax error with empty IN clause
     422              :     }
     423              : 
     424            0 :     QString ret = "";
     425            0 :     bool first = true;
     426              : 
     427            0 :     for (qint64 id : ids) {
     428            0 :         if (!first) {
     429            0 :             ret += ", ";
     430              :         }
     431            0 :         ret += QString::number(id);
     432            0 :         first = false;
     433              :     }
     434              : 
     435            0 :     return ret;
     436            7 : }
        

Generated by: LCOV version 2.0-1