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 : }
|