diff --git a/internal/webserver/handler_messages.go b/internal/webserver/handler_messages.go index b45674b..5190ce6 100644 --- a/internal/webserver/handler_messages.go +++ b/internal/webserver/handler_messages.go @@ -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) } diff --git a/internal/webserver/server_test.go b/internal/webserver/server_test.go index bd8a5a1..a48a718 100644 --- a/internal/webserver/server_test.go +++ b/internal/webserver/server_test.go @@ -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) } diff --git a/internal/webserver/tpl/tweet_page_includes/chat_view.tpl b/internal/webserver/tpl/tweet_page_includes/chat_view.tpl index cf14862..7fd4169 100644 --- a/internal/webserver/tpl/tweet_page_includes/chat_view.tpl +++ b/internal/webserver/tpl/tweet_page_includes/chat_view.tpl @@ -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)}} +
+
+ +
+ {{if (ne $message.InReplyToID 0)}} +
+
+ + Replying to +
+
+ {{(index $.DMTrove.Messages $message.InReplyToID).Text}} +
+
+ {{end}} + {{if (ne $message.EmbeddedTweetID 0)}} +
+ {{template "tweet" (dict + "TweetID" $message.EmbeddedTweetID + "RetweetID" 0 + "QuoteNestingLevel" 1) + }} +
+ {{end}} +
+ {{template "text-with-entities" $message.Text}} +
+
+
+
+ {{range $message.Reactions}} + {{$sender := (user .SenderID)}} + {{.Emoji}} + {{end}} +
+

+ {{$message.SentAt.Time.Format "Jan 2, 2006 @ 3:04 pm"}} +

+
+ {{end}} + +
+{{end}} + {{define "chat-view"}}
- {{range .MessageIDs}} - {{$message := (index $.DMTrove.Messages .)}} - {{$user := (user $message.SenderID)}} - {{$is_us := (eq $message.SenderID (active_user).ID)}} -
-
- -
- {{if (ne $message.InReplyToID 0)}} -
-
- - Replying to -
-
- {{(index $.DMTrove.Messages $message.InReplyToID).Text}} -
-
- {{end}} - {{if (ne $message.EmbeddedTweetID 0)}} -
- {{template "tweet" (dict - "TweetID" $message.EmbeddedTweetID - "RetweetID" 0 - "QuoteNestingLevel" 1) - }} -
- {{end}} -
- {{template "text-with-entities" $message.Text}} -
-
-
-
- {{range $message.Reactions}} - {{$sender := (user .SenderID)}} - {{.Emoji}} - {{end}} -
-

- {{$message.SentAt.Time.Format "Jan 2, 2006 @ 3:04 pm"}} -

-
+ {{if $.ActiveRoomID}} + {{template "messages-with-poller" .}} {{end}}
{{if $.ActiveRoomID}} diff --git a/pkg/persistence/dm_queries.go b/pkg/persistence/dm_queries.go index 3a3f6aa..d4679d2 100644 --- a/pkg/persistence/dm_queries.go +++ b/pkg/persistence/dm_queries.go @@ -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)) } diff --git a/pkg/persistence/dm_queries_test.go b/pkg/persistence/dm_queries_test.go index 8ce630e..a2840bf 100644 --- a/pkg/persistence/dm_queries_test.go +++ b/pkg/persistence/dm_queries_test.go @@ -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) + } +}