diff --git a/NOTES.md b/NOTES.md index 4cc6f77..9d4f3f9 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1,6 +1,6 @@ # Notes -## Subscriptions (temp) +## Subscriptions - TODO: remove all logger lines ## Issues diff --git a/README.md b/README.md index 1cf6b42..a13bcd6 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Features in *italics* are in the master branch, but not in the latest release. - https://lists.orbitalfox.eu/archives/gemini/2020/001400.html - [x] Subscriptions - RSS, Atom, and [JSON Feeds](https://jsonfeed.org/) are all supported - - So is tracking any page to be notified when it changes + - So is tracking a page, to know when its content changes - [ ] Stream support - [ ] Table of contents for pages - [ ] History browser diff --git a/config/config.go b/config/config.go index e32654b..d69184a 100644 --- a/config/config.go +++ b/config/config.go @@ -237,6 +237,7 @@ func Init() error { viper.SetDefault("subscriptions.popup", true) viper.SetDefault("subscriptions.update_interval", 1800) viper.SetDefault("subscriptions.workers", 3) + viper.SetDefault("subscriptions.entries_per_page", 20) viper.SetConfigFile(configPath) viper.SetConfigType("toml") diff --git a/config/default.go b/config/default.go index 705664e..dd1fc7b 100644 --- a/config/default.go +++ b/config/default.go @@ -153,6 +153,9 @@ update_interval = 1800 # 30 mins # update times. Any value below 1 will be corrected to 1. workers = 3 +# The number of subscription updates displayed per page. +entries_per_page = 20 + [theme] # This section is for changing the COLORS used in Amfora. diff --git a/default-config.toml b/default-config.toml index 4ba508d..05cc68d 100644 --- a/default-config.toml +++ b/default-config.toml @@ -150,6 +150,9 @@ update_interval = 1800 # 30 mins # update times. Any value below 1 will be corrected to 1. workers = 3 +# The number of subscription updates displayed per page. +entries_per_page = 20 + [theme] # This section is for changing the COLORS used in Amfora. diff --git a/display/display.go b/display/display.go index 7bb147d..2110745 100644 --- a/display/display.go +++ b/display/display.go @@ -294,7 +294,7 @@ func Init() { } return nil case tcell.KeyCtrlA: - Subscriptions(tabs[curTab]) + Subscriptions(tabs[curTab], "about:subscriptions") tabs[curTab].addToHistory("about:subscriptions") return nil case tcell.KeyCtrlX: @@ -578,8 +578,8 @@ func Reload() { func URL(u string) { t := tabs[curTab] if strings.HasPrefix(u, "about:") { - if ok := handleAbout(t, u); ok { - t.addToHistory(u) + if final, ok := handleAbout(t, u); ok { + t.addToHistory(final) } return } diff --git a/display/handlers.go b/display/handlers.go index 5d791b4..58833f1 100644 --- a/display/handlers.go +++ b/display/handlers.go @@ -169,34 +169,40 @@ func handleFavicon(t *tab, host, old string) { // // It does not add the displayed page to history. // -// It returns a bool indicating if the provided URL could be handled. -func handleAbout(t *tab, u string) bool { +// It returns the URL displayed, and a bool indicating if the provided +// URL could be handled. The string returned will always be empty +// if the bool is false. +func handleAbout(t *tab, u string) (string, bool) { if !strings.HasPrefix(u, "about:") { - return false + return "", false } switch u { case "about:bookmarks": Bookmarks(t) - return true - case "about:subscriptions": - Subscriptions(t) - return true + return u, true case "about:newtab": temp := newTabPage // Copy setPage(t, &temp) t.applyBottomBar() - return true + return u, true } + if u == "about:subscriptions" || (len(u) > 20 && u[:20] == "about:subscriptions?") { + // about:subscriptions?2 views page 2 + return Subscriptions(t, u), true + } if u == "about:manage-subscriptions" || (len(u) > 27 && u[:27] == "about:manage-subscriptions?") { ManageSubscriptions(t, u) // Don't count remove command in history - return u == "about:manage-subscriptions" + if u == "about:manage-subscriptions" { + return u, true + } + return "", false } Error("Error", "Not a valid 'about:' URL.") - return false + return "", false } // handleURL displays whatever action is needed for the provided URL, @@ -252,7 +258,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { App.SetFocus(t.view) if strings.HasPrefix(u, "about:") { - return ret(u, handleAbout(t, u)) + return ret(handleAbout(t, u)) } u = normalizeURL(u) diff --git a/display/private.go b/display/private.go index d718a9f..1ead234 100644 --- a/display/private.go +++ b/display/private.go @@ -20,8 +20,8 @@ import ( // It will handle setting the bottomBar. func followLink(t *tab, prev, next string) { if strings.HasPrefix(next, "about:") { - if ok := handleAbout(t, next); ok { - t.addToHistory(next) + if final, ok := handleAbout(t, next); ok { + t.addToHistory(final) } return } diff --git a/display/subscriptions.go b/display/subscriptions.go index bfed1b6..27edfb9 100644 --- a/display/subscriptions.go +++ b/display/subscriptions.go @@ -4,6 +4,7 @@ import ( "fmt" "net/url" "path" + "sort" "strconv" "strings" "time" @@ -20,7 +21,9 @@ import ( "github.com/spf13/viper" ) -var subscriptionPageUpdated time.Time +// Map page number (zero-indexed) to the time it was made at. +// This allows for caching the pages until there's an update. +var subscriptionPageUpdated = make(map[int]time.Time) // toLocalDay truncates the provided time to a date only, // but converts to the local time first. @@ -30,54 +33,125 @@ func toLocalDay(t time.Time) time.Time { } // Subscriptions displays the subscriptions page on the current tab. -func Subscriptions(t *tab) { +func Subscriptions(t *tab, u string) string { logger.Log.Println("display.Subscriptions called") + pageN := 0 // Pages are zero-indexed internally + + // Correct URL if query string exists + // The only valid query string is an int above 1. + // Anything "redirects" to the first page, with no query string. + // This is done over just serving the first page content for + // invalid query strings so that there won't be duplicate caches. + correctURL := func(u2 string) string { + if len(u2) > 20 && u2[:20] == "about:subscriptions?" { + query, err := gemini.QueryUnescape(u2[20:]) + if err != nil { + return "about:subscriptions" + } + // Valid query string + i, err := strconv.Atoi(query) + if err != nil { + // Not an int + return "about:subscriptions" + } + if i < 2 { + return "about:subscriptions" + } + // Valid int above 1 + pageN = i - 1 // Pages are zero-indexed internally + return u2 + } + return u2 + } + u = correctURL(u) + // Retrieve cached version if there hasn't been any updates - p, ok := cache.GetPage("about:subscriptions") - if subscriptionPageUpdated.After(subscriptions.LastUpdated) && ok { + p, ok := cache.GetPage(u) + if subscriptionPageUpdated[pageN].After(subscriptions.LastUpdated) && ok { logger.Log.Println("using cached subscriptions page") setPage(t, p) t.applyBottomBar() - return + return u } logger.Log.Println("started rendering subscriptions page") - rawPage := "# Subscriptions\n\n" + - "See the help (by pressing ?) for details on how to use this page.\n\n" + - "If you just opened Amfora then updates will appear incrementally. Reload the page to see them.\n\n" + - "=> about:manage-subscriptions Manage subscriptions\n" - - // curDay represents what day of posts the loop is on. - // It only goes backwards in time. - // Its initial setting means: - // Only display posts older than 26 hours in the future, nothing further in the future. - // - // 26 hours was chosen because it is the largest timezone difference - // currently in the world. Posts may be dated in the future - // due to software bugs, where the local user's date is used, but - // the UTC timezone is specified. I believe gemfeed does this. - curDay := toLocalDay(time.Now()).Add(26 * time.Hour) - pe := subscriptions.GetPageEntries() - for _, entry := range pe.Entries { // From new to old - // Convert to local time, remove sub-day info - pub := toLocalDay(entry.Published) + // Figure out where the entries for this page start, if at all. + epp := viper.GetInt("subscriptions.entries_per_page") + if epp <= 0 { + epp = 1 + } + start := pageN * epp // Index of the first page entry to be displayed + end := start + epp + if end > len(pe.Entries) { + end = len(pe.Entries) + } - if pub.Before(curDay) { - // This post is on a new day, add a day header - curDay = pub - rawPage += fmt.Sprintf("\n## %s\n\n", curDay.Format("Jan 02, 2006")) + var rawPage string + if pageN == 0 { + rawPage = "# Subscriptions\n\n" + rawPage + } else { + rawPage = fmt.Sprintf("# Subscriptions (page %d)\n\n", pageN+1) + rawPage + } + + if start > len(pe.Entries)-1 && len(pe.Entries) != 0 { + // The page is out of range, doesn't exist + rawPage += "This page does not exist.\n\n=> about:subscriptions Subscriptions\n" + } else { + // Render page + + rawPage += "You can use Ctrl-X to subscribe to a page, or to an Atom/RSS/JSON feed. See the online wiki for more.\n" + + "If you just opened Amfora then updates may appear incrementally. Reload the page to see them.\n\n" + + "=> about:manage-subscriptions Manage subscriptions\n\n" + + // curDay represents what day of posts the loop is on. + // It only goes backwards in time. + // Its initial setting means: + // Only display posts older than 26 hours in the future, nothing further in the future. + // + // 26 hours was chosen because it is the largest timezone difference + // currently in the world. Posts may be dated in the future + // due to software bugs, where the local user's date is used, but + // the UTC timezone is specified. Gemfeed does this at the time of + // writing, but will not after #3 gets merged on its repo. Still, + // the older version will be used for a while. + curDay := toLocalDay(time.Now()).Add(26 * time.Hour) + + for _, entry := range pe.Entries[start:end] { // From new to old + // Convert to local time, remove sub-day info + pub := toLocalDay(entry.Published) + + if pub.Before(curDay) { + // This post is on a new day, add a day header + curDay = pub + rawPage += fmt.Sprintf("\n## %s\n\n", curDay.Format("Jan 02, 2006")) + } + if entry.Title == "" || entry.Title == "/" { + // Just put author/title + // Mainly used for when you're tracking the root domain of a site + rawPage += fmt.Sprintf("=>%s %s\n", entry.URL, entry.Prefix) + } else { + // Include title and dash + rawPage += fmt.Sprintf("=>%s %s - %s\n", entry.URL, entry.Prefix, entry.Title) + } } - if entry.Title == "" || entry.Title == "/" { - // Just put author/title - // Mainly used for when you're tracking the root domain of a site - rawPage += fmt.Sprintf("=>%s %s\n", entry.URL, entry.Prefix) - } else { - // Include title and dash - rawPage += fmt.Sprintf("=>%s %s - %s\n", entry.URL, entry.Prefix, entry.Title) + + if pageN == 0 && len(pe.Entries) > epp { + // First page, and there's more than can fit + rawPage += "\n\n=> about:subscriptions?2 Next Page\n" + } else if pageN > 0 { + // A later page + rawPage += fmt.Sprintf( + "\n\n=> about:subscriptions?%d Previous Page\n", + pageN, // pageN is zero-indexed but the query string is one-indexed + ) + if end != len(pe.Entries)-1 { + // There's more + rawPage += fmt.Sprintf("=> about:subscriptions?%d Next Page\n", pageN+2) + } } } @@ -86,7 +160,7 @@ func Subscriptions(t *tab) { Raw: rawPage, Content: content, Links: links, - URL: "about:subscriptions", + URL: u, Width: termW, Mediatype: structs.TextGemini, } @@ -94,9 +168,11 @@ func Subscriptions(t *tab) { setPage(t, &page) t.applyBottomBar() - subscriptionPageUpdated = time.Now() + subscriptionPageUpdated[pageN] = time.Now() logger.Log.Println("done rendering subscriptions page") + + return u } // ManageSubscriptions displays the subscription managing page in @@ -109,9 +185,13 @@ func ManageSubscriptions(t *tab, u string) { } rawPage := "# Manage Subscriptions\n\n" + - "Below is list of URLs, both feeds and pages. Navigate to the link to unsubscribe from that feed or page.\n\n" + "Below is list of URLs you are subscribed to, both feeds and pages. " + + "Navigate to the link to unsubscribe from that feed or page.\n\n" - for _, u2 := range subscriptions.AllURLS() { + urls := subscriptions.AllURLS() + sort.Strings(urls) + + for _, u2 := range urls { rawPage += fmt.Sprintf( "=>%s %s\n", "about:manage-subscriptions?"+gemini.QueryEscape(u2), diff --git a/subscriptions/subscriptions.go b/subscriptions/subscriptions.go index 16b4743..d12a00d 100644 --- a/subscriptions/subscriptions.go +++ b/subscriptions/subscriptions.go @@ -11,7 +11,6 @@ import ( "os" "path" "reflect" - "sort" "strings" "sync" "time" @@ -370,7 +369,7 @@ func updateAll() { wg.Wait() } -// AllURLs returns all the subscribed-to URLS, sorted alphabetically. +// AllURLs returns all the subscribed-to URLS. func AllURLS() []string { data.RLock() defer data.RUnlock() @@ -386,7 +385,6 @@ func AllURLS() []string { i++ } - sort.Strings(urls) return urls } @@ -394,7 +392,7 @@ func AllURLS() []string { // The URL must be provided. It will do nothing if the URL is // not an actual subscription. // -// It returns any errors that occured when saving to disk. +// It returns any errors that occurred when saving to disk. func Remove(u string) error { data.Lock() // Just delete from both instead of using a loop to find it