Add support for group DMs; also make UI highlight which chat is currently active
This commit is contained in:
parent
6e9a370073
commit
b73ee1f5d2
@ -33,15 +33,11 @@ func (app *Application) Messages(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
chat_view := app.Profile.GetChatRoomsPreview(app.ActiveUser.ID)
|
chat_view := app.Profile.GetChatRoomsPreview(app.ActiveUser.ID)
|
||||||
if strings.Trim(r.URL.Path, "/") != "" {
|
if strings.Trim(r.URL.Path, "/") != "" {
|
||||||
message_id := scraper.DMChatRoomID(strings.Trim(r.URL.Path, "/"))
|
chat_view.ActiveRoomID = scraper.DMChatRoomID(strings.Trim(r.URL.Path, "/"))
|
||||||
chat_contents := app.Profile.GetChatRoomContents(message_id)
|
chat_contents := app.Profile.GetChatRoomContents(chat_view.ActiveRoomID)
|
||||||
chat_view.MergeWith(chat_contents.DMTrove)
|
chat_view.MergeWith(chat_contents.DMTrove)
|
||||||
chat_view.MessageIDs = chat_contents.MessageIDs
|
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))
|
app.buffered_render_tweet_page(w, "tpl/messages.tpl", MessageData(chat_view))
|
||||||
|
@ -666,6 +666,14 @@ ul.dropdown-items {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
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 {
|
.chats-container .chat-list .chat .chat-preview-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -674,13 +682,19 @@ ul.dropdown-items {
|
|||||||
.chats-container .chat-list .chat .chat-preview-header .posted-at {
|
.chats-container .chat-list .chat .chat-preview-header .posted-at {
|
||||||
margin: 0;
|
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 {
|
.chats-container .chat-list .chat .chat-preview {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: var(--color-twitter-text-gray);
|
color: var(--color-twitter-text-gray);
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
border-left: 1px solid var(--color-outline-gray);
|
border-left: 1px solid var(--color-outline-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chats-container #chat-view {
|
.chats-container #chat-view {
|
||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
flex-grow: 7;
|
flex-grow: 7;
|
||||||
|
@ -1,27 +1,7 @@
|
|||||||
{{define "chat-list"}}
|
{{define "chat-list"}}
|
||||||
<div class="chat-list">
|
<div class="chat-list">
|
||||||
{{range .RoomIDs}}
|
{{range .RoomIDs}}
|
||||||
{{$room := (index $.Rooms .)}}
|
{{template "chat-list-entry" (dict "room" (index $.Rooms .) "messages" $.DMTrove.Messages "is_active" (eq $.ActiveRoomID .))}}
|
||||||
<div class="chat" hx-get="/messages/{{$room.ID}}" hx-target="#chat-view" hx-swap="outerHTML" hx-push-url="true">
|
|
||||||
<div class="chat-preview-header">
|
|
||||||
{{range $room.Participants}}
|
|
||||||
{{if (ne .UserID (active_user).ID)}}
|
|
||||||
<!-- This is some fuckery; I have no idea why "hx-target" is needed, but otherwise it targets the #chat-view. -->
|
|
||||||
<div class="click-eater" hx-trigger="click consume" hx-target="body">
|
|
||||||
{{template "author-info" (user .UserID)}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
<div class="chat-preview-timestamp .posted-at-container">
|
|
||||||
<p class="posted-at">
|
|
||||||
{{$room.LastMessagedAt.Time.Format "Jan 2, 2006"}}
|
|
||||||
<br/>
|
|
||||||
{{$room.LastMessagedAt.Time.Format "3:04 pm"}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="chat-preview">{{(index $.DMTrove.Messages $room.LastMessageID).Text}}</p>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
{{define "chat-list-entry"}}
|
||||||
|
{{$room := $.room}}
|
||||||
|
<div class="chat {{if .is_active}}active-chat{{end}}" hx-get="/messages/{{$room.ID}}" hx-push-url="true" hx-swap="outerHTML" hx-target="body">
|
||||||
|
<div class="chat-preview-header">
|
||||||
|
{{if (eq $room.Type "ONE_TO_ONE")}}
|
||||||
|
{{range $room.Participants}}
|
||||||
|
{{if (ne .UserID (active_user).ID)}}
|
||||||
|
<!-- This is some fuckery; I have no idea why "hx-target" is needed, but otherwise it targets the #chat-view. -->
|
||||||
|
<div class="click-eater" hx-trigger="click consume" hx-target="body">
|
||||||
|
{{template "author-info" (user .UserID)}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<div class="groupchat-profile-image-container">
|
||||||
|
<img class="profile-image" src="{{$room.AvatarImageRemoteURL}}" />
|
||||||
|
<div class="display-name row">{{$room.Name}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="chat-preview-timestamp .posted-at-container">
|
||||||
|
<p class="posted-at">
|
||||||
|
{{$room.LastMessagedAt.Time.Format "Jan 2, 2006"}}
|
||||||
|
<br/>
|
||||||
|
{{$room.LastMessagedAt.Time.Format "3:04 pm"}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="chat-preview">{{(index $.messages $room.LastMessageID).Text}}</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
@ -13,10 +13,15 @@ import (
|
|||||||
|
|
||||||
func (p Profile) SaveChatRoom(r DMChatRoom) error {
|
func (p Profile) SaveChatRoom(r DMChatRoom) error {
|
||||||
_, err := p.DB.NamedExec(`
|
_, err := p.DB.NamedExec(`
|
||||||
insert into chat_rooms (id, type, last_messaged_at, is_nsfw)
|
insert into chat_rooms (id, type, last_messaged_at, is_nsfw, created_at, created_by_user_id, name,
|
||||||
values (:id, :type, :last_messaged_at, :is_nsfw)
|
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
|
on conflict do update
|
||||||
set last_messaged_at=:last_messaged_at
|
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,
|
`, r,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -68,7 +73,7 @@ func (p Profile) SaveChatRoom(r DMChatRoom) error {
|
|||||||
|
|
||||||
func (p Profile) GetChatRoom(id DMChatRoomID) (ret DMChatRoom, err error) {
|
func (p Profile) GetChatRoom(id DMChatRoomID) (ret DMChatRoom, err error) {
|
||||||
err = p.DB.Get(&ret, `
|
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
|
from chat_rooms
|
||||||
where id = ?
|
where id = ?
|
||||||
`, id)
|
`, id)
|
||||||
@ -152,6 +157,7 @@ type DMChatView struct {
|
|||||||
DMTrove
|
DMTrove
|
||||||
RoomIDs []DMChatRoomID
|
RoomIDs []DMChatRoomID
|
||||||
MessageIDs []DMMessageID
|
MessageIDs []DMMessageID
|
||||||
|
ActiveRoomID DMChatRoomID
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDMChatView() DMChatView {
|
func NewDMChatView() DMChatView {
|
||||||
@ -167,7 +173,8 @@ func (p Profile) GetChatRoomsPreview(id UserID) DMChatView {
|
|||||||
|
|
||||||
var rooms []DMChatRoom
|
var rooms []DMChatRoom
|
||||||
err := p.DB.Select(&rooms, `
|
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
|
from chat_rooms
|
||||||
where exists (select 1 from chat_room_participants where chat_room_id = chat_rooms.id and user_id = ?)
|
where exists (select 1 from chat_room_participants where chat_room_id = chat_rooms.id and user_id = ?)
|
||||||
order by last_messaged_at desc
|
order by last_messaged_at desc
|
||||||
@ -231,7 +238,8 @@ func (p Profile) GetChatRoomContents(id DMChatRoomID) DMChatView {
|
|||||||
ret := NewDMChatView()
|
ret := NewDMChatView()
|
||||||
var room DMChatRoom
|
var room DMChatRoom
|
||||||
err := p.DB.Get(&room, `
|
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
|
from chat_rooms
|
||||||
where id = ?
|
where id = ?
|
||||||
`, id)
|
`, id)
|
||||||
|
@ -210,7 +210,14 @@ create table chat_rooms (rowid integer primary key,
|
|||||||
id text unique not null,
|
id text unique not null,
|
||||||
type text not null,
|
type text not null,
|
||||||
last_messaged_at integer 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,
|
create table chat_room_participants(rowid integer primary key,
|
||||||
|
@ -47,6 +47,12 @@ type APIDMConversation struct {
|
|||||||
Trusted bool `json:"trusted"`
|
Trusted bool `json:"trusted"`
|
||||||
Muted bool `json:"muted"`
|
Muted bool `json:"muted"`
|
||||||
Status string `json:"status"`
|
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 {
|
type APIInbox struct {
|
||||||
|
@ -90,6 +90,35 @@ func TestParseAPIDMConversation(t *testing.T) {
|
|||||||
assert.False(p2.IsChatSettingsValid)
|
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) {
|
func TestParseInbox(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
data, err := os.ReadFile("test_responses/dms/inbox.json")
|
data, err := os.ReadFile("test_responses/dms/inbox.json")
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
package scraper
|
package scraper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
type DMChatRoomID string
|
type DMChatRoomID string
|
||||||
|
|
||||||
// A participant in a chat room.
|
// 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
|
LastMessagedAt Timestamp `db:"last_messaged_at"` // Used for ordering the chats in the UI
|
||||||
IsNSFW bool `db:"is_nsfw"`
|
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
|
LastMessageID DMMessageID `db:"last_message_id"` // Not stored, but used to generate preview
|
||||||
Participants map[UserID]DMChatParticipant
|
Participants map[UserID]DMChatParticipant
|
||||||
}
|
}
|
||||||
@ -39,6 +52,18 @@ func ParseAPIDMChatRoom(api_room APIDMConversation) DMChatRoom {
|
|||||||
ret.LastMessagedAt = TimestampFromUnix(int64(api_room.SortTimestamp))
|
ret.LastMessagedAt = TimestampFromUnix(int64(api_room.SortTimestamp))
|
||||||
ret.IsNSFW = api_room.NSFW
|
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)
|
ret.Participants = make(map[UserID]DMChatParticipant)
|
||||||
for _, api_participant := range api_room.Participants {
|
for _, api_participant := range api_room.Participants {
|
||||||
participant := DMChatParticipant{}
|
participant := DMChatParticipant{}
|
||||||
|
1
pkg/scraper/test_responses/dms/chat_room_group_chat.json
Normal file
1
pkg/scraper/test_responses/dms/chat_room_group_chat.json
Normal file
@ -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"}
|
@ -336,11 +336,19 @@ create table chat_rooms (rowid integer primary key,
|
|||||||
id text unique not null,
|
id text unique not null,
|
||||||
type text not null,
|
type text not null,
|
||||||
last_messaged_at integer 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
|
INSERT INTO chat_rooms VALUES
|
||||||
(1,'1458284524761075714-1488963321701171204','ONE_TO_ONE',1686025129132,0),
|
(1,'1458284524761075714-1488963321701171204','ONE_TO_ONE',1686025129132,0,0,0,'','',''),
|
||||||
(2,'1488963321701171204-1178839081222115328','ONE_TO_ONE',1686025129144,0);
|
(2,'1488963321701171204-1178839081222115328','ONE_TO_ONE',1686025129144,0,0,0,'','','');
|
||||||
|
|
||||||
create table chat_room_participants(rowid integer primary key,
|
create table chat_room_participants(rowid integer primary key,
|
||||||
chat_room_id text not null,
|
chat_room_id text not null,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user