diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-09-25 08:59:49 +0300 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-09-25 08:59:49 +0300 |
commit | 69ef98b895be4be6d3471f22e5e153878495523a (patch) | |
tree | e86774dab9a818d503be0fe5ebe6f23f69812b56 | |
parent | de976d7d8bcfcb7bcc1436e215a8713aefbf091f (diff) |
Feeds: Fixed tracking of heading entries; unread count
Heading entries were being discarded from the list of known entries prematurely, causing them to be rediscovered as new later on.
The unread count ignores heading entries without a valid discovery time, i.e., the ones from the first update.
-rw-r--r-- | src/feeds.c | 109 | ||||
-rw-r--r-- | src/feeds.h | 1 |
2 files changed, 87 insertions, 23 deletions
diff --git a/src/feeds.c b/src/feeds.c index 91ef8c2c..9770ca0a 100644 --- a/src/feeds.c +++ b/src/feeds.c | |||
@@ -50,6 +50,7 @@ void init_FeedEntry(iFeedEntry *d) { | |||
50 | init_String(&d->url); | 50 | init_String(&d->url); |
51 | init_String(&d->title); | 51 | init_String(&d->title); |
52 | d->bookmarkId = 0; | 52 | d->bookmarkId = 0; |
53 | d->isHeading = iFalse; | ||
53 | } | 54 | } |
54 | 55 | ||
55 | void deinit_FeedEntry(iFeedEntry *d) { | 56 | void deinit_FeedEntry(iFeedEntry *d) { |
@@ -234,6 +235,7 @@ static void parseResult_FeedJob_(iFeedJob *d) { | |||
234 | } | 235 | } |
235 | trimStart_Rangecc(&line); | 236 | trimStart_Rangecc(&line); |
236 | iFeedEntry *entry = new_FeedEntry(); | 237 | iFeedEntry *entry = new_FeedEntry(); |
238 | entry->isHeading = iTrue; | ||
237 | entry->posted = now; | 239 | entry->posted = now; |
238 | if (!d->isFirstUpdate) { | 240 | if (!d->isFirstUpdate) { |
239 | entry->discovered = now; | 241 | entry->discovered = now; |
@@ -275,7 +277,8 @@ static void save_Feeds_(iFeeds *d) { | |||
275 | initCurrent_Time(&now); | 277 | initCurrent_Time(&now); |
276 | iConstForEach(Array, i, &d->entries.values) { | 278 | iConstForEach(Array, i, &d->entries.values) { |
277 | const iFeedEntry *entry = *(const iFeedEntry **) i.value; | 279 | const iFeedEntry *entry = *(const iFeedEntry **) i.value; |
278 | if (isValid_Time(&entry->discovered) && | 280 | /* Heading entries are kept as long as they are present in the source. */ |
281 | if (!entry->isHeading && isValid_Time(&entry->discovered) && | ||
279 | secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) { | 282 | secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) { |
280 | continue; /* Forget entries discovered long ago. */ | 283 | continue; /* Forget entries discovered long ago. */ |
281 | } | 284 | } |
@@ -298,25 +301,78 @@ static iBool isHeadingEntry_FeedEntry_(const iFeedEntry *d) { | |||
298 | return contains_String(&d->url, '#'); | 301 | return contains_String(&d->url, '#'); |
299 | } | 302 | } |
300 | 303 | ||
301 | static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) { | 304 | static iStringSet *listHeadingEntriesFrom_Feeds_(const iFeeds *d, uint32_t sourceId) { |
305 | iStringSet *set = new_StringSet(); | ||
306 | iConstForEach(Array, i, &d->entries.values) { | ||
307 | const iFeedEntry *entry = *(const iFeedEntry **) i.value; | ||
308 | if (entry->bookmarkId == sourceId) { | ||
309 | insert_StringSet(set, &entry->url); | ||
310 | } | ||
311 | } | ||
312 | return set; | ||
313 | } | ||
314 | |||
315 | static iBool updateEntries_Feeds_(iFeeds *d, iBool isHeadings, uint32_t sourceId, | ||
316 | iPtrArray *incoming) { | ||
317 | /* Entries are removed from `incoming` if they are added to the Feeds entries array. | ||
318 | Anything remaining in `incoming` will be deleted afterwards. */ | ||
302 | iBool gotNew = iFalse; | 319 | iBool gotNew = iFalse; |
303 | lock_Mutex(d->mtx); | 320 | lock_Mutex(d->mtx); |
304 | iTime now; | 321 | iTime now; |
305 | initCurrent_Time(&now); | 322 | initCurrent_Time(&now); |
306 | iForEach(PtrArray, i, incoming) { | 323 | if (isHeadings) { |
307 | iFeedEntry *entry = i.ptr; | 324 | // printf("Updating sourceID %d...\n", sourceId); |
308 | /* Disregard old entries. */ | 325 | iStringSet *known = listHeadingEntriesFrom_Feeds_(d, sourceId); |
309 | if (secondsSince_Time(&now, &entry->posted) >= maxAge_Visited) { | 326 | // puts(" Known URLs:"); |
310 | /* We don't remember this far back, so the unread status of the entry would | 327 | // iConstForEach(StringSet, ss, known) { |
311 | be incorrect. */ | 328 | // printf(" {%s}\n", cstr_String(ss.value)); |
312 | continue; | 329 | // } |
330 | iStringSet *presentInSource = new_StringSet(); | ||
331 | /* Look for unknown entries. */ | ||
332 | iForEach(PtrArray, i, incoming) { | ||
333 | iFeedEntry *entry = i.ptr; | ||
334 | insert_StringSet(presentInSource, &entry->url); | ||
335 | if (!contains_StringSet(known, &entry->url)) { | ||
336 | // printf(" {%s} is new\n", cstr_String(&entry->url)); | ||
337 | insert_SortedArray(&d->entries, &entry); | ||
338 | gotNew = iTrue; | ||
339 | remove_PtrArrayIterator(&i); | ||
340 | } | ||
341 | } | ||
342 | // puts(" URLs present in source:"); | ||
343 | // iConstForEach(StringSet, ps, presentInSource) { | ||
344 | // printf(" {%s}\n", cstr_String(ps.value)); | ||
345 | // } | ||
346 | // puts(" URLs to purge:"); | ||
347 | /* All known entries that are no longer present in source must be deleted. */ | ||
348 | iForEach(Array, e, &d->entries.values) { | ||
349 | iFeedEntry *entry = *(iFeedEntry **) e.value; | ||
350 | if (entry->bookmarkId == sourceId && | ||
351 | !contains_StringSet(presentInSource, &entry->url)) { | ||
352 | // printf(" {%s}\n", cstr_String(&entry->url)); | ||
353 | delete_FeedEntry(entry); | ||
354 | remove_ArrayIterator(&e); | ||
355 | } | ||
313 | } | 356 | } |
314 | size_t pos; | 357 | // puts("Done."); |
315 | if (locate_SortedArray(&d->entries, &entry, &pos)) { | 358 | iRelease(presentInSource); |
316 | iFeedEntry *existing = *(iFeedEntry **) at_SortedArray(&d->entries, pos); | 359 | iRelease(known); |
317 | iAssert(isHeadingEntry_FeedEntry_(existing) == isHeadingEntry_FeedEntry_(entry)); | 360 | } |
318 | /* Already known, but update it, maybe the time and label have changed. */ | 361 | else { |
319 | if (!isHeadingEntry_FeedEntry_(existing)) { | 362 | iForEach(PtrArray, i, incoming) { |
363 | iFeedEntry *entry = i.ptr; | ||
364 | /* Disregard old incoming entries. */ | ||
365 | if (secondsSince_Time(&now, &entry->posted) >= maxAge_Visited) { | ||
366 | /* We don't remember this far back, so the unread status of the entry would | ||
367 | be incorrect. */ | ||
368 | continue; | ||
369 | } | ||
370 | size_t pos; | ||
371 | if (locate_SortedArray(&d->entries, &entry, &pos)) { | ||
372 | iFeedEntry *existing = *(iFeedEntry **) at_SortedArray(&d->entries, pos); | ||
373 | iAssert(!isHeadingEntry_FeedEntry_(existing)); | ||
374 | iAssert(!isHeadingEntry_FeedEntry_(entry)); | ||
375 | /* Already known, but update it, maybe the time and label have changed. */ | ||
320 | iBool changed = iFalse; | 376 | iBool changed = iFalse; |
321 | iDate newDate; | 377 | iDate newDate; |
322 | iDate oldDate; | 378 | iDate oldDate; |
@@ -336,12 +392,12 @@ static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) { | |||
336 | gotNew = iTrue; | 392 | gotNew = iTrue; |
337 | } | 393 | } |
338 | } | 394 | } |
395 | else { | ||
396 | insert_SortedArray(&d->entries, &entry); | ||
397 | gotNew = iTrue; | ||
398 | } | ||
399 | remove_PtrArrayIterator(&i); | ||
339 | } | 400 | } |
340 | else { | ||
341 | insert_SortedArray(&d->entries, &entry); | ||
342 | gotNew = iTrue; | ||
343 | } | ||
344 | remove_PtrArrayIterator(&i); | ||
345 | } | 401 | } |
346 | unlock_Mutex(d->mtx); | 402 | unlock_Mutex(d->mtx); |
347 | return gotNew; | 403 | return gotNew; |
@@ -369,7 +425,8 @@ static iThreadResult fetch_Feeds_(iThread *thread) { | |||
369 | if (isFinished_GmRequest(work[i]->request)) { | 425 | if (isFinished_GmRequest(work[i]->request)) { |
370 | /* TODO: Handle redirects. Need to resubmit the job with new URL. */ | 426 | /* TODO: Handle redirects. Need to resubmit the job with new URL. */ |
371 | parseResult_FeedJob_(work[i]); | 427 | parseResult_FeedJob_(work[i]); |
372 | gotNew |= updateEntries_Feeds_(d, &work[i]->results); | 428 | gotNew |= updateEntries_Feeds_( |
429 | d, work[i]->checkHeadings, work[i]->bookmarkId, &work[i]->results); | ||
373 | delete_FeedJob(work[i]); | 430 | delete_FeedJob(work[i]); |
374 | work[i] = NULL; | 431 | work[i] = NULL; |
375 | } | 432 | } |
@@ -407,7 +464,7 @@ static iBool startWorker_Feeds_(iFeeds *d) { | |||
407 | if (!contains_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm))) { | 464 | if (!contains_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm))) { |
408 | job->isFirstUpdate = iTrue; | 465 | job->isFirstUpdate = iTrue; |
409 | // printf("first check of %x: %s\n", id_Bookmark(bm), cstr_String(&bm->title)); | 466 | // printf("first check of %x: %s\n", id_Bookmark(bm), cstr_String(&bm->title)); |
410 | fflush(stdout); | 467 | // fflush(stdout); |
411 | insert_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm)); | 468 | insert_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm)); |
412 | } | 469 | } |
413 | pushBack_PtrArray(&d->jobs, job); | 470 | pushBack_PtrArray(&d->jobs, job); |
@@ -539,6 +596,11 @@ static void load_Feeds_(iFeeds *d) { | |||
539 | stripDefaultUrlPort_String(&entry->url); | 596 | stripDefaultUrlPort_String(&entry->url); |
540 | set_String(&entry->url, canonicalUrl_String(&entry->url)); | 597 | set_String(&entry->url, canonicalUrl_String(&entry->url)); |
541 | set_String(&entry->title, title); | 598 | set_String(&entry->title, title); |
599 | entry->isHeading = isHeadingEntry_FeedEntry_(entry); | ||
600 | if (entry->isHeading) { | ||
601 | printf("[Feeds] src:%d url:{%s}\n", entry->bookmarkId, | ||
602 | cstr_String(&entry->url)); | ||
603 | } | ||
542 | insert_SortedArray(&d->entries, &entry); | 604 | insert_SortedArray(&d->entries, &entry); |
543 | } | 605 | } |
544 | delete_String(title); | 606 | delete_String(title); |
@@ -642,7 +704,8 @@ size_t numUnread_Feeds(void) { | |||
642 | size_t max = 100; /* match the number of items shown in the sidebar */ | 704 | size_t max = 100; /* match the number of items shown in the sidebar */ |
643 | iConstForEach(PtrArray, i, listEntries_Feeds()) { | 705 | iConstForEach(PtrArray, i, listEntries_Feeds()) { |
644 | if (!max--) break; | 706 | if (!max--) break; |
645 | if (isUnread_FeedEntry(i.ptr)) { | 707 | const iFeedEntry *entry = i.ptr; |
708 | if (isValid_Time(&entry->discovered) && isUnread_FeedEntry(i.ptr)) { | ||
646 | count++; | 709 | count++; |
647 | } | 710 | } |
648 | } | 711 | } |
diff --git a/src/feeds.h b/src/feeds.h index 5ff2adfb..8863d24f 100644 --- a/src/feeds.h +++ b/src/feeds.h | |||
@@ -34,6 +34,7 @@ struct Impl_FeedEntry { | |||
34 | iTime discovered; | 34 | iTime discovered; |
35 | iString url; | 35 | iString url; |
36 | iString title; | 36 | iString title; |
37 | iBool isHeading; /* URL fragment points to a heading */ | ||
37 | uint32_t bookmarkId; /* note: runtime only, not a persistent ID */ | 38 | uint32_t bookmarkId; /* note: runtime only, not a persistent ID */ |
38 | }; | 39 | }; |
39 | 40 | ||