Add polling for new messages in a chat room while on the page
This commit is contained in:
parent
d80a2bd5b1
commit
bd90b1c528
@ -5,12 +5,16 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence"
|
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence"
|
||||||
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
|
"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 {
|
func (t MessageData) Tweet(id scraper.TweetID) scraper.Tweet {
|
||||||
return t.Tweets[id]
|
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, "/"), "/")
|
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
||||||
room_id := scraper.DMChatRoomID(parts[0])
|
room_id := scraper.DMChatRoomID(parts[0])
|
||||||
if len(parts) == 2 && parts[1] == "send" {
|
|
||||||
body, err := io.ReadAll(r.Body)
|
if r.URL.Query().Has("scrape") {
|
||||||
panic_if(err)
|
// TODO: where is this going to be used?
|
||||||
var message_data struct {
|
app.background_dm_polling_scrape()
|
||||||
Text string `json:"text"`
|
}
|
||||||
|
|
||||||
|
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)
|
app.buffered_render_tweet_page(w, "tpl/messages.tpl", chat_view_data)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
@ -608,4 +608,30 @@ func TestMessagesRoom(t *testing.T) {
|
|||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
assert.Len(cascadia.QueryAll(root, selector(".chat-list .chat")), 2) // Chat list still renders
|
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)
|
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)
|
||||||
}
|
}
|
||||||
|
@ -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"}}
|
{{define "chat-view"}}
|
||||||
<div id="chat-view">
|
<div id="chat-view">
|
||||||
<div class="chat-messages">
|
<div class="chat-messages">
|
||||||
{{range .MessageIDs}}
|
{{if $.ActiveRoomID}}
|
||||||
{{$message := (index $.DMTrove.Messages .)}}
|
{{template "messages-with-poller" .}}
|
||||||
{{$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}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if $.ActiveRoomID}}
|
{{if $.ActiveRoomID}}
|
||||||
|
@ -234,7 +234,7 @@ func (p Profile) GetChatRoomsPreview(id UserID) DMChatView {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Profile) GetChatRoomContents(id DMChatRoomID) DMChatView {
|
func (p Profile) GetChatRoomContents(id DMChatRoomID, latest_timestamp int) DMChatView {
|
||||||
ret := NewDMChatView()
|
ret := NewDMChatView()
|
||||||
var room DMChatRoom
|
var room DMChatRoom
|
||||||
err := p.DB.Get(&room, `
|
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
|
select id, chat_room_id, sender_id, sent_at, request_id, text, in_reply_to_id, embedded_tweet_id
|
||||||
from chat_messages
|
from chat_messages
|
||||||
where chat_room_id = ?
|
where chat_room_id = ?
|
||||||
|
and sent_at > ?
|
||||||
order by sent_at desc
|
order by sent_at desc
|
||||||
limit 50
|
limit 50
|
||||||
`, room.ID)
|
`, room.ID, latest_timestamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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))
|
p.fill_content(&ret.DMTrove.TweetTrove, UserID(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +191,7 @@ func TestGetChatRoomContents(t *testing.T) {
|
|||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
room_id := DMChatRoomID("1458284524761075714-1488963321701171204")
|
room_id := DMChatRoomID("1458284524761075714-1488963321701171204")
|
||||||
chat_view := profile.GetChatRoomContents(room_id)
|
chat_view := profile.GetChatRoomContents(room_id, -1)
|
||||||
assert.Len(chat_view.Rooms, 1)
|
assert.Len(chat_view.Rooms, 1)
|
||||||
room, is_ok := chat_view.Rooms[room_id]
|
room, is_ok := chat_view.Rooms[room_id]
|
||||||
require.True(is_ok)
|
require.True(is_ok)
|
||||||
@ -232,3 +232,28 @@ func TestGetChatRoomContents(t *testing.T) {
|
|||||||
require.True(is_ok)
|
require.True(is_ok)
|
||||||
assert.Equal(u.Location, "on my computer")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user