REFACTOR: split Messages handler into functions; add 'is_htmx' helper function
This commit is contained in:
parent
6982b28cb2
commit
4d6407492a
@ -46,7 +46,7 @@ func (app *Application) ListDetailFeed(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil && !errors.Is(err, persistence.ErrEndOfFeed) {
|
if err != nil && !errors.Is(err, persistence.ErrEndOfFeed) {
|
||||||
panic(err)
|
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
|
// It's a Show More request
|
||||||
app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, feed)
|
app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, feed)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package webserver
|
package webserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -18,32 +20,21 @@ type MessageData struct {
|
|||||||
UnreadRoomIDs map[scraper.DMChatRoomID]bool
|
UnreadRoomIDs map[scraper.DMChatRoomID]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *Application) Messages(w http.ResponseWriter, r *http.Request) {
|
func (app *Application) messages_index(w http.ResponseWriter, r *http.Request) {
|
||||||
app.traceLog.Printf("'Messages' handler (path: %q)", r.URL.Path)
|
chat_view_data, global_data := app.get_message_global_data()
|
||||||
|
app.buffered_render_page(w, "tpl/messages.tpl", global_data, chat_view_data)
|
||||||
if app.ActiveUser.ID == 0 {
|
|
||||||
app.error_401(w)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, "/"), "/")
|
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
||||||
room_id := scraper.DMChatRoomID(parts[0])
|
is_sending := len(parts) == 1 && parts[0] == "send"
|
||||||
|
|
||||||
if r.URL.Query().Has("poll") {
|
chat_view_data, global_data := app.get_message_global_data()
|
||||||
// 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}
|
|
||||||
|
|
||||||
if room_id != "" {
|
|
||||||
// First send a message, if applicable
|
// First send a message, if applicable
|
||||||
if len(parts) == 2 && parts[1] == "send" {
|
if is_sending {
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
panic_if(err)
|
panic_if(err)
|
||||||
var message_data struct {
|
var message_data struct {
|
||||||
@ -57,24 +48,27 @@ func (app *Application) Messages(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
chat_view_data.ActiveRoomID = room_id
|
chat_view_data.ActiveRoomID = room_id
|
||||||
chat_view_data.LatestPollingTimestamp = -1
|
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 != "" {
|
if latest_timestamp_str := r.URL.Query().Get("latest_timestamp"); latest_timestamp_str != "" {
|
||||||
var err error
|
var err error
|
||||||
chat_view_data.LatestPollingTimestamp, err = strconv.Atoi(latest_timestamp_str)
|
chat_view_data.LatestPollingTimestamp, err = strconv.Atoi(latest_timestamp_str)
|
||||||
panic_if(err)
|
panic_if(err) // TODO: 400 not 500
|
||||||
}
|
}
|
||||||
if r.URL.Query().Get("scroll_bottom") != "0" {
|
if r.URL.Query().Get("scroll_bottom") != "0" {
|
||||||
chat_view_data.ScrollBottom = true
|
chat_view_data.ScrollBottom = true
|
||||||
}
|
}
|
||||||
chat_contents := app.Profile.GetChatRoomContents(room_id, chat_view_data.LatestPollingTimestamp)
|
chat_contents := app.Profile.GetChatRoomContents(room_id, chat_view_data.LatestPollingTimestamp)
|
||||||
chat_view_data.MergeWith(chat_contents.DMTrove)
|
chat_view_data.MergeWith(chat_contents.DMTrove)
|
||||||
|
chat_view_data.DMChatView.MergeWith(chat_contents.DMTrove)
|
||||||
chat_view_data.MessageIDs = chat_contents.MessageIDs
|
chat_view_data.MessageIDs = chat_contents.MessageIDs
|
||||||
if len(chat_view_data.MessageIDs) > 0 {
|
if len(chat_view_data.MessageIDs) > 0 {
|
||||||
last_message_id := chat_view_data.MessageIDs[len(chat_view_data.MessageIDs)-1]
|
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())
|
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" {
|
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)
|
app.buffered_render_htmx(w, "messages-with-poller", global_data, chat_view_data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -82,3 +76,61 @@ func (app *Application) Messages(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) 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)
|
||||||
|
|
||||||
|
if app.ActiveUser.ID == 0 {
|
||||||
|
app.error_401(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
||||||
|
room_id := scraper.DMChatRoomID(parts[0])
|
||||||
|
|
||||||
|
// Messages index
|
||||||
|
if room_id == "" {
|
||||||
|
app.messages_index(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
@ -139,7 +139,7 @@ func (app *Application) Search(w http.ResponseWriter, r *http.Request) {
|
|||||||
data.SearchText = search_text
|
data.SearchText = search_text
|
||||||
data.SortOrder = c.SortOrder
|
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
|
// It's a Show More request
|
||||||
app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: data.Feed.TweetTrove, SearchText: search_text}, data)
|
app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: data.Feed.TweetTrove, SearchText: search_text}, data)
|
||||||
} else {
|
} else {
|
||||||
|
@ -8,7 +8,7 @@ func (app *Application) NavSidebarPollUpdates(w http.ResponseWriter, r *http.Req
|
|||||||
app.traceLog.Printf("'NavSidebarPollUpdates' handler (path: %q)", r.URL.Path)
|
app.traceLog.Printf("'NavSidebarPollUpdates' handler (path: %q)", r.URL.Path)
|
||||||
|
|
||||||
// Must be an HTMX request, otherwise HTTP 400
|
// 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")
|
app.error_400_with_message(w, "This is an HTMX-only endpoint, not a page")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ func (app *Application) OfflineTimeline(w http.ResponseWriter, r *http.Request)
|
|||||||
panic(err)
|
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
|
// It's a Show More request
|
||||||
app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, feed)
|
app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, feed)
|
||||||
} else {
|
} else {
|
||||||
@ -74,7 +74,7 @@ func (app *Application) Timeline(w http.ResponseWriter, r *http.Request) {
|
|||||||
panic(err)
|
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
|
// It's a Show More request
|
||||||
app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, feed)
|
app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, feed)
|
||||||
} else {
|
} else {
|
||||||
|
@ -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
|
// It's a Show More request
|
||||||
app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, data)
|
app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, data)
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,12 +3,6 @@ package webserver
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
// "fmt"
|
|
||||||
// "net/http"
|
|
||||||
// "net/http/httptest"
|
|
||||||
// "net/url"
|
|
||||||
// "strings"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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].EntityType, ENTITY_TYPE_TEXT)
|
||||||
assert.Equal(entities[0].Contents, s)
|
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.)")
|
||||||
|
}
|
||||||
|
@ -184,3 +184,7 @@ func get_entities(text string) []Entity {
|
|||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func is_htmx(r *http.Request) bool {
|
||||||
|
return r.Header.Get("HX-Request") == "true"
|
||||||
|
}
|
||||||
|
@ -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.)")
|
|
||||||
}
|
|
@ -92,7 +92,7 @@
|
|||||||
|
|
||||||
{{define "chat-view"}}
|
{{define "chat-view"}}
|
||||||
<div id="chat-view">
|
<div id="chat-view">
|
||||||
{{if $.ActiveRoomID}}
|
{{if .ActiveRoomID}}
|
||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
{{ $room := (index $.Rooms $.ActiveRoomID) }}
|
{{ $room := (index $.Rooms $.ActiveRoomID) }}
|
||||||
{{template "chat-profile-image" $room}}
|
{{template "chat-profile-image" $room}}
|
||||||
@ -105,11 +105,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="chat-messages">
|
<div class="chat-messages">
|
||||||
{{if $.ActiveRoomID}}
|
{{if .ActiveRoomID}}
|
||||||
{{template "messages-with-poller" .}}
|
{{template "messages-with-poller" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if $.ActiveRoomID}}
|
{{if .ActiveRoomID}}
|
||||||
<div class="dm-composer">
|
<div class="dm-composer">
|
||||||
<form
|
<form
|
||||||
hx-post="/messages/{{$.ActiveRoomID}}/send"
|
hx-post="/messages/{{$.ActiveRoomID}}/send"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user