From 72110e6558fa48d4c1f3c3e5f22054fc41e69e2a Mon Sep 17 00:00:00 2001 From: Alessio Date: Sun, 20 Aug 2023 15:02:47 -0300 Subject: [PATCH] Enable searching through the search bar --- internal/webserver/handler_search.go | 58 ++++++++++++++++++++++++ internal/webserver/server.go | 2 + internal/webserver/server_test.go | 52 ++++++++++++++++++++- internal/webserver/static/styles.css | 4 ++ internal/webserver/tpl/includes/base.tpl | 4 +- internal/webserver/tpl/search.tpl | 7 +++ 6 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 internal/webserver/handler_search.go create mode 100644 internal/webserver/tpl/search.tpl diff --git a/internal/webserver/handler_search.go b/internal/webserver/handler_search.go new file mode 100644 index 0000000..7a0005f --- /dev/null +++ b/internal/webserver/handler_search.go @@ -0,0 +1,58 @@ +package webserver + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + "gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence" +) + +func (app *Application) Search(w http.ResponseWriter, r *http.Request) { + app.traceLog.Printf("'Search' handler (path: %q)", r.URL.Path) + + search_text := strings.Trim(r.URL.Path, "/") + if search_text == "" { + // Redirect GET param "q" to use a URL param instead + search_text = r.URL.Query().Get("q") + if search_text == "" { + app.error_400_with_message(w, "Empty search query") + return + // TODO: return an actual page + } + http.Redirect(w, r, fmt.Sprintf("/search/%s", url.PathEscape(search_text)), 302) + return + } + + c, err := persistence.NewCursorFromSearchQuery(search_text) + if err != nil { + app.error_400_with_message(w, err.Error()) + return + // TODO: return actual page + } + 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) + if err != nil { + if errors.Is(err, persistence.ErrEndOfFeed) { + // TODO + } else { + panic(err) + } + } + + data := UserProfileData{Feed: feed} // TODO: wrong struct + + if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE { + // It's a Show More request + app.buffered_render_tweet_htmx(w, "timeline", data) + } else { + app.buffered_render_tweet_page(w, "tpl/search.tpl", data) + } +} diff --git a/internal/webserver/server.go b/internal/webserver/server.go index 1f0daa4..6c3c44b 100644 --- a/internal/webserver/server.go +++ b/internal/webserver/server.go @@ -118,6 +118,8 @@ func (app *Application) ServeHTTP(w http.ResponseWriter, r *http.Request) { app.UserFollow(w, r) case "unfollow": app.UserUnfollow(w, r) + case "search": + http.StripPrefix("/search", http.HandlerFunc(app.Search)).ServeHTTP(w, r) default: app.UserFeed(w, r) } diff --git a/internal/webserver/server_test.go b/internal/webserver/server_test.go index b8c4a91..2537ed8 100644 --- a/internal/webserver/server_test.go +++ b/internal/webserver/server_test.go @@ -1,10 +1,13 @@ package webserver_test import ( + "testing" + + "fmt" "net/http" "net/http/httptest" + "net/url" "strings" - "testing" "github.com/andybalholm/cascadia" "github.com/stretchr/testify/assert" @@ -158,6 +161,53 @@ func TestTimelineWithCursorBadNumber(t *testing.T) { require.Equal(resp.StatusCode, 400) } +// Search page +// ----------- + +func TestSearchQueryStringRedirect(t *testing.T) { + assert := assert.New(t) + + // With a cursor but it sucks + resp := do_request(httptest.NewRequest("GET", "/search?q=asdf", nil)) + assert.Equal(resp.StatusCode, 302) + assert.Equal(resp.Header.Get("Location"), "/search/asdf") +} + +func TestSearch(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", fmt.Sprintf("/search/%s", url.PathEscape("to:spacex to:covfefeanon")), nil)) + 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, "Offline Twitter | Search") + + tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) + assert.Len(tweet_nodes, 1) +} + +func TestSearchWithCursor(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // First, without the cursor + resp := do_request(httptest.NewRequest("GET", "/search/who%20are", nil)) + require.Equal(resp.StatusCode, 200) + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".timeline > .tweet")), 3) + + // Add a cursor with the 1st tweet's posted_at time + resp = do_request(httptest.NewRequest("GET", "/search/who%20are?cursor=1628979529", nil)) + require.Equal(resp.StatusCode, 200) + root, err = html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".timeline > .tweet")), 2) +} + // Tweet Detail page // ----------------- diff --git a/internal/webserver/static/styles.css b/internal/webserver/static/styles.css index f01bd87..2e811aa 100644 --- a/internal/webserver/static/styles.css +++ b/internal/webserver/static/styles.css @@ -381,6 +381,10 @@ svg { left: 50%; transform: translate(-50%, -50%); } +.top-bar form { + flex-grow: 1; + display: flex; +} .search-bar { flex-grow: 1; } diff --git a/internal/webserver/tpl/includes/base.tpl b/internal/webserver/tpl/includes/base.tpl index 8bd3f19..8848d9b 100644 --- a/internal/webserver/tpl/includes/base.tpl +++ b/internal/webserver/tpl/includes/base.tpl @@ -14,7 +14,9 @@ - +
+ +
{{template "nav-sidebar"}}
diff --git a/internal/webserver/tpl/search.tpl b/internal/webserver/tpl/search.tpl new file mode 100644 index 0000000..7cff985 --- /dev/null +++ b/internal/webserver/tpl/search.tpl @@ -0,0 +1,7 @@ +{{define "title"}}Search{{end}} + +{{define "main"}} +
+ {{template "timeline" .}} +
+{{end}}