diff --git a/internal/webserver/handler_messages.go b/internal/webserver/handler_messages.go
index 70ca5d6..076f850 100644
--- a/internal/webserver/handler_messages.go
+++ b/internal/webserver/handler_messages.go
@@ -33,15 +33,11 @@ func (app *Application) Messages(w http.ResponseWriter, r *http.Request) {
chat_view := app.Profile.GetChatRoomsPreview(app.ActiveUser.ID)
if strings.Trim(r.URL.Path, "/") != "" {
- message_id := scraper.DMChatRoomID(strings.Trim(r.URL.Path, "/"))
- chat_contents := app.Profile.GetChatRoomContents(message_id)
+ chat_view.ActiveRoomID = scraper.DMChatRoomID(strings.Trim(r.URL.Path, "/"))
+ chat_contents := app.Profile.GetChatRoomContents(chat_view.ActiveRoomID)
chat_view.MergeWith(chat_contents.DMTrove)
chat_view.MessageIDs = chat_contents.MessageIDs
- if r.Header.Get("HX-Request") == "true" {
- app.buffered_render_tweet_htmx(w, "chat-view", MessageData(chat_view))
- return
- }
}
app.buffered_render_tweet_page(w, "tpl/messages.tpl", MessageData(chat_view))
diff --git a/internal/webserver/static/styles.css b/internal/webserver/static/styles.css
index 8ebc8bb..4084ae9 100644
--- a/internal/webserver/static/styles.css
+++ b/internal/webserver/static/styles.css
@@ -666,6 +666,14 @@ ul.dropdown-items {
box-sizing: border-box;
cursor: pointer;
}
+.chats-container .chat-list .chat.active-chat {
+ color: var(--color-twitter-blue);
+ border-left: 0.2em solid var(--color-twitter-blue);
+ background-color: var(--color-twitter-off-white);
+}
+.chats-container .chat-list .chat.active-chat .profile-image{
+ box-shadow: 0 0 1em 0em var(--color-twitter-blue);
+}
.chats-container .chat-list .chat .chat-preview-header {
display: flex;
align-items: center;
@@ -674,13 +682,19 @@ ul.dropdown-items {
.chats-container .chat-list .chat .chat-preview-header .posted-at {
margin: 0;
}
+.chats-container .chat-list .chat .groupchat-profile-image-container {
+ display: flex;
+}
+.chats-container .chat-list .chat .groupchat-profile-image-container .display-name {
+ padding: 0.6em;
+ font-style: italic;
+}
.chats-container .chat-list .chat .chat-preview {
font-size: 0.9em;
color: var(--color-twitter-text-gray);
padding: 0 1em;
border-left: 1px solid var(--color-outline-gray);
}
-
.chats-container #chat-view {
flex-basis: 0;
flex-grow: 7;
diff --git a/internal/webserver/tpl/tweet_page_includes/chat_list.tpl b/internal/webserver/tpl/tweet_page_includes/chat_list.tpl
index 73a46af..560d096 100644
--- a/internal/webserver/tpl/tweet_page_includes/chat_list.tpl
+++ b/internal/webserver/tpl/tweet_page_includes/chat_list.tpl
@@ -1,27 +1,7 @@
{{define "chat-list"}}
{{range .RoomIDs}}
- {{$room := (index $.Rooms .)}}
-
-
-
{{(index $.DMTrove.Messages $room.LastMessageID).Text}}
-
+ {{template "chat-list-entry" (dict "room" (index $.Rooms .) "messages" $.DMTrove.Messages "is_active" (eq $.ActiveRoomID .))}}
{{end}}
{{end}}
diff --git a/internal/webserver/tpl/tweet_page_includes/chat_list_entry.tpl b/internal/webserver/tpl/tweet_page_includes/chat_list_entry.tpl
new file mode 100644
index 0000000..467d77e
--- /dev/null
+++ b/internal/webserver/tpl/tweet_page_includes/chat_list_entry.tpl
@@ -0,0 +1,30 @@
+{{define "chat-list-entry"}}
+ {{$room := $.room}}
+
+
+
{{(index $.messages $room.LastMessageID).Text}}
+
+{{end}}
diff --git a/pkg/persistence/dm_queries.go b/pkg/persistence/dm_queries.go
index c77ac1c..fdb1922 100644
--- a/pkg/persistence/dm_queries.go
+++ b/pkg/persistence/dm_queries.go
@@ -13,10 +13,15 @@ import (
func (p Profile) SaveChatRoom(r DMChatRoom) error {
_, err := p.DB.NamedExec(`
- insert into chat_rooms (id, type, last_messaged_at, is_nsfw)
- values (:id, :type, :last_messaged_at, :is_nsfw)
- on conflict do update
- set last_messaged_at=:last_messaged_at
+ insert into chat_rooms (id, type, last_messaged_at, is_nsfw, created_at, created_by_user_id, name,
+ avatar_image_remote_url, avatar_image_local_path)
+ values (:id, :type, :last_messaged_at, :is_nsfw, :created_at, :created_by_user_id, :name,
+ :avatar_image_remote_url, :avatar_image_local_path)
+ on conflict do update
+ set last_messaged_at=:last_messaged_at,
+ name=:name,
+ avatar_image_remote_url=:avatar_image_remote_url,
+ avatar_image_local_path=:avatar_image_local_path
`, r,
)
if err != nil {
@@ -68,7 +73,7 @@ func (p Profile) SaveChatRoom(r DMChatRoom) error {
func (p Profile) GetChatRoom(id DMChatRoomID) (ret DMChatRoom, err error) {
err = p.DB.Get(&ret, `
- select id, type, last_messaged_at, is_nsfw
+ select id, type, last_messaged_at, is_nsfw, created_at, created_by_user_id, name, avatar_image_remote_url, avatar_image_local_path
from chat_rooms
where id = ?
`, id)
@@ -150,8 +155,9 @@ func (p Profile) GetChatMessage(id DMMessageID) (ret DMMessage, err error) {
type DMChatView struct {
DMTrove
- RoomIDs []DMChatRoomID
- MessageIDs []DMMessageID
+ RoomIDs []DMChatRoomID
+ MessageIDs []DMMessageID
+ ActiveRoomID DMChatRoomID
}
func NewDMChatView() DMChatView {
@@ -167,7 +173,8 @@ func (p Profile) GetChatRoomsPreview(id UserID) DMChatView {
var rooms []DMChatRoom
err := p.DB.Select(&rooms, `
- select id, type, last_messaged_at, is_nsfw
+ select id, type, last_messaged_at, is_nsfw, created_at, created_by_user_id, name,
+ avatar_image_remote_url, avatar_image_local_path
from chat_rooms
where exists (select 1 from chat_room_participants where chat_room_id = chat_rooms.id and user_id = ?)
order by last_messaged_at desc
@@ -231,7 +238,8 @@ func (p Profile) GetChatRoomContents(id DMChatRoomID) DMChatView {
ret := NewDMChatView()
var room DMChatRoom
err := p.DB.Get(&room, `
- select id, type, last_messaged_at, is_nsfw
+ select id, type, last_messaged_at, is_nsfw, created_at, created_by_user_id, name,
+ avatar_image_remote_url, avatar_image_local_path
from chat_rooms
where id = ?
`, id)
diff --git a/pkg/persistence/schema.sql b/pkg/persistence/schema.sql
index 675485d..f85b175 100644
--- a/pkg/persistence/schema.sql
+++ b/pkg/persistence/schema.sql
@@ -210,7 +210,14 @@ create table chat_rooms (rowid integer primary key,
id text unique not null,
type text not null,
last_messaged_at integer not null,
- is_nsfw boolean not null
+ is_nsfw boolean not null,
+
+ -- Group DM info
+ created_at integer not null,
+ created_by_user_id integer not null,
+ name text not null default '',
+ avatar_image_remote_url text not null default '',
+ avatar_image_local_path text not null default ''
);
create table chat_room_participants(rowid integer primary key,
diff --git a/pkg/scraper/api_types_dms.go b/pkg/scraper/api_types_dms.go
index 9e5458a..76833ec 100644
--- a/pkg/scraper/api_types_dms.go
+++ b/pkg/scraper/api_types_dms.go
@@ -47,6 +47,12 @@ type APIDMConversation struct {
Trusted bool `json:"trusted"`
Muted bool `json:"muted"`
Status string `json:"status"`
+
+ // For type == "GROUP_DM"
+ CreateTime int `json:"create_time,string"`
+ CreatedByUserID int `json:"created_by_user_id,string"`
+ Name string `json:"name"`
+ AvatarImage string `json:"avatar_image_https"`
}
type APIInbox struct {
diff --git a/pkg/scraper/api_types_dms_test.go b/pkg/scraper/api_types_dms_test.go
index 99340e5..8198160 100644
--- a/pkg/scraper/api_types_dms_test.go
+++ b/pkg/scraper/api_types_dms_test.go
@@ -90,6 +90,35 @@ func TestParseAPIDMConversation(t *testing.T) {
assert.False(p2.IsChatSettingsValid)
}
+func TestParseAPIDMGroupChat(t *testing.T) {
+ assert := assert.New(t)
+ data, err := os.ReadFile("test_responses/dms/chat_room_group_chat.json")
+ require.NoError(t, err)
+
+ var api_room APIDMConversation
+ err = json.Unmarshal(data, &api_room)
+ require.NoError(t, err)
+
+ // Simulate one of the participants being logged in
+ InitApi(API{UserID: 1458284524761075714})
+
+ chat_room := ParseAPIDMChatRoom(api_room)
+ assert.Equal(DMChatRoomID("1710215025518948715"), chat_room.ID)
+ assert.Equal("GROUP_DM", chat_room.Type)
+ assert.Equal(TimestampFromUnix(1700112789457), chat_room.LastMessagedAt)
+ assert.False(chat_room.IsNSFW)
+
+ // Group DM settings
+ assert.Equal(chat_room.CreatedAt, TimestampFromUnix(1696582011))
+ assert.Equal(chat_room.CreatedByUserID, UserID(2694459866))
+ assert.Equal(chat_room.Name, "Schön ist die Welt")
+ assert.Equal(chat_room.AvatarImageRemoteURL,
+ "https://pbs.twimg.com/dm_group_img/1722785857403240448/3Wt_yJEq6i_G-kAT2rXheTojjhqkYE3okoW5JGUUHY7J9D8O9o?format=jpg&name=orig")
+ assert.Equal(chat_room.AvatarImageLocalPath, "1710215025518948715_avatar_3Wt_yJEq6i_G-kAT2rXheTojjhqkYE3okoW5JGUUHY7J9D8O9o.jpg")
+
+ assert.Len(chat_room.Participants, 5)
+}
+
func TestParseInbox(t *testing.T) {
assert := assert.New(t)
data, err := os.ReadFile("test_responses/dms/inbox.json")
diff --git a/pkg/scraper/dm_chat_room.go b/pkg/scraper/dm_chat_room.go
index c1ac634..a98caec 100644
--- a/pkg/scraper/dm_chat_room.go
+++ b/pkg/scraper/dm_chat_room.go
@@ -1,5 +1,11 @@
package scraper
+import (
+ "fmt"
+ "net/url"
+ "path"
+)
+
type DMChatRoomID string
// A participant in a chat room.
@@ -28,6 +34,13 @@ type DMChatRoom struct {
LastMessagedAt Timestamp `db:"last_messaged_at"` // Used for ordering the chats in the UI
IsNSFW bool `db:"is_nsfw"`
+ // GROUP_DM rooms
+ CreatedAt Timestamp `db:"created_at"`
+ CreatedByUserID UserID `db:"created_by_user_id"`
+ Name string `db:"name"`
+ AvatarImageRemoteURL string `db:"avatar_image_remote_url"`
+ AvatarImageLocalPath string `db:"avatar_image_local_path"`
+
LastMessageID DMMessageID `db:"last_message_id"` // Not stored, but used to generate preview
Participants map[UserID]DMChatParticipant
}
@@ -39,6 +52,18 @@ func ParseAPIDMChatRoom(api_room APIDMConversation) DMChatRoom {
ret.LastMessagedAt = TimestampFromUnix(int64(api_room.SortTimestamp))
ret.IsNSFW = api_room.NSFW
+ if ret.Type == "GROUP_DM" {
+ ret.CreatedAt = TimestampFromUnix(int64(api_room.CreateTime / 1000))
+ ret.CreatedByUserID = UserID(api_room.CreatedByUserID)
+ ret.Name = api_room.Name
+ ret.AvatarImageRemoteURL = api_room.AvatarImage
+ tmp_url, err := url.Parse(ret.AvatarImageRemoteURL)
+ if err != nil {
+ panic(err)
+ }
+ ret.AvatarImageLocalPath = fmt.Sprintf("%s_avatar_%s.%s", ret.ID, path.Base(tmp_url.Path), tmp_url.Query().Get("format"))
+ }
+
ret.Participants = make(map[UserID]DMChatParticipant)
for _, api_participant := range api_room.Participants {
participant := DMChatParticipant{}
diff --git a/pkg/scraper/test_responses/dms/chat_room_group_chat.json b/pkg/scraper/test_responses/dms/chat_room_group_chat.json
new file mode 100644
index 0000000..152f20a
--- /dev/null
+++ b/pkg/scraper/test_responses/dms/chat_room_group_chat.json
@@ -0,0 +1 @@
+{"conversation_id":"1710215025518948715","type":"GROUP_DM","sort_event_id":"1725024183728394258","sort_timestamp":"1700112789457","participants":[{"user_id":"1040322921145589760","join_time":"1699480601054","join_conversation_event_id":"1722372593372606464","is_admin":false},{"user_id":"2694459866","join_time":"1696582011037","last_read_event_id":"1725024183728394258","is_admin":true},{"user_id":"1532476989721952256","join_time":"1696582011037","last_read_event_id":"1725024183728394258","join_conversation_event_id":"1710215025518948717","is_admin":false},{"user_id":"1590450335315021824","join_time":"1698335060149","last_read_event_id":"1725024183728394258","join_conversation_event_id":"1717567846618669056","is_admin":false},{"user_id":"1458284524761075714","join_time":"1696582011037","last_read_event_id":"1725024183728394258","join_conversation_event_id":"1710215025518948717","is_admin":false}],"create_time":"1696582011037","created_by_user_id":"2694459866","name":"Schön ist die Welt","avatar_image_https":"https://pbs.twimg.com/dm_group_img/1722785857403240448/3Wt_yJEq6i_G-kAT2rXheTojjhqkYE3okoW5JGUUHY7J9D8O9o?format=jpg&name=orig","avatar":{"image":{"original_info":{"url":"https://pbs.twimg.com/dm_group_img/1722785857403240448/3Wt_yJEq6i_G-kAT2rXheTojjhqkYE3okoW5JGUUHY7J9D8O9o?format=jpg&name=orig","width":400,"height":400}}},"nsfw":false,"notifications_disabled":false,"mention_notifications_disabled":false,"last_read_event_id":"1725024183728394258","trusted":true,"low_quality":false,"muted":false,"status":"HAS_MORE","min_entry_id":"1724956348776120338","max_entry_id":"1725024183728394258"}
diff --git a/sample_data/seed_data.sql b/sample_data/seed_data.sql
index ad7a4a8..f21c54d 100644
--- a/sample_data/seed_data.sql
+++ b/sample_data/seed_data.sql
@@ -336,11 +336,19 @@ create table chat_rooms (rowid integer primary key,
id text unique not null,
type text not null,
last_messaged_at integer not null,
- is_nsfw boolean not null
+ is_nsfw boolean not null,
+
+ -- Group DM info
+ created_at integer not null default 0,
+ created_by_user_id integer not null default 0,
+ name text not null default '',
+ avatar_image_remote_url text not null default '',
+ avatar_image_local_path text not null default ''
);
+
INSERT INTO chat_rooms VALUES
- (1,'1458284524761075714-1488963321701171204','ONE_TO_ONE',1686025129132,0),
- (2,'1488963321701171204-1178839081222115328','ONE_TO_ONE',1686025129144,0);
+ (1,'1458284524761075714-1488963321701171204','ONE_TO_ONE',1686025129132,0,0,0,'','',''),
+ (2,'1488963321701171204-1178839081222115328','ONE_TO_ONE',1686025129144,0,0,0,'','','');
create table chat_room_participants(rowid integer primary key,
chat_room_id text not null,