diff --git a/internal/webserver/handler_tweet_detail.go b/internal/webserver/handler_tweet_detail.go index 1c1f49a..d6c2a60 100644 --- a/internal/webserver/handler_tweet_detail.go +++ b/internal/webserver/handler_tweet_detail.go @@ -50,13 +50,16 @@ func (app *Application) ensure_tweet(id scraper.TweetID, is_forced bool, is_conv if is_needing_scrape && !app.IsScrapingDisabled { trove, err := scraper.GetTweetFullAPIV2(id, 50) // TODO: parameterizable + + // Save the trove unless there was an unrecoverable error if err == nil || errors.Is(err, scraper.END_OF_FEED) || errors.Is(err, scraper.ErrRateLimited) { app.Profile.SaveTweetTrove(trove, false) go app.Profile.SaveTweetTrove(trove, true) // Download the content in the background _, is_available = trove.Tweets[id] - } else { - app.ErrorLog.Print(err) - // TODO: show error in UI + } + + if err != nil && !errors.Is(err, scraper.END_OF_FEED) { + return scraper.Tweet{}, fmt.Errorf("scraper error: %w", err) } } else if is_needing_scrape { app.InfoLog.Printf("Would have scraped Tweet: %d", id) @@ -115,10 +118,31 @@ func (app *Application) TweetDetail(w http.ResponseWriter, r *http.Request) { is_conversation_required := len(parts) <= 2 || (parts[2] != "like" && parts[2] != "unlike") tweet, err := app.ensure_tweet(tweet_id, is_scrape_required, is_conversation_required) - if errors.Is(err, ErrNotFound) { - app.error_404(w) - return + var toasts []Toast + if err != nil { + app.ErrorLog.Print(fmt.Errorf("TweetDetail (%d): %w", tweet_id, err)) + if errors.Is(err, ErrNotFound) { + // Can't find the tweet; abort + app.toast(w, r, Toast{Title: "Not found", Message: "Tweet not found in database", Type: "error"}) + return + } else if errors.Is(err, scraper.ErrSessionInvalidated) { + toasts = append(toasts, Toast{ + Title: "Session invalidated", + Message: "Your session has been invalidated by Twitter. You'll have to log in again.", + Type: "error", + }) + // TODO: delete the invalidated session + } else if errors.Is(err, scraper.ErrRateLimited) { + toasts = append(toasts, Toast{ + Title: "Rate limited", + Message: "While scraping, a rate-limit was hit. Results may be incomplete.", + Type: "warning", + }) + } else { + panic(err) // Let the 500 handler deal with it + } } + req_with_tweet := r.WithContext(add_tweet_to_context(r.Context(), tweet)) if len(parts) > 2 && parts[2] == "like" { @@ -137,7 +161,7 @@ func (app *Application) TweetDetail(w http.ResponseWriter, r *http.Request) { app.buffered_render_page( w, "tpl/tweet_detail.tpl", - PageGlobalData{TweetTrove: twt_detail.TweetTrove, FocusedTweetID: data.MainTweetID}, + PageGlobalData{TweetTrove: twt_detail.TweetTrove, FocusedTweetID: data.MainTweetID, Toasts: toasts}, data, ) } diff --git a/internal/webserver/renderer_helpers.go b/internal/webserver/renderer_helpers.go index c6e9c42..6351ead 100644 --- a/internal/webserver/renderer_helpers.go +++ b/internal/webserver/renderer_helpers.go @@ -25,6 +25,7 @@ type PageGlobalData struct { SearchText string FocusedTweetID scraper.TweetID Notifications + Toasts []Toast } func (d PageGlobalData) Tweet(id scraper.TweetID) scraper.Tweet { diff --git a/internal/webserver/response_helpers.go b/internal/webserver/response_helpers.go index f489c10..4a50ee5 100644 --- a/internal/webserver/response_helpers.go +++ b/internal/webserver/response_helpers.go @@ -38,7 +38,7 @@ func (app *Application) error_500(w http.ResponseWriter, r *http.Request, err er } func (app *Application) toast(w http.ResponseWriter, r *http.Request, t Toast) { - // Reset the HTMX response to return an error toast and put it in the + // Reset the HTMX response to return an error toast and append it to the Toasts container w.Header().Set("HX-Reswap", "beforeend") w.Header().Set("HX-Retarget", "#toasts") w.Header().Set("HX-Push-Url", "false") @@ -46,6 +46,12 @@ func (app *Application) toast(w http.ResponseWriter, r *http.Request, t Toast) { app.buffered_render_htmx(w, "toast", PageGlobalData{}, t) } +// `Type` can be: +// - "success" (default) +// - "warning" +// - "error" +// +// If "AutoCloseDelay" is not 0, the toast will auto-disappear after that many milliseconds. type Toast struct { Title string Message string diff --git a/internal/webserver/tpl/includes/base.tpl b/internal/webserver/tpl/includes/base.tpl index b3755d0..83e9771 100644 --- a/internal/webserver/tpl/includes/base.tpl +++ b/internal/webserver/tpl/includes/base.tpl @@ -50,6 +50,9 @@
+ {{range (global_data).Toasts}} + {{template "toast" .}} + {{end}}
diff --git a/internal/webserver/tpl/includes/toast.tpl b/internal/webserver/tpl/includes/toast.tpl index 8f76653..7e5634e 100644 --- a/internal/webserver/tpl/includes/toast.tpl +++ b/internal/webserver/tpl/includes/toast.tpl @@ -5,6 +5,9 @@ hx-on::load="setTimeout(() => this.remove(), {{.AutoCloseDelay}} + 2000); setTimeout(() => this.classList.add('disappearing'), {{.AutoCloseDelay}})" {{end}} > + {{if .Title}} +

{{.Title}}

+ {{end}} {{.Message}} {{if not .AutoCloseDelay}}