summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--res/about/version.gmi1
-rw-r--r--src/app.c6
-rw-r--r--src/feeds.c127
-rw-r--r--src/feeds.h9
-rw-r--r--src/ui/documentwidget.c35
-rw-r--r--src/ui/sidebarwidget.c22
6 files changed, 161 insertions, 39 deletions
diff --git a/res/about/version.gmi b/res/about/version.gmi
index d209d376..85490dc7 100644
--- a/res/about/version.gmi
+++ b/res/about/version.gmi
@@ -7,6 +7,7 @@
7# Release notes 7# Release notes
8 8
9## 0.12 9## 0.12
10* Subscribe to new headings on pages.
10 11
11## 0.11 12## 0.11
12* Added feed subscriptions. A subscription is any bookmark with the "subscribed" tag. Subscribed feeds are refreshed in the background while Lagrange is running. 13* Added feed subscriptions. A subscription is any bookmark with the "subscribed" tag. Subscribed feeds are refreshed in the background while Lagrange is running.
diff --git a/src/app.c b/src/app.c
index 39636c29..eb8eb16a 100644
--- a/src/app.c
+++ b/src/app.c
@@ -1090,6 +1090,12 @@ iBool handleCommand_App(const char *cmd) {
1090 if (gotoHeading.start) { 1090 if (gotoHeading.start) {
1091 postCommandf_App("document.goto heading:%s", cstr_Rangecc(gotoHeading)); 1091 postCommandf_App("document.goto heading:%s", cstr_Rangecc(gotoHeading));
1092 } 1092 }
1093 const iRangecc gotoUrlHeading = range_Command(cmd, "gotourlheading");
1094 if (gotoUrlHeading.start) {
1095 postCommandf_App("document.goto heading:%s",
1096 cstrCollect_String(urlDecode_String(
1097 collect_String(newRange_String(gotoUrlHeading)))));
1098 }
1093 } 1099 }
1094 else if (equal_Command(cmd, "document.request.cancelled")) { 1100 else if (equal_Command(cmd, "document.request.cancelled")) {
1095 /* TODO: How should cancelled requests be treated in the history? */ 1101 /* TODO: How should cancelled requests be treated in the history? */
diff --git a/src/feeds.c b/src/feeds.c
index 37a4a48b..9df94d05 100644
--- a/src/feeds.c
+++ b/src/feeds.c
@@ -27,10 +27,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
27#include "app.h" 27#include "app.h"
28 28
29#include <the_Foundation/file.h> 29#include <the_Foundation/file.h>
30#include <the_Foundation/mutex.h>
31#include <the_Foundation/hash.h> 30#include <the_Foundation/hash.h>
32#include <the_Foundation/queue.h> 31#include <the_Foundation/intset.h>
32#include <the_Foundation/mutex.h>
33#include <the_Foundation/path.h> 33#include <the_Foundation/path.h>
34#include <the_Foundation/queue.h>
34#include <the_Foundation/regexp.h> 35#include <the_Foundation/regexp.h>
35#include <the_Foundation/stringset.h> 36#include <the_Foundation/stringset.h>
36#include <the_Foundation/thread.h> 37#include <the_Foundation/thread.h>
@@ -55,12 +56,36 @@ void deinit_FeedEntry(iFeedEntry *d) {
55 deinit_String(&d->url); 56 deinit_String(&d->url);
56} 57}
57 58
59const iString *url_FeedEntry(const iFeedEntry *d) {
60 const size_t fragPos = indexOf_String(&d->url, '#');
61 if (fragPos != iInvalidPos) {
62 return collect_String(newRange_String((iRangecc){ constBegin_String(&d->url),
63 constBegin_String(&d->url) + fragPos }));
64 }
65 return &d->url;
66}
67
68iBool isUnread_FeedEntry(const iFeedEntry *d) {
69 const size_t fragPos = indexOf_String(&d->url, '#');
70 if (fragPos != iInvalidPos) {
71 /* Check if the entry is newer than the latest visit. */
72 const iTime visTime = urlVisitTime_Visited(visited_App(), url_FeedEntry(d));
73 return cmp_Time(&visTime, &d->posted) < 0;
74 }
75 if (!containsUrl_Visited(visited_App(), &d->url)) {
76 return iTrue;
77 }
78 return iFalse;
79}
80
58/*----------------------------------------------------------------------------------------------*/ 81/*----------------------------------------------------------------------------------------------*/
59 82
60struct Impl_FeedJob { 83struct Impl_FeedJob {
61 iString url; 84 iString url;
62 uint32_t bookmarkId; 85 uint32_t bookmarkId;
63 iTime startTime; 86 iTime startTime;
87 iBool isFirstUpdate; /* hasn't been checked ever before */
88 iBool checkHeadings;
64 iGmRequest *request; 89 iGmRequest *request;
65 iPtrArray results; 90 iPtrArray results;
66}; 91};
@@ -71,6 +96,8 @@ static void init_FeedJob(iFeedJob *d, const iBookmark *bookmark) {
71 d->request = NULL; 96 d->request = NULL;
72 init_PtrArray(&d->results); 97 init_PtrArray(&d->results);
73 iZap(d->startTime); 98 iZap(d->startTime);
99 d->isFirstUpdate = iFalse;
100 d->checkHeadings = hasTag_Bookmark(bookmark, "headings");
74} 101}
75 102
76static void deinit_FeedJob(iFeedJob *d) { 103static void deinit_FeedJob(iFeedJob *d) {
@@ -92,6 +119,7 @@ static const int updateIntervalSeconds_Feeds_ = 4 * 60 * 60;
92struct Impl_Feeds { 119struct Impl_Feeds {
93 iMutex * mtx; 120 iMutex * mtx;
94 iString saveDir; 121 iString saveDir;
122 iIntSet previouslyCheckedFeeds; /* bookmark IDs */
95 iTime lastRefreshedAt; 123 iTime lastRefreshedAt;
96 int refreshTimer; 124 int refreshTimer;
97 iThread * worker; 125 iThread * worker;
@@ -113,7 +141,13 @@ static void submit_FeedJob_(iFeedJob *d) {
113 141
114static iBool isSubscribed_(void *context, const iBookmark *bm) { 142static iBool isSubscribed_(void *context, const iBookmark *bm) {
115 iUnused(context); 143 iUnused(context);
116 return indexOfCStr_String(&bm->tags, "subscribed") != iInvalidPos; /* TODO: RegExp with \b */ 144 static iRegExp *pattern_ = NULL;
145 if (!pattern_) {
146 pattern_ = new_RegExp("\\bsubscribed\\b", caseSensitive_RegExpOption);
147 }
148 iRegExpMatch m;
149 init_RegExpMatch(&m);
150 return matchString_RegExp(pattern_, &bm->tags, &m);
117} 151}
118 152
119static const iPtrArray *listSubscriptions_(void) { 153static const iPtrArray *listSubscriptions_(void) {
@@ -181,6 +215,28 @@ static void parseResult_FeedJob_(iFeedJob *d) {
181 .year = year, .month = month, .day = day, .hour = 12 /* noon UTC */ }); 215 .year = year, .month = month, .day = day, .hour = 12 /* noon UTC */ });
182 pushBack_PtrArray(&d->results, entry); 216 pushBack_PtrArray(&d->results, entry);
183 } 217 }
218 if (d->checkHeadings) {
219 init_RegExpMatch(&m);
220 if (startsWith_Rangecc(line, "#")) {
221 while (*line.start == '#' && line.start < line.end) {
222 line.start++;
223 }
224 trimStart_Rangecc(&line);
225 iFeedEntry *entry = new_FeedEntry();
226 entry->posted = now;
227 if (!d->isFirstUpdate) {
228 entry->discovered = now;
229 }
230 entry->bookmarkId = d->bookmarkId;
231 iString *title = newRange_String(line);
232 set_String(&entry->title, title);
233 set_String(&entry->url, &d->url);
234 appendChar_String(&entry->url, '#');
235 append_String(&entry->url, collect_String(urlEncode_String(title)));
236 delete_String(title);
237 pushBack_PtrArray(&d->results, entry);
238 }
239 }
184 } 240 }
185 deinit_String(&src); 241 deinit_String(&src);
186 iRelease(linkPattern); 242 iRelease(linkPattern);
@@ -207,7 +263,8 @@ static void save_Feeds_(iFeeds *d) {
207 initCurrent_Time(&now); 263 initCurrent_Time(&now);
208 iConstForEach(Array, i, &d->entries.values) { 264 iConstForEach(Array, i, &d->entries.values) {
209 const iFeedEntry *entry = *(const iFeedEntry **) i.value; 265 const iFeedEntry *entry = *(const iFeedEntry **) i.value;
210 if (secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) { 266 if (isValid_Time(&entry->discovered) &&
267 secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) {
211 continue; /* Forget entries discovered long ago. */ 268 continue; /* Forget entries discovered long ago. */
212 } 269 }
213 format_String(str, "%x\n%llu\n%llu\n%s\n%s\n", 270 format_String(str, "%x\n%llu\n%llu\n%s\n%s\n",
@@ -225,6 +282,10 @@ static void save_Feeds_(iFeeds *d) {
225 iRelease(f); 282 iRelease(f);
226} 283}
227 284
285static iBool isHeadingEntry_FeedEntry_(const iFeedEntry *d) {
286 return contains_String(&d->url, '#');
287}
288
228static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) { 289static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) {
229 iBool gotNew = iFalse; 290 iBool gotNew = iFalse;
230 lock_Mutex(d->mtx); 291 lock_Mutex(d->mtx);
@@ -234,23 +295,25 @@ static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) {
234 if (locate_SortedArray(&d->entries, &entry, &pos)) { 295 if (locate_SortedArray(&d->entries, &entry, &pos)) {
235 iFeedEntry *existing = *(iFeedEntry **) at_SortedArray(&d->entries, pos); 296 iFeedEntry *existing = *(iFeedEntry **) at_SortedArray(&d->entries, pos);
236 /* Already known, but update it, maybe the time and label have changed. */ 297 /* Already known, but update it, maybe the time and label have changed. */
237 iBool changed = iFalse; 298 if (!isHeadingEntry_FeedEntry_(existing)) {
238 iDate newDate; 299 iBool changed = iFalse;
239 iDate oldDate; 300 iDate newDate;
240 init_Date(&newDate, &entry->posted); 301 iDate oldDate;
241 init_Date(&oldDate, &existing->posted); 302 init_Date(&newDate, &entry->posted);
242 if (!equalCase_String(&existing->title, &entry->title) || 303 init_Date(&oldDate, &existing->posted);
243 (newDate.year != oldDate.year || newDate.month != oldDate.month || 304 if (!equalCase_String(&existing->title, &entry->title) ||
244 newDate.day != oldDate.day)) { 305 (newDate.year != oldDate.year || newDate.month != oldDate.month ||
245 changed = iTrue; 306 newDate.day != oldDate.day)) {
246 } 307 changed = iTrue;
247 set_String(&existing->title, &entry->title); 308 }
248 existing->posted = entry->posted; 309 set_String(&existing->title, &entry->title);
249 delete_FeedEntry(entry); 310 existing->posted = entry->posted;
250 if (changed) { 311 delete_FeedEntry(entry);
251 /* TODO: better to use a new flag for read feed entries? */ 312 if (changed) {
252 removeUrl_Visited(visited_App(), &existing->url); 313 /* TODO: better to use a new flag for read feed entries? */
253 gotNew = iTrue; 314 removeUrl_Visited(visited_App(), &existing->url);
315 gotNew = iTrue;
316 }
254 } 317 }
255 } 318 }
256 else { 319 else {
@@ -312,7 +375,14 @@ static iBool startWorker_Feeds_(iFeeds *d) {
312 } 375 }
313 /* Queue up all the subscriptions for the worker. */ 376 /* Queue up all the subscriptions for the worker. */
314 iConstForEach(PtrArray, i, listSubscriptions_()) { 377 iConstForEach(PtrArray, i, listSubscriptions_()) {
315 iFeedJob* job = new_FeedJob(i.ptr); 378 const iBookmark *bm = i.ptr;
379 iFeedJob *job = new_FeedJob(bm);
380 if (!contains_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm))) {
381 job->isFirstUpdate = iTrue;
382 printf("first check of %x: %s\n", id_Bookmark(bm), cstr_String(&bm->title));
383 fflush(stdout);
384 insert_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm));
385 }
316 pushBack_PtrArray(&d->jobs, job); 386 pushBack_PtrArray(&d->jobs, job);
317 } 387 }
318 if (!isEmpty_Array(&d->jobs)) { 388 if (!isEmpty_Array(&d->jobs)) {
@@ -344,7 +414,7 @@ static void stopWorker_Feeds_(iFeeds *d) {
344} 414}
345 415
346static int cmp_FeedEntryPtr_(const void *a, const void *b) { 416static int cmp_FeedEntryPtr_(const void *a, const void *b) {
347 const iFeedEntry * const *elem[2] = { a, b }; 417 const iFeedEntry * const *elem[2] = { a, b };
348 return cmpString_String(&(*elem[0])->url, &(*elem[1])->url); 418 return cmpString_String(&(*elem[0])->url, &(*elem[1])->url);
349} 419}
350 420
@@ -391,6 +461,7 @@ static void load_Feeds_(iFeeds *d) {
391 node->node.key = id; 461 node->node.key = id;
392 node->bookmarkId = bookmarkId; 462 node->bookmarkId = bookmarkId;
393 insert_Hash(feeds, &node->node); 463 insert_Hash(feeds, &node->node);
464 insert_IntSet(&d->previouslyCheckedFeeds, id);
394 } 465 }
395 } 466 }
396 break; 467 break;
@@ -409,8 +480,9 @@ static void load_Feeds_(iFeeds *d) {
409 if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) { 480 if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) {
410 goto aborted; 481 goto aborted;
411 } 482 }
412 const unsigned long long discovered = strtoull(line.start, NULL, 10); 483 char *endp = NULL;
413 if (discovered == 0) { 484 const unsigned long long discovered = strtoull(line.start, &endp, 10);
485 if (endp != line.end) {
414 goto aborted; 486 goto aborted;
415 } 487 }
416 if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) { 488 if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) {
@@ -457,6 +529,7 @@ void init_Feeds(const char *saveDir) {
457 iFeeds *d = &feeds_; 529 iFeeds *d = &feeds_;
458 d->mtx = new_Mutex(); 530 d->mtx = new_Mutex();
459 initCStr_String(&d->saveDir, saveDir); 531 initCStr_String(&d->saveDir, saveDir);
532 init_IntSet(&d->previouslyCheckedFeeds);
460 iZap(d->lastRefreshedAt); 533 iZap(d->lastRefreshedAt);
461 d->worker = NULL; 534 d->worker = NULL;
462 init_PtrArray(&d->jobs); 535 init_PtrArray(&d->jobs);
@@ -483,6 +556,7 @@ void deinit_Feeds(void) {
483 iFeedEntry **entry = i.value; 556 iFeedEntry **entry = i.value;
484 delete_FeedEntry(*entry); 557 delete_FeedEntry(*entry);
485 } 558 }
559 deinit_IntSet(&d->previouslyCheckedFeeds);
486 deinit_SortedArray(&d->entries); 560 deinit_SortedArray(&d->entries);
487} 561}
488 562
@@ -554,6 +628,9 @@ const iString *entryListPage_Feeds(void) {
554 iZap(on); 628 iZap(on);
555 iConstForEach(PtrArray, i, listEntries_Feeds()) { 629 iConstForEach(PtrArray, i, listEntries_Feeds()) {
556 const iFeedEntry *entry = i.ptr; 630 const iFeedEntry *entry = i.ptr;
631 if (isHidden_FeedEntry(entry)) {
632 continue; /* A hidden entry. */
633 }
557 iDate entryDate; 634 iDate entryDate;
558 init_Date(&entryDate, &entry->posted); 635 init_Date(&entryDate, &entry->posted);
559 if (on.year != entryDate.year || on.month != entryDate.month || on.day != entryDate.day) { 636 if (on.year != entryDate.year || on.month != entryDate.month || on.day != entryDate.day) {
diff --git a/src/feeds.h b/src/feeds.h
index 85769ba8..7528d111 100644
--- a/src/feeds.h
+++ b/src/feeds.h
@@ -35,6 +35,15 @@ struct Impl_FeedEntry {
35 uint32_t bookmarkId; /* note: runtime only, not a persistent ID */ 35 uint32_t bookmarkId; /* note: runtime only, not a persistent ID */
36}; 36};
37 37
38iLocalDef iBool isHidden_FeedEntry(const iFeedEntry *d) {
39 return !isValid_Time(&d->discovered);
40}
41
42const iString * url_FeedEntry (const iFeedEntry *);
43iBool isUnread_FeedEntry (const iFeedEntry *);
44
45/*----------------------------------------------------------------------------------------------*/
46
38void init_Feeds (const char *saveDir); 47void init_Feeds (const char *saveDir);
39void deinit_Feeds (void); 48void deinit_Feeds (void);
40void refresh_Feeds (void); 49void refresh_Feeds (void);
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index cca77c15..96440feb 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -171,6 +171,7 @@ struct Impl_DocumentWidget {
171 const iGmRun * firstVisibleRun; 171 const iGmRun * firstVisibleRun;
172 const iGmRun * lastVisibleRun; 172 const iGmRun * lastVisibleRun;
173 iClick click; 173 iClick click;
174 iString pendingGotoHeading;
174 float initNormScrollY; 175 float initNormScrollY;
175 iAnim scrollY; 176 iAnim scrollY;
176 iAnim sideOpacity; 177 iAnim sideOpacity;
@@ -230,6 +231,7 @@ void init_DocumentWidget(iDocumentWidget *d) {
230 init_PtrArray(&d->visiblePlayers); 231 init_PtrArray(&d->visiblePlayers);
231 d->grabbedPlayer = NULL; 232 d->grabbedPlayer = NULL;
232 d->playerTimer = 0; 233 d->playerTimer = 0;
234 init_String(&d->pendingGotoHeading);
233 init_Click(&d->click, d, SDL_BUTTON_LEFT); 235 init_Click(&d->click, d, SDL_BUTTON_LEFT);
234 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); 236 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
235 d->menu = NULL; /* created when clicking */ 237 d->menu = NULL; /* created when clicking */
@@ -259,6 +261,7 @@ void deinit_DocumentWidget(iDocumentWidget *d) {
259 deinit_Array(&d->outline); 261 deinit_Array(&d->outline);
260 iRelease(d->media); 262 iRelease(d->media);
261 iRelease(d->request); 263 iRelease(d->request);
264 deinit_String(&d->pendingGotoHeading);
262 deinit_Block(&d->sourceContent); 265 deinit_Block(&d->sourceContent);
263 deinit_String(&d->sourceMime); 266 deinit_String(&d->sourceMime);
264 iRelease(d->doc); 267 iRelease(d->doc);
@@ -1070,6 +1073,16 @@ static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool ce
1070 scroll_DocumentWidget_(d, 0); /* clamp it */ 1073 scroll_DocumentWidget_(d, 0); /* clamp it */
1071} 1074}
1072 1075
1076static void scrollToHeading_DocumentWidget_(iDocumentWidget *d, const char *heading) {
1077 iConstForEach(Array, h, headings_GmDocument(d->doc)) {
1078 const iGmHeading *head = h.value;
1079 if (startsWithCase_Rangecc(head->text, heading)) {
1080 postCommandf_App("document.goto loc:%p", head->text.start);
1081 break;
1082 }
1083 }
1084}
1085
1073static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, int delta, 1086static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, int delta,
1074 int duration) { 1087 int duration) {
1075 if (delta == 0) { 1088 if (delta == 0) {
@@ -1623,6 +1636,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1623 updateSideIconBuf_DocumentWidget_(d); 1636 updateSideIconBuf_DocumentWidget_(d);
1624 updateOutline_DocumentWidget_(d); 1637 updateOutline_DocumentWidget_(d);
1625 postCommandf_App("document.changed url:%s", cstr_String(d->mod.url)); 1638 postCommandf_App("document.changed url:%s", cstr_String(d->mod.url));
1639 /* Check for a pending goto. */
1640 if (!isEmpty_String(&d->pendingGotoHeading)) {
1641 scrollToHeading_DocumentWidget_(d, cstr_String(&d->pendingGotoHeading));
1642 clear_String(&d->pendingGotoHeading);
1643 }
1626 return iFalse; 1644 return iFalse;
1627 } 1645 }
1628 else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) { 1646 else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) {
@@ -1779,17 +1797,14 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1779 return iTrue; 1797 return iTrue;
1780 } 1798 }
1781 else if (equal_Command(cmd, "document.goto") && document_App() == d) { 1799 else if (equal_Command(cmd, "document.goto") && document_App() == d) {
1782 const iRangecc heading = range_Command(cmd, "heading"); 1800 const char *heading = suffixPtr_Command(cmd, "heading");
1783 if (heading.start) { 1801 if (heading) {
1784 const char *target = cstr_Rangecc(heading); 1802 if (isRequestOngoing_DocumentWidget(d)) {
1785 iConstForEach(Array, h, headings_GmDocument(d->doc)) { 1803 /* Scroll position set when request finishes. */
1786 const iGmHeading *head = h.value; 1804 setCStr_String(&d->pendingGotoHeading, heading);
1787 if (startsWithCase_Rangecc(head->text, target)) { 1805 return iTrue;
1788 /* TODO: A bit lazy here, the code is right down below. */
1789 postCommandf_App("document.goto loc:%p", head->text.start);
1790 break;
1791 }
1792 } 1806 }
1807 scrollToHeading_DocumentWidget_(d, heading);
1793 return iTrue; 1808 return iTrue;
1794 } 1809 }
1795 const char *loc = pointerLabel_Command(cmd, "loc"); 1810 const char *loc = pointerLabel_Command(cmd, "loc");
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index 6bb6d7a1..5baa08f7 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -123,6 +123,9 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
123 iZap(on); 123 iZap(on);
124 iConstForEach(PtrArray, i, listEntries_Feeds()) { 124 iConstForEach(PtrArray, i, listEntries_Feeds()) {
125 const iFeedEntry *entry = i.ptr; 125 const iFeedEntry *entry = i.ptr;
126 if (isHidden_FeedEntry(entry)) {
127 continue; /* A hidden entry. */
128 }
126 /* For more items, one can always see "about:feeds". A large number of items 129 /* For more items, one can always see "about:feeds". A large number of items
127 is a bit difficult to navigate in the sidebar. */ 130 is a bit difficult to navigate in the sidebar. */
128 if (numItems_ListWidget(d->list) == 100) { 131 if (numItems_ListWidget(d->list) == 100) {
@@ -151,9 +154,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
151 if (equal_String(docUrl, &entry->url)) { 154 if (equal_String(docUrl, &entry->url)) {
152 item->listItem.isSelected = iTrue; /* currently being viewed */ 155 item->listItem.isSelected = iTrue; /* currently being viewed */
153 } 156 }
154 if (!containsUrl_Visited(visited_App(), &entry->url)) { 157 item->indent = isUnread_FeedEntry(entry);
155 item->indent = 1; /* unread */
156 }
157 set_String(&item->url, &entry->url); 158 set_String(&item->url, &entry->url);
158 set_String(&item->label, &entry->title); 159 set_String(&item->label, &entry->title);
159 const iBookmark *bm = get_Bookmarks(bookmarks_App(), entry->bookmarkId); 160 const iBookmark *bm = get_Bookmarks(bookmarks_App(), entry->bookmarkId);
@@ -498,7 +499,20 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, const iSidebarItem *it
498 } 499 }
499 case feeds_SidebarMode: 500 case feeds_SidebarMode:
500 if (!isEmpty_String(&item->url)) { 501 if (!isEmpty_String(&item->url)) {
501 postCommandf_App("open url:%s", cstr_String(&item->url)); 502 const size_t fragPos = indexOf_String(&item->url, '#');
503 if (fragPos != iInvalidPos) {
504 iString *head = collect_String(
505 newRange_String((iRangecc){ constBegin_String(&item->url) + fragPos + 1,
506 constEnd_String(&item->url) }));
507 postCommandf_App(
508 "open gotourlheading:%s url:%s",
509 cstr_String(head),
510 cstr_Rangecc((iRangecc){ constBegin_String(&item->url),
511 constBegin_String(&item->url) + fragPos }));
512 }
513 else {
514 postCommandf_App("open url:%s", cstr_String(&item->url));
515 }
502 } 516 }
503 break; 517 break;
504 case bookmarks_SidebarMode: 518 case bookmarks_SidebarMode: