REFACTOR: move /messages/<id>/send handler into its own function

This commit is contained in:
Alessio 2024-05-08 21:59:27 -07:00
parent 5cbf96f379
commit 73c89f70fb
3 changed files with 81 additions and 65 deletions

View File

@ -25,6 +25,21 @@ func (app *Application) messages_index(w http.ResponseWriter, r *http.Request) {
app.buffered_render_page(w, "tpl/messages.tpl", global_data, chat_view_data) app.buffered_render_page(w, "tpl/messages.tpl", global_data, chat_view_data)
} }
func (app *Application) message_send(w http.ResponseWriter, r *http.Request) {
room_id := get_room_id_from_context(r.Context())
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)
go app.Profile.SaveDMTrove(trove, true)
}
func (app *Application) message_detail(w http.ResponseWriter, r *http.Request) { func (app *Application) message_detail(w http.ResponseWriter, r *http.Request) {
room_id := get_room_id_from_context(r.Context()) room_id := get_room_id_from_context(r.Context())
@ -35,25 +50,13 @@ func (app *Application) message_detail(w http.ResponseWriter, r *http.Request) {
// First send a message, if applicable // First send a message, if applicable
if is_sending { if is_sending {
body, err := io.ReadAll(r.Body) app.message_send(w, r)
panic_if(err)
var message_data struct {
Text string `json:"text"`
LatestPollingTimestamp int `json:"latest_polling_timestamp,string"`
}
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 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.LatestPollingTimestamp = message_data.LatestPollingTimestamp // We're going to return some messages
} else {
chat_view_data.LatestPollingTimestamp = -1 // TODO: why not 0? If `0` then it won't generate a SQL `where` clause
} }
// TODO: Why are the input (parsed out of the HTTP request) and output (computed from the messsages // `LatestPollingTimestamp` sort of passes-through the function; if we're not updating it, it
// fetched and printed into the HTTP response) using the same variable in `chat_view_data`? // goes out the same it came in. Hence, using a single variable for it
chat_view_data.LatestPollingTimestamp = 0
chat_view_data.ActiveRoomID = room_id chat_view_data.ActiveRoomID = room_id
if latest_timestamp_str := r.URL.Query().Get("latest_timestamp"); latest_timestamp_str != "" { if latest_timestamp_str := r.URL.Query().Get("latest_timestamp"); latest_timestamp_str != "" {
var err error var err error
@ -84,13 +87,6 @@ func (app *Application) message_detail(w http.ResponseWriter, r *http.Request) {
// Polling for updates and sending a message should add messages at the bottom of the page (newest) // 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 { if r.URL.Query().Has("poll") || is_sending {
app.buffered_render_htmx(w, "messages-with-poller", global_data, chat_view_data) app.buffered_render_htmx(w, "messages-with-poller", global_data, chat_view_data)
// OOB-swap the composer polling timestamp field since it may have updated
app.buffered_render_htmx(w, "composer-polling-timestamp-field", global_data, map[string]interface{}{
"LatestPollingTimestamp": chat_view_data.LatestPollingTimestamp,
"oob": true,
})
return return
} }

View File

@ -756,12 +756,17 @@ func TestMessagesRoom(t *testing.T) {
assert.Len(cascadia.QueryAll(root, selector("#chat-view .dm-message")), 5) assert.Len(cascadia.QueryAll(root, selector("#chat-view .dm-message")), 5)
// Should have the poller at the bottom // Should have the poller at the bottom
node := cascadia.Query(root, selector("#new-messages-poller")) poller := cascadia.Query(root, selector("#new-messages-poller"))
assert.NotNil(node) assert.NotNil(poller)
assert.Contains(node.Attr, html.Attribute{ assert.Contains(poller.Attr, html.Attribute{Key: "hx-get", Val: "/messages/1488963321701171204-1178839081222115328"})
Key: "hx-get", assert.Contains(
Val: "/messages/1488963321701171204-1178839081222115328?poll&latest_timestamp=1686025129144&scroll_bottom=1", cascadia.Query(poller, selector("input[name='scroll_bottom']")).Attr,
}) html.Attribute{Key: "value", Val: "1"},
)
assert.Contains(
cascadia.Query(poller, selector("input[name='latest_timestamp']")).Attr,
html.Attribute{Key: "value", Val: "1686025129144"},
)
} }
// Loading the page since a given message // Loading the page since a given message
@ -785,12 +790,17 @@ func TestMessagesRoomPollForUpdates(t *testing.T) {
assert.Len(cascadia.QueryAll(root, selector(".dm-message")), 3) assert.Len(cascadia.QueryAll(root, selector(".dm-message")), 3)
// Should have the poller at the bottom // Should have the poller at the bottom
node := cascadia.Query(root, selector("#new-messages-poller")) poller := cascadia.Query(root, selector("#new-messages-poller"))
assert.NotNil(node) assert.NotNil(poller)
assert.Contains(node.Attr, html.Attribute{ assert.Contains(poller.Attr, html.Attribute{Key: "hx-get", Val: "/messages/1488963321701171204-1178839081222115328"})
Key: "hx-get", assert.Contains(
Val: "/messages/1488963321701171204-1178839081222115328?poll&latest_timestamp=1686025129144&scroll_bottom=1", cascadia.Query(poller, selector("input[name='scroll_bottom']")).Attr,
}) html.Attribute{Key: "value", Val: "1"},
)
assert.Contains(
cascadia.Query(poller, selector("input[name='latest_timestamp']")).Attr,
html.Attribute{Key: "value", Val: "1686025129144"},
)
} }
// Loading the page since latest message (no updates) // Loading the page since latest message (no updates)
@ -814,12 +824,17 @@ func TestMessagesRoomPollForUpdatesEmptyResult(t *testing.T) {
assert.Len(cascadia.QueryAll(root, selector(".dm-message")), 0) assert.Len(cascadia.QueryAll(root, selector(".dm-message")), 0)
// Should have the poller at the bottom, with the same value as previously // Should have the poller at the bottom, with the same value as previously
node := cascadia.Query(root, selector("#new-messages-poller")) poller := cascadia.Query(root, selector("#new-messages-poller"))
assert.NotNil(node) assert.NotNil(poller)
assert.Contains(node.Attr, html.Attribute{ assert.Contains(poller.Attr, html.Attribute{Key: "hx-get", Val: "/messages/1488963321701171204-1178839081222115328"})
Key: "hx-get", assert.Contains(
Val: "/messages/1488963321701171204-1178839081222115328?poll&latest_timestamp=1686025129144&scroll_bottom=1", cascadia.Query(poller, selector("input[name='scroll_bottom']")).Attr,
}) html.Attribute{Key: "value", Val: "1"},
)
assert.Contains(
cascadia.Query(poller, selector("input[name='latest_timestamp']")).Attr,
html.Attribute{Key: "value", Val: "1686025129144"},
)
} }
// Scroll back in the messages // Scroll back in the messages

View File

@ -86,23 +86,33 @@
{{define "messages-with-poller"}} {{define "messages-with-poller"}}
{{template "messages" .}} {{template "messages" .}}
<div id="new-messages-poller" <form id="new-messages-poller"
hx-swap="outerHTML {{if $.ScrollBottom}}scroll:.chat-messages:bottom{{end}}" hx-swap="outerHTML {{if $.ScrollBottom}}scroll:.chat-messages:bottom{{end}}"
hx-trigger="load delay:3s" hx-trigger="load delay:3s"
hx-get="/messages/{{$.ActiveRoomID}}?poll&latest_timestamp={{$.LatestPollingTimestamp}}&scroll_bottom={{if $.ScrollBottom}}1{{else}}0{{end}}" hx-get="/messages/{{$.ActiveRoomID}}"
></div>
{{end}}
{{define "composer-polling-timestamp-field"}}
<input id="composerPollingTimestamp"
type="hidden"
name="latest_polling_timestamp"
value="{{.LatestPollingTimestamp}}"
{{if .oob}}
{{/* See comment about `oob` in the "dm-composer" template. Here we pass a parameter to say oob or not */}}
hx-swap-oob="true"
{{end}}
> >
<input type="hidden" name="poll">
<input type="hidden" name="latest_timestamp" value="{{$.LatestPollingTimestamp}}">
<input type="hidden" name="scroll_bottom" value="{{if $.ScrollBottom}}1{{else}}0{{end}}">
</form>
<script>
/**
* The poller's timestamp will be updated by HTMX, but the POST URL for /send needs updating too
*/
(function() {
const composer_form = document.querySelector(".dm-composer form");
if (composer_form === null) {
// Initial page load; composer isn't rendered yet
return;
}
const [path, qs] = composer_form.attributes["hx-post"].value.split("?");
const params = new URLSearchParams(qs);
params.set("latest_timestamp", "{{.LatestPollingTimestamp}}");
composer_form.setAttribute("hx-post", [path, params.toString()].join("?"));
htmx.process(composer_form); // Manually enable HTMX on the manually-added node
})();
</script>
{{end}} {{end}}
{{define "chat-view"}} {{define "chat-view"}}
@ -133,14 +143,13 @@
{{if .ActiveRoomID}} {{if .ActiveRoomID}}
<div class="dm-composer"> <div class="dm-composer">
<form <form
hx-post="/messages/{{.ActiveRoomID}}/send" hx-post="/messages/{{.ActiveRoomID}}/send?latest_timestamp={{.LatestPollingTimestamp}}"
hx-target="#new-messages-poller" hx-target="#new-messages-poller"
hx-swap="outerHTML scroll:.chat-messages:bottom" hx-swap="outerHTML scroll:.chat-messages:bottom"
hx-ext="json-enc" hx-ext="json-enc"
> >
{{template "dm-composer"}} {{template "dm-composer"}}
<input id="realInput" type="hidden" name="text" value="" /> <input id="realInput" type="hidden" name="text" value="" />
{{template "composer-polling-timestamp-field" (dict "LatestPollingTimestamp" .LatestPollingTimestamp "oob" false)}}
<input type="submit" /> <input type="submit" />
</form> </form>
</div> </div>
@ -183,25 +192,21 @@
// Disable auto-scroll-bottom on new message loads if the user has scrolled up // Disable auto-scroll-bottom on new message loads if the user has scrolled up
chat_messages.addEventListener('scroll', function() { chat_messages.addEventListener('scroll', function() {
const _node = document.querySelector("#new-messages-poller"); const _node = document.querySelector("#new-messages-poller");
const node = _node.cloneNode() const node = _node.cloneNode(true)
_node.remove(); // Removing and re-inserting the element cancels the HTMX polling, otherwise it will use the old values _node.remove(); // Removing and re-inserting the element cancels the HTMX polling, otherwise it will use the old values
const scroll_bottom_input = node.querySelector("input[name='scroll_bottom']")
const scrollPosition = chat_messages.scrollTop; const scrollPosition = chat_messages.scrollTop;
var bottomOfElement = chat_messages.scrollHeight - chat_messages.clientHeight; var bottomOfElement = chat_messages.scrollHeight - chat_messages.clientHeight;
var [path, qs] = node.attributes["hx-get"].value.split("?")
var params = new URLSearchParams(qs)
if (scrollPosition === bottomOfElement) { if (scrollPosition === bottomOfElement) {
// At bottom; new messages should be scrolled into view // At bottom; new messages should be scrolled into view
node.setAttribute("hx-swap", "outerHTML scroll:.chat-messages:bottom"); node.setAttribute("hx-swap", "outerHTML scroll:.chat-messages:bottom");
params.set("scroll_bottom", "1") scroll_bottom_input.value = 1;
node.setAttribute("hx-get", [path, params.toString()].join("?"))
} else { } else {
// User has scrolled up; disable auto-scrolling when new messages arrive // User has scrolled up; disable auto-scrolling when new messages arrive
node.setAttribute("hx-swap", "outerHTML"); node.setAttribute("hx-swap", "outerHTML");
params.set("scroll_bottom", "0") scroll_bottom_input.value = 0;
node.setAttribute("hx-get", [path, params.toString()].join("?"))
} }
chat_messages.appendChild(node); chat_messages.appendChild(node);