Skip to content

Commit 04c7b16

Browse files
committed
reply to messages
1 parent ff6188e commit 04c7b16

8 files changed

Lines changed: 125 additions & 45 deletions

File tree

server/controllers/messages.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ import (
1313
)
1414

1515
type SendMessageReq struct {
16-
ReceiverID string `json:"receiver_id"`
17-
Content string `json:"content"`
16+
ReceiverID string `json:"receiver_id"`
17+
Content string `json:"content"`
18+
ReplyToMessageID string `json:"reply_to_message_id,omitempty"`
1819
}
1920

2021
func HandleSendMessage(w http.ResponseWriter, r *http.Request) {
@@ -41,7 +42,7 @@ func HandleSendMessage(w http.ResponseWriter, r *http.Request) {
4142
}
4243

4344
svc := services.NewMessageService()
44-
msg, err := svc.SendMessage(r.Context(), senderID, req.ReceiverID, req.Content)
45+
msg, err := svc.SendMessage(r.Context(), senderID, req.ReceiverID, req.Content, req.ReplyToMessageID)
4546
if err != nil {
4647
w.WriteHeader(http.StatusBadRequest)
4748
_ = json.NewEncoder(w).Encode(models.JSONResponse{Success: false, Error: err.Error()})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ALTER TABLE messages ADD COLUMN reply_to_message_id UUID REFERENCES messages(id) ON DELETE SET NULL;
2+
3+
CREATE INDEX IF NOT EXISTS idx_messages_reply_to
4+
ON messages(reply_to_message_id)
5+
WHERE reply_to_message_id IS NOT NULL;

server/models/message.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@ package models
22

33
import "time"
44

5+
// QuotedMessage represents a lightweight snapshot of the parent message being replied to
6+
type QuotedMessage struct {
7+
ID string `json:"id"`
8+
SenderID string `json:"sender_id"`
9+
Content string `json:"content"`
10+
}
11+
512
type Message struct {
6-
ID string `json:"id"`
7-
SenderID string `json:"sender_id"`
8-
ReceiverID string `json:"receiver_id,omitempty"`
9-
GroupID string `json:"group_id,omitempty"`
10-
Content string `json:"content"`
11-
CreatedAt time.Time `json:"created_at"`
12-
IsRead bool `json:"is_read"`
13-
}
13+
ID string `json:"id"`
14+
SenderID string `json:"sender_id"`
15+
ReceiverID string `json:"receiver_id,omitempty"`
16+
GroupID string `json:"group_id,omitempty"`
17+
Content string `json:"content"`
18+
CreatedAt time.Time `json:"created_at"`
19+
IsRead bool `json:"is_read"`
20+
ReplyToMessageID *string `json:"reply_to_message_id,omitempty"`
21+
QuotedMessage *QuotedMessage `json:"quoted_message,omitempty"`
22+
}

server/models/ws.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ package models
33
import "time"
44

55
type WSMessage struct {
6-
Type string `json:"type"`
7-
SenderID string `json:"sender_id,omitempty"`
8-
ReceiverID string `json:"receiver_id,omitempty"`
9-
GroupID string `json:"group_id,omitempty"`
10-
Content string `json:"content,omitempty"`
11-
MessageID string `json:"message_id,omitempty"`
12-
Timestamp time.Time `json:"timestamp,omitempty"`
6+
Type string `json:"type"`
7+
SenderID string `json:"sender_id,omitempty"`
8+
ReceiverID string `json:"receiver_id,omitempty"`
9+
GroupID string `json:"group_id,omitempty"`
10+
Content string `json:"content,omitempty"`
11+
MessageID string `json:"message_id,omitempty"`
12+
ReplyToMessageID string `json:"reply_to_message_id,omitempty"`
13+
Timestamp time.Time `json:"timestamp,omitempty"`
1314
}

server/repository/groups.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,13 @@ func (r *GroupRepository) GetGroupMembers(ctx context.Context, groupID string) (
105105

106106
func (r *GroupRepository) GetGroupMessages(ctx context.Context, groupID string, before time.Time, limit int) ([]models.Message, error) {
107107
query := `
108-
SELECT id, sender_id, content, created_at
109-
FROM messages
110-
WHERE group_id = $1 AND created_at < $2
111-
ORDER BY created_at DESC
108+
SELECT
109+
m.id, m.sender_id, m.content, m.created_at, m.reply_to_message_id,
110+
q.sender_id AS quoted_sender_id, q.content AS quoted_content
111+
FROM messages m
112+
LEFT JOIN messages q ON m.reply_to_message_id = q.id
113+
WHERE m.group_id = $1::uuid AND m.created_at < $2
114+
ORDER BY m.created_at DESC
112115
LIMIT $3;
113116
`
114117
rows, err := config.DB.Query(ctx, query, groupID, before, limit)
@@ -117,16 +120,27 @@ func (r *GroupRepository) GetGroupMessages(ctx context.Context, groupID string,
117120
}
118121
defer rows.Close()
119122

120-
messages := []models.Message{}
123+
var history []models.Message = []models.Message{}
121124
for rows.Next() {
122125
var m models.Message
123-
if err := rows.Scan(&m.ID, &m.SenderID, &m.Content, &m.CreatedAt); err != nil {
126+
var qSender, qContent *string
127+
128+
err := rows.Scan(&m.ID, &m.SenderID, &m.Content, &m.CreatedAt, &m.ReplyToMessageID, &qSender, &qContent)
129+
if err != nil {
124130
return nil, err
125131
}
132+
126133
m.GroupID = groupID
127-
messages = append(messages, m)
134+
if m.ReplyToMessageID != nil && qSender != nil && qContent != nil {
135+
m.QuotedMessage = &models.QuotedMessage{
136+
ID: *m.ReplyToMessageID,
137+
SenderID: *qSender,
138+
Content: *qContent,
139+
}
140+
}
141+
history = append(history, m)
128142
}
129-
return messages, nil
143+
return history, nil
130144
}
131145

132146
func (r *GroupRepository) GetMembersDetails(ctx context.Context, groupID string) ([]models.GroupMemberDetail, error) {

server/repository/message.go

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,19 @@ func NewMessageRepository() *MessageRepository {
1414
return &MessageRepository{}
1515
}
1616

17-
func (r *MessageRepository) CreateMessage(ctx context.Context, senderID, receiverID, content string) (*models.Message, error) {
17+
func (r *MessageRepository) CreateMessage(ctx context.Context, senderID, receiverID, content, replyToID string) (*models.Message, error) {
1818
query := `
19-
INSERT INTO messages (sender_id, receiver_id, content)
20-
VALUES ($1, $2, $3)
21-
RETURNING id, sender_id, receiver_id, content, created_at;
19+
INSERT INTO messages (sender_id, receiver_id, content, reply_to_message_id)
20+
VALUES ($1, $2, $3, NULLIF($4, '')::uuid)
21+
RETURNING id, sender_id, receiver_id, content, reply_to_message_id, created_at;
2222
`
2323
var msg models.Message
24-
err := config.DB.QueryRow(ctx, query, senderID, receiverID, content).Scan(
24+
err := config.DB.QueryRow(ctx, query, senderID, receiverID, content, replyToID).Scan(
2525
&msg.ID,
2626
&msg.SenderID,
2727
&msg.ReceiverID,
2828
&msg.Content,
29+
&msg.ReplyToMessageID,
2930
&msg.CreatedAt,
3031
)
3132
if err != nil {
@@ -36,11 +37,14 @@ func (r *MessageRepository) CreateMessage(ctx context.Context, senderID, receive
3637

3738
func (r *MessageRepository) GetChatHistory(ctx context.Context, userA, userB string, before time.Time, limit int) ([]models.Message, error) {
3839
query := `
39-
SELECT id, sender_id, receiver_id, content, created_at, is_read
40-
FROM messages
41-
WHERE ((sender_id = $1 AND receiver_id = $2) OR (sender_id = $2 AND receiver_id = $1))
42-
AND created_at < $3
43-
ORDER BY created_at DESC
40+
SELECT
41+
m.id, m.sender_id, m.receiver_id, m.content, m.created_at, m.is_read, m.reply_to_message_id,
42+
q.sender_id AS quoted_sender_id, q.content AS quoted_content
43+
FROM messages m
44+
LEFT JOIN messages q ON m.reply_to_message_id = q.id
45+
WHERE ((m.sender_id = $1 AND m.receiver_id = $2) OR (m.sender_id = $2 AND m.receiver_id = $1))
46+
AND m.group_id IS NULL AND m.created_at < $3
47+
ORDER BY m.created_at DESC
4448
LIMIT $4;
4549
`
4650
rows, err := config.DB.Query(ctx, query, userA, userB, before, limit)
@@ -52,15 +56,35 @@ func (r *MessageRepository) GetChatHistory(ctx context.Context, userA, userB str
5256
var history []models.Message = []models.Message{}
5357
for rows.Next() {
5458
var m models.Message
55-
err := rows.Scan(&m.ID, &m.SenderID, &m.ReceiverID, &m.Content, &m.CreatedAt, &m.IsRead)
59+
var qSender, qContent *string
60+
61+
err := rows.Scan(&m.ID, &m.SenderID, &m.ReceiverID, &m.Content, &m.CreatedAt, &m.IsRead, &m.ReplyToMessageID, &qSender, &qContent)
5662
if err != nil {
5763
return nil, err
5864
}
65+
66+
if m.ReplyToMessageID != nil && qSender != nil && qContent != nil {
67+
m.QuotedMessage = &models.QuotedMessage{
68+
ID: *m.ReplyToMessageID,
69+
SenderID: *qSender,
70+
Content: *qContent,
71+
}
72+
}
5973
history = append(history, m)
6074
}
6175
return history, nil
6276
}
6377

78+
func (r *MessageRepository) GetMessagePreview(ctx context.Context, messageID string) (*models.QuotedMessage, error) {
79+
query := `SELECT id, sender_id, content FROM messages WHERE id = $1::uuid;`
80+
var q models.QuotedMessage
81+
err := config.DB.QueryRow(ctx, query, messageID).Scan(&q.ID, &q.SenderID, &q.Content)
82+
if err != nil {
83+
return nil, err
84+
}
85+
return &q, nil
86+
}
87+
6488
func (r *MessageRepository) GetConversations(ctx context.Context, userID string) ([]models.Conversation, error) {
6589
query := `
6690
WITH raw_conversations AS (

server/services/message.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ func NewMessageService() *MessageService {
1717
return &MessageService{repo: repository.NewMessageRepository()}
1818
}
1919

20-
func (s *MessageService) SendMessage(ctx context.Context, senderID, receiverID, content string) (*models.Message, error) {
20+
func (s *MessageService) SendMessage(ctx context.Context, senderID, receiverID, content, replyToMessageID string) (*models.Message, error) {
2121
content = strings.TrimSpace(content)
2222
if content == "" {
2323
return nil, errors.New("message content cannot be empty")
2424
}
2525
if senderID == receiverID {
2626
return nil, errors.New("cannot send a message to yourself")
2727
}
28-
return s.repo.CreateMessage(ctx, senderID, receiverID, content)
29-
}
28+
return s.repo.CreateMessage(ctx, senderID, receiverID, content, replyToMessageID)
29+
}

server/services/ws.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -222,16 +222,43 @@ func (c *WSClient) ReadPump() {
222222

223223
go func(msg models.WSMessage) {
224224
query := `
225-
INSERT INTO messages (sender_id, receiver_id, group_id, content, id)
226-
VALUES ($1::uuid, NULLIF($2, '')::uuid, NULLIF($3, '')::uuid, $4, COALESCE(NULLIF($5, '')::uuid, gen_random_uuid()));
225+
INSERT INTO messages (sender_id, receiver_id, group_id, content, id, reply_to_message_id)
226+
VALUES (
227+
$1::uuid,
228+
NULLIF($2, '')::uuid,
229+
NULLIF($3, '')::uuid,
230+
$4,
231+
COALESCE(NULLIF($5, '')::uuid, gen_random_uuid()),
232+
NULLIF($6, '')::uuid
233+
);
227234
`
228-
_, err := config.DB.Exec(context.Background(), query, msg.SenderID, msg.ReceiverID, msg.GroupID, msg.Content, msg.MessageID)
235+
_, err := config.DB.Exec(context.Background(), query, msg.SenderID, msg.ReceiverID, msg.GroupID, msg.Content, msg.MessageID, msg.ReplyToMessageID)
229236
if err != nil {
230237
log.Printf("Async group/direct write failure: %v", err)
231238
}
232239
}(incoming)
233240

234-
outboundBytes, _ := json.Marshal(incoming)
241+
var outboundBytes []byte
242+
if incoming.ReplyToMessageID != "" {
243+
if q, err := msgRepo.GetMessagePreview(context.Background(), incoming.ReplyToMessageID); err == nil {
244+
enriched := map[string]interface{}{
245+
"type": incoming.Type,
246+
"sender_id": incoming.SenderID,
247+
"receiver_id": incoming.ReceiverID,
248+
"group_id": incoming.GroupID,
249+
"content": incoming.Content,
250+
"message_id": incoming.MessageID,
251+
"reply_to_message_id": incoming.ReplyToMessageID,
252+
"timestamp": incoming.Timestamp,
253+
"quoted_message": q,
254+
}
255+
outboundBytes, _ = json.Marshal(enriched)
256+
} else {
257+
outboundBytes, _ = json.Marshal(incoming)
258+
}
259+
} else {
260+
outboundBytes, _ = json.Marshal(incoming)
261+
}
235262

236263
if incoming.GroupID != "" {
237264
groupRepo := repository.NewGroupRepository()
@@ -256,7 +283,6 @@ func (c *WSClient) ReadPump() {
256283
}
257284
}
258285
Hub.mu.RUnlock()
259-
260286
} else {
261287
Hub.mu.RLock()
262288
targetClient, online := Hub.clients[strings.ToLower(incoming.ReceiverID)]

0 commit comments

Comments
 (0)