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 35 : LisvelLoadNewsOperation::LisvelLoadNewsOperation(OperationManager *parent, LisvelFeedItem *feedItem, LoadMode mode, int loadLimit, bool prependOnInit) :
8 : LoadNewsOperation(parent, feedItem, mode, loadLimit),
9 35 : lisvelNews(feedItem),
10 35 : prependOnInit(prependOnInit)
11 : {
12 35 : }
13 :
14 27 : void LisvelLoadNewsOperation::execute()
15 : {
16 54 : 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 27 : 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 27 : if (getMode() == LoadNewsOperation::Initial) {
29 17 : 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 17 : feedItem->getNewsList()->clear();
33 : }
34 :
35 : // DB query/ies.
36 27 : bool dbResult = true;
37 27 : switch (getMode()) {
38 17 : case Initial:
39 : {
40 17 : dbResult &= doAppend(); // First load everything after bookmark.
41 :
42 17 : if (prependOnInit) {
43 14 : dbResult &= doPrepend(); // Now the stuff on top
44 : }
45 :
46 17 : break;
47 : }
48 :
49 6 : case Append:
50 : {
51 6 : dbResult &= doAppend();
52 :
53 6 : break;
54 : }
55 :
56 4 : case Prepend:
57 : {
58 4 : dbResult &= doPrepend();
59 :
60 4 : 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 27 : 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 27 : qint64 initialBookmarkID = -1;
80 27 : if (getMode() == Initial && !listPrepend.isEmpty()) {
81 6 : 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 28 : for (NewsItem* newsItem: std::as_const(listPrepend)) {
86 22 : listAppend.prepend(newsItem);
87 : }
88 6 : listPrepend.clear();
89 : }
90 :
91 : // Remove any duplicate IDs within the lists. (Note: should not be necessary.)
92 54 : QSet<qint64> seenIds;
93 27 : QList<NewsItem*> duplicatesToDelete;
94 :
95 : // Rmove duplicates from listAppend.
96 111 : for (int i = listAppend.size() - 1; i >= 0; --i) {
97 84 : NewsItem* item = listAppend.at(i);
98 84 : 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 84 : seenIds.insert(item->getDbID());
105 : }
106 : }
107 :
108 : // Remove duplicates from listPrepend.
109 34 : for (int i = listPrepend.size() - 1; i >= 0; --i) {
110 7 : NewsItem* item = listPrepend.at(i);
111 7 : 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 7 : seenIds.insert(item->getDbID());
118 : }
119 : }
120 :
121 : // Delete the duplicate NewsItem objects to prevent memory leaks.
122 27 : 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 54 : qCDebug(logOperation) << "LisvelLoadNewsOperation: Processing" << listAppend.size()
132 27 : << "items to append," << listPrepend.size() << "items to prepend";
133 :
134 27 : int appendedCount = 0;
135 27 : int skippedAppend = 0;
136 27 : QList<NewsItem*> itemsToSendToJS; // Track items that should actually be sent to JS
137 :
138 27 : if (!listAppend.isEmpty()) {
139 104 : 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 84 : if (!feedItem->getNewsList()->containsID(newsItem->getDbID())) {
143 79 : feedItem->getNewsList()->append(newsItem);
144 79 : appendedCount++;
145 79 : 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 5 : qsizetype fullIdx = feedItem->getNewsList()->fullIndexForItemID(newsItem->getDbID());
149 10 : qCDebug(logOperation) << "Skipped append for id=" << newsItem->getDbID()
150 5 : << "already at fullIndex=" << fullIdx
151 5 : << "(paged item will still be sent to JS)";
152 5 : skippedAppend++;
153 : // Paged items should still be sent to JS since they were shrunk from the display
154 5 : itemsToSendToJS.append(newsItem);
155 : }
156 : }
157 : }
158 :
159 : // Replace listAppend with only the items that should be sent to JS.
160 27 : listAppend = itemsToSendToJS;
161 :
162 27 : int prependedCount = 0;
163 27 : int skippedPrepend = 0;
164 27 : if (!listPrepend.isEmpty()) {
165 10 : for (NewsItem* newsItem: std::as_const(listPrepend)) {
166 : // Only add items not already in NewsList.
167 7 : if (!feedItem->getNewsList()->containsID(newsItem->getDbID())) {
168 2 : feedItem->getNewsList()->prepend(newsItem);
169 2 : prependedCount++;
170 : } else {
171 5 : skippedPrepend++;
172 : }
173 : }
174 : }
175 :
176 : // Log final state
177 54 : qCDebug(logOperation) << "LisvelLoadNewsOperation: Added" << appendedCount << "appended,"
178 27 : << prependedCount << "prepended. Skipped" << skippedAppend + skippedPrepend
179 27 : << "already in list. NewsList now has"
180 27 : << feedItem->getNewsList()->size() << "items in display window,"
181 27 : << 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 27 : if (initialBookmarkID >= 0) {
186 6 : feedItem->setBookmark(initialBookmarkID);
187 : }
188 :
189 : // Set the first possible ID for that top bookmark display action.
190 27 : lisvelNews->setFirstNewsID(getFirstNewsID());
191 : }
192 :
193 18 : bool LisvelLoadNewsOperation::doPrepend()
194 : {
195 : // Remaining items to load.
196 18 : int remainingLoadLimit = getLoadLimit();
197 18 : NewsList* newsList = lisvelNews->getNewsList();
198 :
199 : // Log current list state for debugging
200 18 : if (!newsList->isEmpty()) {
201 4 : NewsItem* first = newsList->first();
202 8 : qCDebug(logOperation) << "doPrepend: First item in display window: id=" << first->getDbID()
203 8 : << "timestamp=" << first->getTimestamp().toString(Qt::ISODate)
204 4 : << "displayStart=" << newsList->getDisplayStart()
205 4 : << "fullSize=" << newsList->fullSize();
206 : }
207 :
208 : //
209 : // STEP ONE: Page up through items already in memory (before the display window).
210 : //
211 18 : if (newsList->canPageUp()) {
212 : // Record current start before paging so we can add the newly-visible items to listPrepend.
213 2 : qsizetype oldDisplayStart = newsList->getDisplayStart();
214 :
215 2 : qsizetype paged = newsList->pageUp(remainingLoadLimit);
216 4 : qCDebug(logOperation) << "doPrepend step 1: Paged up" << paged << "items from memory";
217 2 : 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 7 : for (qsizetype i = oldDisplayStart - 1; i >= newsList->getDisplayStart(); --i) {
222 5 : NewsItem* item = newsList->fullAt(i);
223 5 : if (item) {
224 5 : 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 18 : QString prependQuery = prependNewQueryString();
235 18 : if (remainingLoadLimit > 0 && !prependQuery.isEmpty()) {
236 16 : QSqlQuery query(db());
237 16 : query.prepare(prependQuery);
238 16 : query.bindValue(":load_limit", remainingLoadLimit);
239 16 : bindQueryParameters(query);
240 :
241 16 : 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 16 : int beforeCount = listPrepend.size();
250 16 : queryToNewsList(query, &listPrepend);
251 :
252 : // Log the prepended items to debug ordering issues
253 32 : qCDebug(logOperation) << "Prepend loaded" << (listPrepend.size() - beforeCount) << "items from DB";
254 40 : for (int i = beforeCount; i < listPrepend.size(); i++) {
255 24 : NewsItem* item = listPrepend.at(i);
256 48 : qCDebug(logOperation) << " [" << i << "] id=" << item->getDbID()
257 48 : << "timestamp=" << item->getTimestamp().toString(Qt::ISODate)
258 24 : << "feed=" << item->getFeedId();
259 : }
260 16 : }
261 :
262 18 : return true;
263 18 : }
264 :
265 23 : bool LisvelLoadNewsOperation::doAppend()
266 : {
267 : // Remaining items to load.
268 23 : int remainingLoadLimit = getLoadLimit();
269 23 : NewsList* newsList = lisvelNews->getNewsList();
270 :
271 : // Log state before doAppend for debugging duplicate issues
272 46 : qCDebug(logOperation) << "doAppend: Starting with displayWindow ["
273 23 : << newsList->getDisplayStart() << "," << newsList->getDisplayEnd() << ")"
274 23 : << "fullSize=" << newsList->fullSize()
275 23 : << "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 23 : if (newsList->canPageDown()) {
284 : // Record current end before paging so we can add the newly-visible items to listAppend.
285 2 : qsizetype oldDisplayEnd = newsList->getDisplayEnd();
286 2 : 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 4 : qCWarning(logOperation) << "doAppend step 1: canPageDown=true while in Append mode!"
291 2 : << "displayEnd=" << oldDisplayEnd << "fullSize=" << fullSize
292 2 : << "- suggests previous removeNewsBottom occurred";
293 :
294 2 : qsizetype paged = newsList->pageDown(remainingLoadLimit);
295 4 : qCDebug(logOperation) << "doAppend step 1: Paged down" << paged << "items from memory"
296 2 : << "oldDisplayEnd=" << oldDisplayEnd
297 2 : << "newDisplayEnd=" << newsList->getDisplayEnd();
298 2 : 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 7 : for (qsizetype i = oldDisplayEnd; i < newsList->getDisplayEnd(); ++i) {
304 5 : NewsItem* item = newsList->fullAt(i);
305 5 : if (item) {
306 10 : qCDebug(logOperation) << "doAppend step 1: Re-sending paged item id=" << item->getDbID()
307 5 : << "at fullIndex=" << i
308 5 : << "timestamp=" << item->getTimestamp().toString(Qt::ISODate);
309 5 : 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 23 : NewsItem* lastItem = newsList->isEmpty() ? nullptr : newsList->last();
318 :
319 : //
320 : // STEP TWO: Load new (unread) items from DB.
321 : //
322 23 : if (remainingLoadLimit > 0) {
323 22 : QSqlQuery query(db());
324 22 : query.prepare(appendNewQueryString());
325 22 : query.bindValue(":load_limit", remainingLoadLimit);
326 22 : bindQueryParameters(query);
327 :
328 22 : 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 22 : int beforeCount = listAppend.size();
337 22 : queryToNewsList(query, &listAppend);
338 22 : remainingLoadLimit -= (listAppend.size() - beforeCount);
339 :
340 44 : qCDebug(logOperation) << "doAppend step 2: Loaded" << (listAppend.size() - beforeCount) << "unread items from DB";
341 22 : }
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 23 : if (remainingLoadLimit > 0 && lastItem != nullptr) {
349 5 : QString fallbackQuery = appendAfterPositionQueryString();
350 5 : if (!fallbackQuery.isEmpty()) {
351 4 : QSqlQuery query(db());
352 4 : query.prepare(fallbackQuery);
353 4 : query.bindValue(":load_limit", remainingLoadLimit);
354 4 : query.bindValue(":last_timestamp", lastItem->getTimestamp());
355 4 : query.bindValue(":last_id", lastItem->getDbID());
356 4 : bindQueryParameters(query);
357 :
358 4 : if (query.exec()) {
359 4 : int beforeCount = listAppend.size();
360 4 : queryToNewsList(query, &listAppend);
361 8 : qCDebug(logOperation) << "doAppend step 3 (fallback): Loaded" << (listAppend.size() - beforeCount)
362 4 : << "items after position" << lastItem->getTimestamp().toString(Qt::ISODate);
363 : } else {
364 0 : qCDebug(logOperation) << "doAppend fallback query failed:" << query.lastError();
365 : }
366 4 : }
367 5 : }
368 :
369 23 : return true;
370 : }
371 :
372 34 : void LisvelLoadNewsOperation::bindQueryParameters(QSqlQuery& query)
373 : {
374 : Q_UNUSED(query);
375 34 : }
376 :
377 1 : QString LisvelLoadNewsOperation::appendAfterPositionQueryString()
378 : {
379 : // Default: no fallback query. Subclasses can override.
380 1 : return QString();
381 : }
382 :
383 50 : QString LisvelLoadNewsOperation::getLoadedIDString()
384 : {
385 : // Get all IDs from the full NewsList to prevent loading duplicates.
386 : // positionAt() works even for unloaded items.
387 50 : NewsList* newsList = lisvelNews->getNewsList();
388 :
389 50 : QSet<qint64> ids;
390 :
391 : // Add IDs from the NewsList.
392 50 : qsizetype newsListCount = 0;
393 113 : for (qsizetype i = 0; i < newsList->fullSize(); ++i) {
394 63 : ids.insert(newsList->positionAt(i).id());
395 63 : 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 50 : qsizetype listAppendCount = 0;
401 103 : for (NewsItem* item : std::as_const(listAppend)) {
402 53 : if (!ids.contains(item->getDbID())) {
403 47 : listAppendCount++; // Count only IDs not already in NewsList
404 : }
405 53 : ids.insert(item->getDbID());
406 : }
407 50 : qsizetype listPrependCount = 0;
408 55 : for (NewsItem* item : std::as_const(listPrepend)) {
409 5 : if (!ids.contains(item->getDbID())) {
410 0 : listPrependCount++; // Count only IDs not already counted
411 : }
412 5 : ids.insert(item->getDbID());
413 : }
414 :
415 100 : qCDebug(logOperation) << "getLoadedIDString: Excluding" << ids.size() << "IDs"
416 50 : << "(NewsList:" << newsListCount
417 50 : << "listAppend:" << listAppendCount
418 50 : << "listPrepend:" << listPrependCount << ")";
419 :
420 50 : if (ids.isEmpty()) {
421 25 : return "-1"; // Return invalid ID to avoid SQL syntax error with empty IN clause
422 : }
423 :
424 25 : QString ret = "";
425 25 : bool first = true;
426 :
427 135 : for (qint64 id : ids) {
428 110 : if (!first) {
429 85 : ret += ", ";
430 : }
431 110 : ret += QString::number(id);
432 110 : first = false;
433 : }
434 :
435 25 : return ret;
436 50 : }
|