Change "scrape" from a URL param to a query param
- create generic "refresh" button for User Feed that refreshes whichever tab you're on - refactor TweetDetail view into multiple pieces for easier maintenance - on "Already liked this tweet" error, save the Like instead of discarding it
This commit is contained in:
parent
fa9c913b53
commit
694a8e0bc5
@ -11,6 +11,8 @@ import (
|
||||
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
type TweetDetailData struct {
|
||||
persistence.TweetDetailView
|
||||
MainTweetID scraper.TweetID
|
||||
@ -37,6 +39,77 @@ func (t TweetDetailData) FocusedTweetID() scraper.TweetID {
|
||||
return t.MainTweetID
|
||||
}
|
||||
|
||||
func (app *Application) ensure_tweet(id scraper.TweetID, is_forced bool, is_conversation_required bool) (scraper.Tweet, error) {
|
||||
is_available := false
|
||||
is_needing_scrape := is_forced
|
||||
|
||||
// Check if tweet is already in DB
|
||||
tweet, err := app.Profile.GetTweetById(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, persistence.ErrNotInDB) {
|
||||
is_needing_scrape = true
|
||||
is_available = false
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
is_available = true
|
||||
if !tweet.IsConversationScraped {
|
||||
is_needing_scrape = true
|
||||
}
|
||||
}
|
||||
if is_available && !is_conversation_required { // TODO: get rid of this, just force the fetch in subsequent handlers if needed
|
||||
is_needing_scrape = false
|
||||
}
|
||||
|
||||
if is_needing_scrape && !app.IsScrapingDisabled {
|
||||
trove, err := scraper.GetTweetFullAPIV2(id, 50) // TODO: parameterizable
|
||||
if err == nil {
|
||||
app.Profile.SaveTweetTrove(trove)
|
||||
is_available = true
|
||||
} else {
|
||||
app.ErrorLog.Print(err)
|
||||
// TODO: show error in UI
|
||||
}
|
||||
} else if is_needing_scrape {
|
||||
app.InfoLog.Printf("Would have scraped Tweet: %d", id)
|
||||
}
|
||||
|
||||
if !is_available {
|
||||
return scraper.Tweet{}, ErrNotFound
|
||||
}
|
||||
return tweet, nil
|
||||
}
|
||||
|
||||
func (app *Application) LikeTweet(w http.ResponseWriter, r *http.Request) {
|
||||
tweet := get_tweet_from_context(r.Context())
|
||||
like, err := scraper.LikeTweet(tweet.ID)
|
||||
// "Already Liked This Tweet" is no big deal-- we can just update the UI as if it succeeded
|
||||
if err != nil && !errors.Is(err, scraper.AlreadyLikedThisTweet) {
|
||||
// It's a different error
|
||||
panic(err)
|
||||
}
|
||||
err = app.Profile.SaveLike(like)
|
||||
panic_if(err)
|
||||
tweet.IsLikedByCurrentUser = true
|
||||
|
||||
app.buffered_render_basic_htmx(w, "likes-count", tweet)
|
||||
}
|
||||
func (app *Application) UnlikeTweet(w http.ResponseWriter, r *http.Request) {
|
||||
tweet := get_tweet_from_context(r.Context())
|
||||
err := scraper.UnlikeTweet(tweet.ID)
|
||||
// As above, "Haven't Liked This Tweet" is no big deal-- we can just update the UI as if the request succeeded
|
||||
if err != nil && !errors.Is(err, scraper.HaventLikedThisTweet) {
|
||||
// It's a different error
|
||||
panic(err)
|
||||
}
|
||||
err = app.Profile.DeleteLike(scraper.Like{UserID: app.ActiveUser.ID, TweetID: tweet.ID})
|
||||
panic_if(err)
|
||||
tweet.IsLikedByCurrentUser = false
|
||||
|
||||
app.buffered_render_basic_htmx(w, "likes-count", tweet)
|
||||
}
|
||||
|
||||
func (app *Application) TweetDetail(w http.ResponseWriter, r *http.Request) {
|
||||
app.traceLog.Printf("'TweetDetail' handler (path: %q)", r.URL.Path)
|
||||
|
||||
@ -51,63 +124,21 @@ func (app *Application) TweetDetail(w http.ResponseWriter, r *http.Request) {
|
||||
data := NewTweetDetailData()
|
||||
data.MainTweetID = tweet_id
|
||||
|
||||
is_needing_scrape := (len(parts) > 2 && parts[2] == "scrape")
|
||||
is_available := false
|
||||
is_scrape_required := r.URL.Query().Has("scrape")
|
||||
is_conversation_required := len(parts) <= 2 || (parts[2] != "like" && parts[2] != "unlike")
|
||||
|
||||
// Check if tweet is already in DB
|
||||
tweet, err := app.Profile.GetTweetById(tweet_id)
|
||||
if err != nil {
|
||||
if errors.Is(err, persistence.ErrNotInDB) {
|
||||
is_needing_scrape = true
|
||||
is_available = false
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
is_available = true
|
||||
if !tweet.IsConversationScraped {
|
||||
is_needing_scrape = true
|
||||
}
|
||||
}
|
||||
if is_available && len(parts) > 2 && (parts[2] == "like" || parts[2] == "unlike") {
|
||||
is_needing_scrape = false
|
||||
}
|
||||
|
||||
if is_needing_scrape && !app.IsScrapingDisabled {
|
||||
trove, err := scraper.GetTweetFullAPIV2(tweet_id, 50) // TODO: parameterizable
|
||||
if err == nil {
|
||||
app.Profile.SaveTweetTrove(trove)
|
||||
is_available = true
|
||||
} else {
|
||||
app.ErrorLog.Print(err)
|
||||
// TODO: show error in UI
|
||||
}
|
||||
}
|
||||
|
||||
if !is_available {
|
||||
tweet, err := app.ensure_tweet(tweet_id, is_scrape_required, is_conversation_required)
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
app.error_404(w)
|
||||
return
|
||||
}
|
||||
req_with_tweet := r.WithContext(add_tweet_to_context(r.Context(), tweet))
|
||||
|
||||
if len(parts) > 2 && parts[2] == "like" {
|
||||
like, err := scraper.LikeTweet(tweet.ID)
|
||||
// if err != nil && !errors.Is(err, scraper.AlreadyLikedThisTweet) {}
|
||||
panic_if(err)
|
||||
fmt.Printf("Like: %#v\n", like)
|
||||
err = app.Profile.SaveLike(like)
|
||||
panic_if(err)
|
||||
tweet.IsLikedByCurrentUser = true
|
||||
|
||||
app.buffered_render_basic_htmx(w, "likes-count", tweet)
|
||||
app.LikeTweet(w, req_with_tweet)
|
||||
return
|
||||
} else if len(parts) > 2 && parts[2] == "unlike" {
|
||||
err = scraper.UnlikeTweet(tweet_id)
|
||||
panic_if(err)
|
||||
err = app.Profile.DeleteLike(scraper.Like{UserID: app.ActiveUser.ID, TweetID: tweet.ID})
|
||||
panic_if(err)
|
||||
tweet.IsLikedByCurrentUser = false
|
||||
|
||||
app.buffered_render_basic_htmx(w, "likes-count", tweet)
|
||||
app.UnlikeTweet(w, req_with_tweet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -42,13 +42,14 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(parts) > 1 && parts[len(parts)-1] == "scrape" {
|
||||
if r.URL.Query().Has("scrape") {
|
||||
if app.IsScrapingDisabled {
|
||||
app.InfoLog.Printf("Would have scraped: %s", r.URL.Path)
|
||||
http.Error(w, "Scraping is disabled (are you logged in?)", 401)
|
||||
return
|
||||
}
|
||||
|
||||
if len(parts) == 2 { // Already checked the last part is "scrape"
|
||||
if len(parts) == 1 { // The URL is just the user handle
|
||||
// Run scraper
|
||||
trove, err := scraper.GetUserFeedGraphqlFor(user.ID, 50) // TODO: parameterizable
|
||||
if err != nil {
|
||||
@ -56,7 +57,7 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) {
|
||||
// TOOD: show error in UI
|
||||
}
|
||||
app.Profile.SaveTweetTrove(trove)
|
||||
} else if len(parts) == 3 && parts[1] == "likes" {
|
||||
} else if len(parts) == 2 && parts[1] == "likes" {
|
||||
trove, err := scraper.GetUserLikes(user.ID, 50) // TODO: parameterizable
|
||||
if err != nil {
|
||||
app.ErrorLog.Print(err)
|
||||
|
@ -2,6 +2,7 @@ package webserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
@ -244,3 +245,15 @@ func cursor_to_query_params(c persistence.Cursor) string {
|
||||
result.Set("sort-order", c.SortOrder.String())
|
||||
return result.Encode()
|
||||
}
|
||||
|
||||
func add_tweet_to_context(ctx context.Context, tweet scraper.Tweet) context.Context {
|
||||
return context.WithValue(ctx, "tweet", tweet)
|
||||
}
|
||||
|
||||
func get_tweet_from_context(ctx context.Context) scraper.Tweet {
|
||||
tweet, is_ok := ctx.Value("tweet").(scraper.Tweet)
|
||||
if !is_ok {
|
||||
panic("Tweet not found in context")
|
||||
}
|
||||
return tweet
|
||||
}
|
||||
|
1
internal/webserver/static/icons/refresh.svg
Normal file
1
internal/webserver/static/icons/refresh.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-refresh" viewBox="0 0 24 24"><path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z" /></svg>
|
After Width: | Height: | Size: 306 B |
@ -3,7 +3,7 @@
|
||||
{{$author := (user $main_tweet.UserID)}}
|
||||
<div class="tweet"
|
||||
{{if (not (eq $main_tweet.ID (focused_tweet_id)))}}
|
||||
hx-post="/tweet/{{$main_tweet.ID}}"
|
||||
hx-get="/tweet/{{$main_tweet.ID}}"
|
||||
hx-trigger="click"
|
||||
hx-target="body"
|
||||
hx-swap="outerHTML show:#focused-tweet:top"
|
||||
@ -130,7 +130,7 @@
|
||||
<span>Open on twitter.com</span>
|
||||
</li>
|
||||
</a>
|
||||
<a class="unstyled-link" target="_blank" hx-post="/tweet/{{$main_tweet.ID}}/scrape" hx-target="body">
|
||||
<a class="unstyled-link" target="_blank" hx-get="/tweet/{{$main_tweet.ID}}?scrape" hx-target="body">
|
||||
<li class="quick-link">
|
||||
<img class="svg-icon" src="/static/icons/download.svg" />
|
||||
<span>Re-fetch tweet</span>
|
||||
|
@ -42,30 +42,18 @@
|
||||
<span class="following-count">{{$user.FollowingCount}}</span>
|
||||
</div>
|
||||
|
||||
<div class="dropdown" hx-trigger="click consume">
|
||||
<button class="dropdown-button" title="Options">
|
||||
<img class="svg-icon" src="/static/icons/more.svg" />
|
||||
</button>
|
||||
<ul class="dropdown-items">
|
||||
<a class="unstyled-link" target="_blank" href="https://twitter.com/{{$user.Handle}}">
|
||||
<li class="quick-link">
|
||||
<img class="svg-icon" src="/static/icons/external-link.svg" />
|
||||
<span>Open on twitter.com</span>
|
||||
</li>
|
||||
</a>
|
||||
<a class="unstyled-link" target="_blank" hx-post="/{{$user.Handle}}/scrape" hx-target="body">
|
||||
<div class="XXX">
|
||||
<a class="unstyled-link" title="Refresh" hx-get="?scrape" hx-target="body">
|
||||
<li class="quick-link">
|
||||
<img class="svg-icon" src="/static/icons/download.svg" />
|
||||
<span>Re-fetch user feed</span>
|
||||
<img class="svg-icon" src="/static/icons/refresh.svg" />
|
||||
</li>
|
||||
</a>
|
||||
<a class="unstyled-link" target="_blank" hx-post="/{{$user.Handle}}/likes/scrape" hx-target="body">
|
||||
<li class="quick-link">
|
||||
<img class="svg-icon" src="/static/icons/download.svg" />
|
||||
<span>Re-fetch user likes</span>
|
||||
</li>
|
||||
</a>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -34,7 +34,11 @@ func (api API) LikeTweet(id TweetID) (Like, error) {
|
||||
}
|
||||
if len(result.Errors) > 0 {
|
||||
if strings.Contains(result.Errors[0].Message, "has already favorited tweet") {
|
||||
return Like{}, AlreadyLikedThisTweet
|
||||
return Like{
|
||||
UserID: api.UserID,
|
||||
TweetID: id,
|
||||
SortID: -1,
|
||||
}, AlreadyLikedThisTweet
|
||||
}
|
||||
}
|
||||
if result.Data.FavoriteTweet != "Done" {
|
||||
|
Loading…
x
Reference in New Issue
Block a user