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 .)}} -
-
- {{range $room.Participants}} - {{if (ne .UserID (active_user).ID)}} - -
- {{template "author-info" (user .UserID)}} -
- {{end}} - {{end}} -
-

- {{$room.LastMessagedAt.Time.Format "Jan 2, 2006"}} -
- {{$room.LastMessagedAt.Time.Format "3:04 pm"}} -

-
-
-

{{(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}} +
+
+ {{if (eq $room.Type "ONE_TO_ONE")}} + {{range $room.Participants}} + {{if (ne .UserID (active_user).ID)}} + +
+ {{template "author-info" (user .UserID)}} +
+ {{end}} + {{end}} + {{else}} +
+ +
{{$room.Name}}
+
+ {{end}} +
+

+ {{$room.LastMessagedAt.Time.Format "Jan 2, 2006"}} +
+ {{$room.LastMessagedAt.Time.Format "3:04 pm"}} +

+
+
+

{{(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,