diff --git a/internal/webserver/handler_timeline.go b/internal/webserver/handler_timeline.go index 9b9d302..0dbf73d 100644 --- a/internal/webserver/handler_timeline.go +++ b/internal/webserver/handler_timeline.go @@ -3,11 +3,18 @@ package webserver import ( "errors" "net/http" + "strings" "gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence" + "gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper" ) -func (app *Application) Timeline(w http.ResponseWriter, r *http.Request) { +type TimelineData struct { + persistence.Feed + ActiveTab string +} + +func (app *Application) OfflineTimeline(w http.ResponseWriter, r *http.Request) { app.traceLog.Printf("'Timeline' handler (path: %q)", r.URL.Path) c := persistence.NewTimelineCursor() @@ -26,6 +33,56 @@ func (app *Application) Timeline(w http.ResponseWriter, r *http.Request) { // It's a Show More request app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, feed) } else { - app.buffered_render_page(w, "tpl/offline_timeline.tpl", PageGlobalData{TweetTrove: feed.TweetTrove}, feed) + app.buffered_render_page( + w, + "tpl/offline_timeline.tpl", + PageGlobalData{TweetTrove: feed.TweetTrove}, + TimelineData{Feed: feed, ActiveTab: "Offline"}, + ) + } +} + +func (app *Application) Timeline(w http.ResponseWriter, r *http.Request) { + app.traceLog.Printf("'Timeline' handler (path: %q)", r.URL.Path) + + parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") + if len(parts) > 1 && parts[1] == "offline" { + app.OfflineTimeline(w, r) + return + } + + c := persistence.Cursor{ + Keywords: []string{}, + ToUserHandles: []scraper.UserHandle{}, + SinceTimestamp: scraper.TimestampFromUnix(0), + UntilTimestamp: scraper.TimestampFromUnix(0), + CursorPosition: persistence.CURSOR_START, + CursorValue: 0, + SortOrder: persistence.SORT_ORDER_NEWEST, + PageSize: 50, + + FollowedByUserHandle: app.ActiveUser.Handle, + } + err := parse_cursor_value(&c, r) + if err != nil { + app.error_400_with_message(w, "invalid cursor (must be a number)") + return + } + + feed, err := app.Profile.NextPage(c, app.ActiveUser.ID) + if err != nil && !errors.Is(err, persistence.ErrEndOfFeed) { + panic(err) + } + + if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE { + // It's a Show More request + app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, feed) + } else { + app.buffered_render_page( + w, + "tpl/offline_timeline.tpl", + PageGlobalData{TweetTrove: feed.TweetTrove}, + TimelineData{Feed: feed, ActiveTab: "User feed"}, + ) } } diff --git a/internal/webserver/server_test.go b/internal/webserver/server_test.go index ea48fa3..4711382 100644 --- a/internal/webserver/server_test.go +++ b/internal/webserver/server_test.go @@ -215,7 +215,7 @@ func TestTimeline(t *testing.T) { assert := assert.New(t) require := require.New(t) - resp := do_request(httptest.NewRequest("GET", "/timeline", nil)) + resp := do_request(httptest.NewRequest("GET", "/timeline/offline", nil)) require.Equal(resp.StatusCode, 200) root, err := html.Parse(resp.Body) @@ -231,7 +231,7 @@ func TestTimelineWithCursor(t *testing.T) { assert := assert.New(t) require := require.New(t) - resp := do_request(httptest.NewRequest("GET", "/timeline?cursor=1631935701000", nil)) + resp := do_request(httptest.NewRequest("GET", "/timeline/offline?cursor=1631935701000", nil)) require.Equal(resp.StatusCode, 200) root, err := html.Parse(resp.Body) @@ -247,10 +247,34 @@ func TestTimelineWithCursorBadNumber(t *testing.T) { require := require.New(t) // With a cursor but it sucks - resp := do_request(httptest.NewRequest("GET", "/timeline?cursor=asdf", nil)) + resp := do_request(httptest.NewRequest("GET", "/timeline/offline?cursor=asdf", nil)) require.Equal(resp.StatusCode, 400) } +func TestUserFeedTimeline(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Boilerplate for setting an active user + app := webserver.NewApp(profile) + app.IsScrapingDisabled = true + app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login + + // Chat list + recorder := httptest.NewRecorder() + app.ServeHTTP(recorder, httptest.NewRequest("GET", "/timeline", nil)) + resp := recorder.Result() + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + title_node := cascadia.Query(root, selector("title")) + assert.Equal(title_node.FirstChild.Data, "Timeline | Offline Twitter") + + tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) + assert.Len(tweet_nodes, 1) +} + // Search page // ----------- diff --git a/internal/webserver/tpl/offline_timeline.tpl b/internal/webserver/tpl/offline_timeline.tpl index 93a6a9a..b27b4ad 100644 --- a/internal/webserver/tpl/offline_timeline.tpl +++ b/internal/webserver/tpl/offline_timeline.tpl @@ -1,7 +1,18 @@ {{define "title"}}Timeline{{end}} {{define "main"}} +
+
+ + User feed + + + Offline timeline + +
+
+
- {{template "timeline" .}} + {{template "timeline" .Feed}}
{{end}} diff --git a/pkg/persistence/compound_ssf_queries.go b/pkg/persistence/compound_ssf_queries.go index e65c26a..c6bbb05 100644 --- a/pkg/persistence/compound_ssf_queries.go +++ b/pkg/persistence/compound_ssf_queries.go @@ -135,6 +135,7 @@ type Cursor struct { ToUserHandles []scraper.UserHandle // In reply to these users LikedByUserHandle scraper.UserHandle // Liked by this user ListID scraper.ListID // Either tweeted or retweeted by users from this List + FollowedByUserHandle scraper.UserHandle // Either tweeted or retweeted by users followed by this user SinceTimestamp scraper.Timestamp UntilTimestamp scraper.Timestamp FilterLinks Filter @@ -395,6 +396,11 @@ func (p Profile) NextPage(c Cursor, current_user_id scraper.UserID) (Feed, error where_clauses = append(where_clauses, "by_user_id in (select user_id from list_users where list_id = ?)") bind_values = append(bind_values, c.ListID) } + if c.FollowedByUserHandle != "" { + where_clauses = append(where_clauses, + "by_user_id in (select followee_id from follows where follower_id = (select id from users where handle like ?))") + bind_values = append(bind_values, c.FollowedByUserHandle) + } // Since and until timestamps if c.SinceTimestamp.Unix() != 0 {