Add polling for new messages in a chat room while on the page

This commit is contained in:
Alessio 2023-12-24 19:28:15 -06:00
parent d80a2bd5b1
commit bd90b1c528
5 changed files with 189 additions and 73 deletions

View File

@ -5,12 +5,16 @@ import (
"io"
"net/http"
"strings"
"strconv"
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence"
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
)
type MessageData persistence.DMChatView
type MessageData struct {
persistence.DMChatView
LatestPollingTimestamp int
}
func (t MessageData) Tweet(id scraper.TweetID) scraper.Tweet {
return t.Tweets[id]
@ -38,25 +42,47 @@ func (app *Application) Messages(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
room_id := scraper.DMChatRoomID(parts[0])
if len(parts) == 2 && parts[1] == "send" {
body, err := io.ReadAll(r.Body)
panic_if(err)
var message_data struct {
Text string `json:"text"`
if r.URL.Query().Has("scrape") {
// TODO: where is this going to be used?
app.background_dm_polling_scrape()
}
chat_view_data := MessageData{DMChatView: app.Profile.GetChatRoomsPreview(app.ActiveUser.ID)} // Get message list previews
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)
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)
}
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.Unix())
}
if r.URL.Query().Has("poll") {
app.buffered_render_tweet_htmx(w, "messages-with-poller", chat_view_data)
return
}
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)
}
chat_view := app.Profile.GetChatRoomsPreview(app.ActiveUser.ID)
if strings.Trim(r.URL.Path, "/") != "" {
chat_view.ActiveRoomID = room_id
chat_contents := app.Profile.GetChatRoomContents(room_id)
chat_view.MergeWith(chat_contents.DMTrove)
chat_view.MessageIDs = chat_contents.MessageIDs
}
app.buffered_render_tweet_page(w, "tpl/messages.tpl", MessageData(chat_view))
app.buffered_render_tweet_page(w, "tpl/messages.tpl", chat_view_data)
}

View File

@ -608,4 +608,30 @@ func TestMessagesRoom(t *testing.T) {
require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".chat-list .chat")), 2) // Chat list still renders
assert.Len(cascadia.QueryAll(root, selector("#chat-view .dm-message-and-reacts-container")), 5)
// Should have the poller at the bottom
node := cascadia.Query(root, selector("#new-messages-poller"))
assert.NotNil(node)
assert.Contains(node.Attr, html.Attribute{Key: "hx-get", Val: "/messages/1488963321701171204-1178839081222115328?poll&latest_timestamp=1686025129144"})
}
// Loading the page since
func TestMessagesRoomPollForUpdates(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 detail
recorder := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/messages/1488963321701171204-1178839081222115328?poll&latest_timestamp=1686025129141", nil)
req.Header.Set("HX-Request", "true")
app.ServeHTTP(recorder, req)
resp := recorder.Result()
root, err := html.Parse(resp.Body)
require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".dm-message-and-reacts-container")), 3)
}

View File

@ -1,57 +1,69 @@
{{define "messages-with-poller"}}
{{range .MessageIDs}}
{{$message := (index $.DMTrove.Messages .)}}
{{$user := (user $message.SenderID)}}
{{$is_us := (eq $message.SenderID (active_user).ID)}}
<div class="dm-message-and-reacts-container {{if $is_us}} our-message {{end}}">
<div class="dm-message-container">
<div class="sender-profile-image-container">
<a class="unstyled-link" href="/{{$user.Handle}}">
{{if $user.IsContentDownloaded}}
<img class="profile-image" src="/content/{{$user.GetProfileImageLocalPath}}" />
{{else}}
<img class="profile-image" src="{{$user.ProfileImageUrl}}" />
{{end}}
</a>
</div>
<div class="dm-message-content-container">
{{if (ne $message.InReplyToID 0)}}
<div class="replying-to-container">
<div class="replying-to-label row">
<img class="svg-icon" src="/static/icons/replying_to.svg" />
<span>Replying to</span>
</div>
<div class="replying-to-message">
{{(index $.DMTrove.Messages $message.InReplyToID).Text}}
</div>
</div>
{{end}}
{{if (ne $message.EmbeddedTweetID 0)}}
<div class="tweet-preview">
{{template "tweet" (dict
"TweetID" $message.EmbeddedTweetID
"RetweetID" 0
"QuoteNestingLevel" 1)
}}
</div>
{{end}}
<div class="dm-message-text-container">
{{template "text-with-entities" $message.Text}}
</div>
</div>
</div>
<div class="dm-message-reactions">
{{range $message.Reactions}}
{{$sender := (user .SenderID)}}
<span title="{{$sender.DisplayName}} (@{{$sender.Handle}})">{{.Emoji}}</span>
{{end}}
</div>
<p class="posted-at">
{{$message.SentAt.Time.Format "Jan 2, 2006 @ 3:04 pm"}}
</p>
</div>
{{end}}
<div id="new-messages-poller"
hx-swap="outerHTML"
hx-trigger="load delay:7s"
hx-get="/messages/{{$.ActiveRoomID}}?poll&latest_timestamp={{$.LatestPollingTimestamp}}"
></div>
{{end}}
{{define "chat-view"}}
<div id="chat-view">
<div class="chat-messages">
{{range .MessageIDs}}
{{$message := (index $.DMTrove.Messages .)}}
{{$user := (user $message.SenderID)}}
{{$is_us := (eq $message.SenderID (active_user).ID)}}
<div class="dm-message-and-reacts-container {{if $is_us}} our-message {{end}}">
<div class="dm-message-container">
<div class="sender-profile-image-container">
<a class="unstyled-link" href="/{{$user.Handle}}">
{{if $user.IsContentDownloaded}}
<img class="profile-image" src="/content/{{$user.GetProfileImageLocalPath}}" />
{{else}}
<img class="profile-image" src="{{$user.ProfileImageUrl}}" />
{{end}}
</a>
</div>
<div class="dm-message-content-container">
{{if (ne $message.InReplyToID 0)}}
<div class="replying-to-container">
<div class="replying-to-label row">
<img class="svg-icon" src="/static/icons/replying_to.svg" />
<span>Replying to</span>
</div>
<div class="replying-to-message">
{{(index $.DMTrove.Messages $message.InReplyToID).Text}}
</div>
</div>
{{end}}
{{if (ne $message.EmbeddedTweetID 0)}}
<div class="tweet-preview">
{{template "tweet" (dict
"TweetID" $message.EmbeddedTweetID
"RetweetID" 0
"QuoteNestingLevel" 1)
}}
</div>
{{end}}
<div class="dm-message-text-container">
{{template "text-with-entities" $message.Text}}
</div>
</div>
</div>
<div class="dm-message-reactions">
{{range $message.Reactions}}
{{$sender := (user .SenderID)}}
<span title="{{$sender.DisplayName}} (@{{$sender.Handle}})">{{.Emoji}}</span>
{{end}}
</div>
<p class="posted-at">
{{$message.SentAt.Time.Format "Jan 2, 2006 @ 3:04 pm"}}
</p>
</div>
{{if $.ActiveRoomID}}
{{template "messages-with-poller" .}}
{{end}}
</div>
{{if $.ActiveRoomID}}

View File

@ -234,7 +234,7 @@ func (p Profile) GetChatRoomsPreview(id UserID) DMChatView {
return ret
}
func (p Profile) GetChatRoomContents(id DMChatRoomID) DMChatView {
func (p Profile) GetChatRoomContents(id DMChatRoomID, latest_timestamp int) DMChatView {
ret := NewDMChatView()
var room DMChatRoom
err := p.DB.Get(&room, `
@ -274,9 +274,10 @@ func (p Profile) GetChatRoomContents(id DMChatRoomID) DMChatView {
select id, chat_room_id, sender_id, sent_at, request_id, text, in_reply_to_id, embedded_tweet_id
from chat_messages
where chat_room_id = ?
and sent_at > ?
order by sent_at desc
limit 50
`, room.ID)
`, room.ID, latest_timestamp)
if err != nil {
panic(err)
}
@ -342,6 +343,32 @@ func (p Profile) GetChatRoomContents(id DMChatRoomID) DMChatView {
}
}
// Fetch message previews
replied_message_ids := []interface{}{}
for _, m := range ret.Messages {
if m.InReplyToID != 0 {
// Don't clobber if it's already been fetched
if _, is_ok := ret.Messages[m.InReplyToID]; !is_ok {
replied_message_ids = append(replied_message_ids, m.InReplyToID)
}
}
}
if len(replied_message_ids) > 0 {
var replied_msgs []DMMessage
err = p.DB.Select(&replied_msgs, `
select id, chat_room_id, sender_id, sent_at, request_id, text, in_reply_to_id, embedded_tweet_id
from chat_messages
where id in (`+strings.Repeat("?,", len(replied_message_ids)-1)+`?)`,
replied_message_ids...)
if err != nil {
panic(err)
}
for _, msg := range replied_msgs {
msg.Reactions = make(map[UserID]DMReaction)
ret.Messages[msg.ID] = msg
}
}
p.fill_content(&ret.DMTrove.TweetTrove, UserID(0))
}

View File

@ -191,7 +191,7 @@ func TestGetChatRoomContents(t *testing.T) {
require.NoError(err)
room_id := DMChatRoomID("1458284524761075714-1488963321701171204")
chat_view := profile.GetChatRoomContents(room_id)
chat_view := profile.GetChatRoomContents(room_id, -1)
assert.Len(chat_view.Rooms, 1)
room, is_ok := chat_view.Rooms[room_id]
require.True(is_ok)
@ -232,3 +232,28 @@ func TestGetChatRoomContents(t *testing.T) {
require.True(is_ok)
assert.Equal(u.Location, "on my computer")
}
func TestGetChatRoomContentsAfterTimestamp(t *testing.T) {
require := require.New(t)
assert := assert.New(t)
profile, err := persistence.LoadProfile("../../sample_data/profile")
require.NoError(err)
room_id := DMChatRoomID("1488963321701171204-1178839081222115328")
chat_view := profile.GetChatRoomContents(room_id, 1686025129141)
// MessageIDs should just be the ones in the thread
require.Equal(chat_view.MessageIDs, []DMMessageID{1665936253483614215, 1665936253483614216, 1665936253483614217})
// Replied messages should be available, but not in the list of MessageIDs
require.Len(chat_view.Messages, 4)
msg, is_ok := chat_view.Messages[1665936253483614214]
assert.True(is_ok)
assert.Equal(msg.ID, DMMessageID(1665936253483614214))
for _, msg_id := range chat_view.MessageIDs {
msg, is_ok := chat_view.Messages[msg_id]
assert.True(is_ok)
assert.Equal(msg.ID, msg_id)
}
}