From 4d6407492a0de1960695f8f56ab7a674dec49ab9 Mon Sep 17 00:00:00 2001 From: Alessio Date: Sat, 4 May 2024 15:14:51 -0700 Subject: [PATCH] REFACTOR: split Messages handler into functions; add 'is_htmx' helper function --- internal/webserver/handler_lists.go | 2 +- internal/webserver/handler_messages.go | 146 ++++++++++++------ internal/webserver/handler_search.go | 2 +- internal/webserver/handler_sidebar.go | 2 +- internal/webserver/handler_timeline.go | 4 +- internal/webserver/handler_user_feed.go | 2 +- internal/webserver/helpers_test.go | 19 ++- internal/webserver/renderer_helpers.go | 4 + internal/webserver/renderer_helpers_test.go | 20 --- .../tpl/tweet_page_includes/chat_view.tpl | 6 +- 10 files changed, 125 insertions(+), 82 deletions(-) delete mode 100644 internal/webserver/renderer_helpers_test.go diff --git a/internal/webserver/handler_lists.go b/internal/webserver/handler_lists.go index 3f664e0..bc774d4 100644 --- a/internal/webserver/handler_lists.go +++ b/internal/webserver/handler_lists.go @@ -46,7 +46,7 @@ func (app *Application) ListDetailFeed(w http.ResponseWriter, r *http.Request) { if err != nil && !errors.Is(err, persistence.ErrEndOfFeed) { panic(err) } - if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE { + if is_htmx(r) && c.CursorPosition == persistence.CURSOR_MIDDLE { // It's a Show More request app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, feed) } else { diff --git a/internal/webserver/handler_messages.go b/internal/webserver/handler_messages.go index 81e3387..1ae85f5 100644 --- a/internal/webserver/handler_messages.go +++ b/internal/webserver/handler_messages.go @@ -1,7 +1,9 @@ package webserver import ( + "context" "encoding/json" + "fmt" "io" "net/http" "strconv" @@ -18,6 +20,77 @@ type MessageData struct { UnreadRoomIDs map[scraper.DMChatRoomID]bool } +func (app *Application) messages_index(w http.ResponseWriter, r *http.Request) { + chat_view_data, global_data := app.get_message_global_data() + app.buffered_render_page(w, "tpl/messages.tpl", global_data, chat_view_data) +} + +func (app *Application) message_detail(w http.ResponseWriter, r *http.Request) { + room_id := get_room_id_from_context(r.Context()) + + parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") + is_sending := len(parts) == 1 && parts[0] == "send" + + chat_view_data, global_data := app.get_message_global_data() + + // First send a message, if applicable + if is_sending { + body, err := io.ReadAll(r.Body) + panic_if(err) + var message_data struct { + Text string `json:"text"` + } + panic_if(json.Unmarshal(body, &message_data)) + trove := scraper.SendDMMessage(room_id, message_data.Text, 0) + app.Profile.SaveDMTrove(trove, false) + app.buffered_render_htmx(w, "dm-composer", global_data, chat_view_data) // Wipe the chat box + go app.Profile.SaveDMTrove(trove, true) + } + + chat_view_data.ActiveRoomID = room_id + chat_view_data.LatestPollingTimestamp = -1 // TODO: why not 0? If `0` then it won't generate a SQL `where` clause + if latest_timestamp_str := r.URL.Query().Get("latest_timestamp"); latest_timestamp_str != "" { + var err error + chat_view_data.LatestPollingTimestamp, err = strconv.Atoi(latest_timestamp_str) + panic_if(err) // TODO: 400 not 500 + } + if r.URL.Query().Get("scroll_bottom") != "0" { + chat_view_data.ScrollBottom = true + } + chat_contents := app.Profile.GetChatRoomContents(room_id, chat_view_data.LatestPollingTimestamp) + chat_view_data.MergeWith(chat_contents.DMTrove) + chat_view_data.DMChatView.MergeWith(chat_contents.DMTrove) + chat_view_data.MessageIDs = chat_contents.MessageIDs + if len(chat_view_data.MessageIDs) > 0 { + last_message_id := chat_view_data.MessageIDs[len(chat_view_data.MessageIDs)-1] + chat_view_data.LatestPollingTimestamp = int(chat_view_data.Messages[last_message_id].SentAt.UnixMilli()) + } + + if is_htmx(r) { + // Polling for updates and sending a message should add messages at the bottom of the page (newest) + if r.URL.Query().Has("poll") || is_sending { + app.buffered_render_htmx(w, "messages-with-poller", global_data, chat_view_data) + return + } + } + + app.buffered_render_page(w, "tpl/messages.tpl", global_data, chat_view_data) +} + +func (app *Application) get_message_global_data() (MessageData, PageGlobalData) { + // Get message list previews + chat_view_data := MessageData{DMChatView: app.Profile.GetChatRoomsPreview(app.ActiveUser.ID)} + chat_view_data.UnreadRoomIDs = make(map[scraper.DMChatRoomID]bool) + for _, id := range app.Profile.GetUnreadConversations(app.ActiveUser.ID) { + chat_view_data.UnreadRoomIDs[id] = true + } + + // Initialize the Global Data from the chat list data (last message previews, etc) + global_data := PageGlobalData{TweetTrove: chat_view_data.DMChatView.TweetTrove} + + return chat_view_data, global_data +} + func (app *Application) Messages(w http.ResponseWriter, r *http.Request) { app.traceLog.Printf("'Messages' handler (path: %q)", r.URL.Path) @@ -26,59 +99,38 @@ func (app *Application) Messages(w http.ResponseWriter, r *http.Request) { return } - parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") - room_id := scraper.DMChatRoomID(parts[0]) - + // Every 3 seconds, message detail page will send request to scrape, with `?poll` set if r.URL.Query().Has("poll") { // Not run as a goroutine; this call blocks. It's not actually "background" app.background_dm_polling_scrape() } - chat_view_data := MessageData{DMChatView: app.Profile.GetChatRoomsPreview(app.ActiveUser.ID)} // Get message list previews - chat_view_data.UnreadRoomIDs = make(map[scraper.DMChatRoomID]bool) - for _, room_id := range app.Profile.GetUnreadConversations(app.ActiveUser.ID) { - chat_view_data.UnreadRoomIDs[room_id] = true - } - global_data := PageGlobalData{TweetTrove: chat_view_data.DMChatView.TweetTrove} + parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") + room_id := scraper.DMChatRoomID(parts[0]) - if room_id != "" { - // First send a message, if applicable - if len(parts) == 2 && parts[1] == "send" { - body, err := io.ReadAll(r.Body) - panic_if(err) - var message_data struct { - Text string `json:"text"` - } - panic_if(json.Unmarshal(body, &message_data)) - trove := scraper.SendDMMessage(room_id, message_data.Text, 0) - app.Profile.SaveDMTrove(trove, false) - app.buffered_render_htmx(w, "dm-composer", global_data, chat_view_data) // Wipe the chat box - go app.Profile.SaveDMTrove(trove, true) - } - - chat_view_data.ActiveRoomID = room_id - chat_view_data.LatestPollingTimestamp = -1 - if latest_timestamp_str := r.URL.Query().Get("latest_timestamp"); latest_timestamp_str != "" { - var err error - chat_view_data.LatestPollingTimestamp, err = strconv.Atoi(latest_timestamp_str) - panic_if(err) - } - if r.URL.Query().Get("scroll_bottom") != "0" { - chat_view_data.ScrollBottom = true - } - chat_contents := app.Profile.GetChatRoomContents(room_id, chat_view_data.LatestPollingTimestamp) - chat_view_data.MergeWith(chat_contents.DMTrove) - chat_view_data.MessageIDs = chat_contents.MessageIDs - if len(chat_view_data.MessageIDs) > 0 { - last_message_id := chat_view_data.MessageIDs[len(chat_view_data.MessageIDs)-1] - chat_view_data.LatestPollingTimestamp = int(chat_view_data.Messages[last_message_id].SentAt.UnixMilli()) - } - - if r.URL.Query().Has("poll") || len(parts) == 2 && parts[1] == "send" { - app.buffered_render_htmx(w, "messages-with-poller", global_data, chat_view_data) - return - } + // Messages index + if room_id == "" { + app.messages_index(w, r) + return } - app.buffered_render_page(w, "tpl/messages.tpl", global_data, chat_view_data) + // Message detail + http.StripPrefix( + fmt.Sprintf("/%s", room_id), + http.HandlerFunc(app.message_detail), + ).ServeHTTP(w, r.WithContext(add_room_id_to_context(r.Context(), room_id))) +} + +const ROOM_ID_KEY = key("room_id") // type `key` is defined in "handler_tweet_detail" + +func add_room_id_to_context(ctx context.Context, room_id scraper.DMChatRoomID) context.Context { + return context.WithValue(ctx, ROOM_ID_KEY, room_id) +} + +func get_room_id_from_context(ctx context.Context) scraper.DMChatRoomID { + room_id, is_ok := ctx.Value(ROOM_ID_KEY).(scraper.DMChatRoomID) + if !is_ok { + panic("room_id not found in context") + } + return room_id } diff --git a/internal/webserver/handler_search.go b/internal/webserver/handler_search.go index 0207893..ad000ea 100644 --- a/internal/webserver/handler_search.go +++ b/internal/webserver/handler_search.go @@ -139,7 +139,7 @@ func (app *Application) Search(w http.ResponseWriter, r *http.Request) { data.SearchText = search_text data.SortOrder = c.SortOrder - if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE { + if is_htmx(r) && c.CursorPosition == persistence.CURSOR_MIDDLE { // It's a Show More request app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: data.Feed.TweetTrove, SearchText: search_text}, data) } else { diff --git a/internal/webserver/handler_sidebar.go b/internal/webserver/handler_sidebar.go index 913b16d..47c0e9e 100644 --- a/internal/webserver/handler_sidebar.go +++ b/internal/webserver/handler_sidebar.go @@ -8,7 +8,7 @@ func (app *Application) NavSidebarPollUpdates(w http.ResponseWriter, r *http.Req app.traceLog.Printf("'NavSidebarPollUpdates' handler (path: %q)", r.URL.Path) // Must be an HTMX request, otherwise HTTP 400 - if r.Header.Get("HX-Request") != "true" { + if !is_htmx(r) { app.error_400_with_message(w, "This is an HTMX-only endpoint, not a page") return } diff --git a/internal/webserver/handler_timeline.go b/internal/webserver/handler_timeline.go index 0dbf73d..0910d9f 100644 --- a/internal/webserver/handler_timeline.go +++ b/internal/webserver/handler_timeline.go @@ -29,7 +29,7 @@ func (app *Application) OfflineTimeline(w http.ResponseWriter, r *http.Request) panic(err) } - if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE { + if is_htmx(r) && c.CursorPosition == persistence.CURSOR_MIDDLE { // It's a Show More request app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, feed) } else { @@ -74,7 +74,7 @@ func (app *Application) Timeline(w http.ResponseWriter, r *http.Request) { panic(err) } - if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE { + if is_htmx(r) && c.CursorPosition == persistence.CURSOR_MIDDLE { // It's a Show More request app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, feed) } else { diff --git a/internal/webserver/handler_user_feed.go b/internal/webserver/handler_user_feed.go index 1e4f9e5..c94f893 100644 --- a/internal/webserver/handler_user_feed.go +++ b/internal/webserver/handler_user_feed.go @@ -123,7 +123,7 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) { } } - if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE { + if is_htmx(r) && c.CursorPosition == persistence.CURSOR_MIDDLE { // It's a Show More request app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, data) } else { diff --git a/internal/webserver/helpers_test.go b/internal/webserver/helpers_test.go index b68ac52..8f7ef8b 100644 --- a/internal/webserver/helpers_test.go +++ b/internal/webserver/helpers_test.go @@ -3,12 +3,6 @@ package webserver import ( "testing" - // "fmt" - // "net/http" - // "net/http/httptest" - // "net/url" - // "strings" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -56,3 +50,16 @@ func TestGetEntitiesNoMatchEmail(t *testing.T) { assert.Equal(entities[0].EntityType, ENTITY_TYPE_TEXT) assert.Equal(entities[0].Contents, s) } + +func TestEntitiesWithParentheses(t *testing.T) { + assert := assert.New(t) + + entities := get_entities("Companies are looking for ways to reduce costs (@BowTiedBull has said this), through process automation.)") + assert.Len(entities, 3) + assert.Equal(entities[0].EntityType, ENTITY_TYPE_TEXT) + assert.Equal(entities[0].Contents, "Companies are looking for ways to reduce costs (") + assert.Equal(entities[1].EntityType, ENTITY_TYPE_MENTION) + assert.Equal(entities[1].Contents, "BowTiedBull") + assert.Equal(entities[2].EntityType, ENTITY_TYPE_TEXT) + assert.Equal(entities[2].Contents, " has said this), through process automation.)") +} diff --git a/internal/webserver/renderer_helpers.go b/internal/webserver/renderer_helpers.go index 49a4ae4..55c57f8 100644 --- a/internal/webserver/renderer_helpers.go +++ b/internal/webserver/renderer_helpers.go @@ -184,3 +184,7 @@ func get_entities(text string) []Entity { return ret } + +func is_htmx(r *http.Request) bool { + return r.Header.Get("HX-Request") == "true" +} diff --git a/internal/webserver/renderer_helpers_test.go b/internal/webserver/renderer_helpers_test.go deleted file mode 100644 index 0903c6e..0000000 --- a/internal/webserver/renderer_helpers_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package webserver - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestEntitiesWithParentheses(t *testing.T) { - assert := assert.New(t) - - entities := get_entities("Companies are looking for ways to reduce costs (@BowTiedBull has said this), through process automation.)") - assert.Len(entities, 3) - assert.Equal(entities[0].EntityType, ENTITY_TYPE_TEXT) - assert.Equal(entities[0].Contents, "Companies are looking for ways to reduce costs (") - assert.Equal(entities[1].EntityType, ENTITY_TYPE_MENTION) - assert.Equal(entities[1].Contents, "BowTiedBull") - assert.Equal(entities[2].EntityType, ENTITY_TYPE_TEXT) - assert.Equal(entities[2].Contents, " has said this), through process automation.)") -} diff --git a/internal/webserver/tpl/tweet_page_includes/chat_view.tpl b/internal/webserver/tpl/tweet_page_includes/chat_view.tpl index aba7d2c..04041c1 100644 --- a/internal/webserver/tpl/tweet_page_includes/chat_view.tpl +++ b/internal/webserver/tpl/tweet_page_includes/chat_view.tpl @@ -92,7 +92,7 @@ {{define "chat-view"}}
- {{if $.ActiveRoomID}} + {{if .ActiveRoomID}}
{{ $room := (index $.Rooms $.ActiveRoomID) }} {{template "chat-profile-image" $room}} @@ -105,11 +105,11 @@
{{end}}
- {{if $.ActiveRoomID}} + {{if .ActiveRoomID}} {{template "messages-with-poller" .}} {{end}}
- {{if $.ActiveRoomID}} + {{if .ActiveRoomID}}