From ff6188ea8789624e814fc8bb29360c11349c61b9 Mon Sep 17 00:00:00 2001 From: Suman Biswas Date: Sat, 4 Jul 2026 16:04:17 +0530 Subject: [PATCH 1/2] comment fix --- server/controllers/auth.go | 2 -- server/controllers/messages.go | 2 +- server/controllers/users.go | 1 - server/controllers/ws.go | 1 - server/env/env.go | 3 +-- server/middlewares/cors.go | 1 - server/repository/user_repository_test.go | 1 - 7 files changed, 2 insertions(+), 9 deletions(-) diff --git a/server/controllers/auth.go b/server/controllers/auth.go index eb0a5f6..93a1441 100644 --- a/server/controllers/auth.go +++ b/server/controllers/auth.go @@ -40,7 +40,6 @@ func HandleRegister(w http.ResponseWriter, r *http.Request) { return } - // Generate the tokens tokens, err := services.GenerateTokenPair(user.ID) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -51,7 +50,6 @@ func HandleRegister(w http.ResponseWriter, r *http.Request) { return } - // FIX: Persist the registration refresh token to the database tokenRepo := repository.NewRefreshTokenRepository() hashedRt := services.HashToken(tokens.RefreshToken) expiry := time.Now().Add(7 * 24 * time.Hour) diff --git a/server/controllers/messages.go b/server/controllers/messages.go index b7cd5be..ddbfbf5 100644 --- a/server/controllers/messages.go +++ b/server/controllers/messages.go @@ -68,7 +68,7 @@ func HandleGetChatHistory(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(limitStr) if limit <= 0 || limit > 100 { - limit = 50 // Reference requirement ceiling limit mapping + limit = 50 } before := time.Now() diff --git a/server/controllers/users.go b/server/controllers/users.go index 3c6246e..298150a 100644 --- a/server/controllers/users.go +++ b/server/controllers/users.go @@ -15,7 +15,6 @@ import ( func HandleUserSearch(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - // Extract parameters q := r.URL.Query().Get("q") pageStr := r.URL.Query().Get("page") limitStr := r.URL.Query().Get("limit") diff --git a/server/controllers/ws.go b/server/controllers/ws.go index 03b8ae0..e2e748e 100644 --- a/server/controllers/ws.go +++ b/server/controllers/ws.go @@ -41,7 +41,6 @@ func HandleWSUpgrade(w http.ResponseWriter, r *http.Request) { services.Hub.Register <- client - // Kick off matching concurrency workers go client.WritePump() go client.ReadPump() } diff --git a/server/env/env.go b/server/env/env.go index 8d417aa..095f48c 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -42,7 +42,6 @@ func init() { } } - // assign the values with fallbacks HOST = getEnv("HOST", "127.0.0.1") PORT = getEnv("PORT", "3000") @@ -64,4 +63,4 @@ func getEnv(key, fallback string) string { return value } return fallback -} \ No newline at end of file +} diff --git a/server/middlewares/cors.go b/server/middlewares/cors.go index 48075c8..858b822 100644 --- a/server/middlewares/cors.go +++ b/server/middlewares/cors.go @@ -4,7 +4,6 @@ import "net/http" func CorsHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Allows all origins for development; can be bound to specific domains via env later w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") diff --git a/server/repository/user_repository_test.go b/server/repository/user_repository_test.go index cd18f54..9d28665 100644 --- a/server/repository/user_repository_test.go +++ b/server/repository/user_repository_test.go @@ -7,7 +7,6 @@ import ( "github.com/commandlinecoding/elephant/server/config" ) -// will have to remove manually deleteing users. Will be updated after delete users function is implemented func TestUserRepository_create_and_search(t *testing.T) { ctx := context.Background() From 04c7b1616a013f01df525ca995f8ebc248504dbf Mon Sep 17 00:00:00 2001 From: Suman Biswas Date: Sat, 4 Jul 2026 16:12:06 +0530 Subject: [PATCH 2/2] reply to messages --- server/controllers/messages.go | 7 +-- .../007_add_message_reply_to_up.sql | 5 ++ server/models/message.go | 25 ++++++---- server/models/ws.go | 15 +++--- server/repository/groups.go | 30 ++++++++---- server/repository/message.go | 46 ++++++++++++++----- server/services/message.go | 6 +-- server/services/ws.go | 36 +++++++++++++-- 8 files changed, 125 insertions(+), 45 deletions(-) create mode 100644 server/migrations/007_add_message_reply_to_up.sql diff --git a/server/controllers/messages.go b/server/controllers/messages.go index ddbfbf5..ea0eb74 100644 --- a/server/controllers/messages.go +++ b/server/controllers/messages.go @@ -13,8 +13,9 @@ import ( ) type SendMessageReq struct { - ReceiverID string `json:"receiver_id"` - Content string `json:"content"` + ReceiverID string `json:"receiver_id"` + Content string `json:"content"` + ReplyToMessageID string `json:"reply_to_message_id,omitempty"` } func HandleSendMessage(w http.ResponseWriter, r *http.Request) { @@ -41,7 +42,7 @@ func HandleSendMessage(w http.ResponseWriter, r *http.Request) { } svc := services.NewMessageService() - msg, err := svc.SendMessage(r.Context(), senderID, req.ReceiverID, req.Content) + msg, err := svc.SendMessage(r.Context(), senderID, req.ReceiverID, req.Content, req.ReplyToMessageID) if err != nil { w.WriteHeader(http.StatusBadRequest) _ = json.NewEncoder(w).Encode(models.JSONResponse{Success: false, Error: err.Error()}) diff --git a/server/migrations/007_add_message_reply_to_up.sql b/server/migrations/007_add_message_reply_to_up.sql new file mode 100644 index 0000000..fd66db5 --- /dev/null +++ b/server/migrations/007_add_message_reply_to_up.sql @@ -0,0 +1,5 @@ +ALTER TABLE messages ADD COLUMN reply_to_message_id UUID REFERENCES messages(id) ON DELETE SET NULL; + +CREATE INDEX IF NOT EXISTS idx_messages_reply_to +ON messages(reply_to_message_id) +WHERE reply_to_message_id IS NOT NULL; \ No newline at end of file diff --git a/server/models/message.go b/server/models/message.go index b8d0791..9bf3583 100644 --- a/server/models/message.go +++ b/server/models/message.go @@ -2,12 +2,21 @@ package models import "time" +// QuotedMessage represents a lightweight snapshot of the parent message being replied to +type QuotedMessage struct { + ID string `json:"id"` + SenderID string `json:"sender_id"` + Content string `json:"content"` +} + type Message struct { - ID string `json:"id"` - SenderID string `json:"sender_id"` - ReceiverID string `json:"receiver_id,omitempty"` - GroupID string `json:"group_id,omitempty"` - Content string `json:"content"` - CreatedAt time.Time `json:"created_at"` - IsRead bool `json:"is_read"` -} \ No newline at end of file + ID string `json:"id"` + SenderID string `json:"sender_id"` + ReceiverID string `json:"receiver_id,omitempty"` + GroupID string `json:"group_id,omitempty"` + Content string `json:"content"` + CreatedAt time.Time `json:"created_at"` + IsRead bool `json:"is_read"` + ReplyToMessageID *string `json:"reply_to_message_id,omitempty"` + QuotedMessage *QuotedMessage `json:"quoted_message,omitempty"` +} diff --git a/server/models/ws.go b/server/models/ws.go index b007b9b..b510008 100644 --- a/server/models/ws.go +++ b/server/models/ws.go @@ -3,11 +3,12 @@ package models import "time" type WSMessage struct { - Type string `json:"type"` - SenderID string `json:"sender_id,omitempty"` - ReceiverID string `json:"receiver_id,omitempty"` - GroupID string `json:"group_id,omitempty"` - Content string `json:"content,omitempty"` - MessageID string `json:"message_id,omitempty"` - Timestamp time.Time `json:"timestamp,omitempty"` + Type string `json:"type"` + SenderID string `json:"sender_id,omitempty"` + ReceiverID string `json:"receiver_id,omitempty"` + GroupID string `json:"group_id,omitempty"` + Content string `json:"content,omitempty"` + MessageID string `json:"message_id,omitempty"` + ReplyToMessageID string `json:"reply_to_message_id,omitempty"` + Timestamp time.Time `json:"timestamp,omitempty"` } diff --git a/server/repository/groups.go b/server/repository/groups.go index 6494de5..c76cf91 100644 --- a/server/repository/groups.go +++ b/server/repository/groups.go @@ -105,10 +105,13 @@ func (r *GroupRepository) GetGroupMembers(ctx context.Context, groupID string) ( func (r *GroupRepository) GetGroupMessages(ctx context.Context, groupID string, before time.Time, limit int) ([]models.Message, error) { query := ` - SELECT id, sender_id, content, created_at - FROM messages - WHERE group_id = $1 AND created_at < $2 - ORDER BY created_at DESC + SELECT + m.id, m.sender_id, m.content, m.created_at, m.reply_to_message_id, + q.sender_id AS quoted_sender_id, q.content AS quoted_content + FROM messages m + LEFT JOIN messages q ON m.reply_to_message_id = q.id + WHERE m.group_id = $1::uuid AND m.created_at < $2 + ORDER BY m.created_at DESC LIMIT $3; ` rows, err := config.DB.Query(ctx, query, groupID, before, limit) @@ -117,16 +120,27 @@ func (r *GroupRepository) GetGroupMessages(ctx context.Context, groupID string, } defer rows.Close() - messages := []models.Message{} + var history []models.Message = []models.Message{} for rows.Next() { var m models.Message - if err := rows.Scan(&m.ID, &m.SenderID, &m.Content, &m.CreatedAt); err != nil { + var qSender, qContent *string + + err := rows.Scan(&m.ID, &m.SenderID, &m.Content, &m.CreatedAt, &m.ReplyToMessageID, &qSender, &qContent) + if err != nil { return nil, err } + m.GroupID = groupID - messages = append(messages, m) + if m.ReplyToMessageID != nil && qSender != nil && qContent != nil { + m.QuotedMessage = &models.QuotedMessage{ + ID: *m.ReplyToMessageID, + SenderID: *qSender, + Content: *qContent, + } + } + history = append(history, m) } - return messages, nil + return history, nil } func (r *GroupRepository) GetMembersDetails(ctx context.Context, groupID string) ([]models.GroupMemberDetail, error) { diff --git a/server/repository/message.go b/server/repository/message.go index 674221d..2a7d4ef 100644 --- a/server/repository/message.go +++ b/server/repository/message.go @@ -14,18 +14,19 @@ func NewMessageRepository() *MessageRepository { return &MessageRepository{} } -func (r *MessageRepository) CreateMessage(ctx context.Context, senderID, receiverID, content string) (*models.Message, error) { +func (r *MessageRepository) CreateMessage(ctx context.Context, senderID, receiverID, content, replyToID string) (*models.Message, error) { query := ` - INSERT INTO messages (sender_id, receiver_id, content) - VALUES ($1, $2, $3) - RETURNING id, sender_id, receiver_id, content, created_at; + INSERT INTO messages (sender_id, receiver_id, content, reply_to_message_id) + VALUES ($1, $2, $3, NULLIF($4, '')::uuid) + RETURNING id, sender_id, receiver_id, content, reply_to_message_id, created_at; ` var msg models.Message - err := config.DB.QueryRow(ctx, query, senderID, receiverID, content).Scan( + err := config.DB.QueryRow(ctx, query, senderID, receiverID, content, replyToID).Scan( &msg.ID, &msg.SenderID, &msg.ReceiverID, &msg.Content, + &msg.ReplyToMessageID, &msg.CreatedAt, ) if err != nil { @@ -36,11 +37,14 @@ func (r *MessageRepository) CreateMessage(ctx context.Context, senderID, receive func (r *MessageRepository) GetChatHistory(ctx context.Context, userA, userB string, before time.Time, limit int) ([]models.Message, error) { query := ` - SELECT id, sender_id, receiver_id, content, created_at, is_read - FROM messages - WHERE ((sender_id = $1 AND receiver_id = $2) OR (sender_id = $2 AND receiver_id = $1)) - AND created_at < $3 - ORDER BY created_at DESC + SELECT + m.id, m.sender_id, m.receiver_id, m.content, m.created_at, m.is_read, m.reply_to_message_id, + q.sender_id AS quoted_sender_id, q.content AS quoted_content + FROM messages m + LEFT JOIN messages q ON m.reply_to_message_id = q.id + WHERE ((m.sender_id = $1 AND m.receiver_id = $2) OR (m.sender_id = $2 AND m.receiver_id = $1)) + AND m.group_id IS NULL AND m.created_at < $3 + ORDER BY m.created_at DESC LIMIT $4; ` 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 var history []models.Message = []models.Message{} for rows.Next() { var m models.Message - err := rows.Scan(&m.ID, &m.SenderID, &m.ReceiverID, &m.Content, &m.CreatedAt, &m.IsRead) + var qSender, qContent *string + + err := rows.Scan(&m.ID, &m.SenderID, &m.ReceiverID, &m.Content, &m.CreatedAt, &m.IsRead, &m.ReplyToMessageID, &qSender, &qContent) if err != nil { return nil, err } + + if m.ReplyToMessageID != nil && qSender != nil && qContent != nil { + m.QuotedMessage = &models.QuotedMessage{ + ID: *m.ReplyToMessageID, + SenderID: *qSender, + Content: *qContent, + } + } history = append(history, m) } return history, nil } +func (r *MessageRepository) GetMessagePreview(ctx context.Context, messageID string) (*models.QuotedMessage, error) { + query := `SELECT id, sender_id, content FROM messages WHERE id = $1::uuid;` + var q models.QuotedMessage + err := config.DB.QueryRow(ctx, query, messageID).Scan(&q.ID, &q.SenderID, &q.Content) + if err != nil { + return nil, err + } + return &q, nil +} + func (r *MessageRepository) GetConversations(ctx context.Context, userID string) ([]models.Conversation, error) { query := ` WITH raw_conversations AS ( diff --git a/server/services/message.go b/server/services/message.go index 9cf39cc..b7f89ca 100644 --- a/server/services/message.go +++ b/server/services/message.go @@ -17,7 +17,7 @@ func NewMessageService() *MessageService { return &MessageService{repo: repository.NewMessageRepository()} } -func (s *MessageService) SendMessage(ctx context.Context, senderID, receiverID, content string) (*models.Message, error) { +func (s *MessageService) SendMessage(ctx context.Context, senderID, receiverID, content, replyToMessageID string) (*models.Message, error) { content = strings.TrimSpace(content) if content == "" { return nil, errors.New("message content cannot be empty") @@ -25,5 +25,5 @@ func (s *MessageService) SendMessage(ctx context.Context, senderID, receiverID, if senderID == receiverID { return nil, errors.New("cannot send a message to yourself") } - return s.repo.CreateMessage(ctx, senderID, receiverID, content) -} \ No newline at end of file + return s.repo.CreateMessage(ctx, senderID, receiverID, content, replyToMessageID) +} diff --git a/server/services/ws.go b/server/services/ws.go index 0a88cb1..2e521a7 100644 --- a/server/services/ws.go +++ b/server/services/ws.go @@ -222,16 +222,43 @@ func (c *WSClient) ReadPump() { go func(msg models.WSMessage) { query := ` - INSERT INTO messages (sender_id, receiver_id, group_id, content, id) - VALUES ($1::uuid, NULLIF($2, '')::uuid, NULLIF($3, '')::uuid, $4, COALESCE(NULLIF($5, '')::uuid, gen_random_uuid())); + INSERT INTO messages (sender_id, receiver_id, group_id, content, id, reply_to_message_id) + VALUES ( + $1::uuid, + NULLIF($2, '')::uuid, + NULLIF($3, '')::uuid, + $4, + COALESCE(NULLIF($5, '')::uuid, gen_random_uuid()), + NULLIF($6, '')::uuid + ); ` - _, err := config.DB.Exec(context.Background(), query, msg.SenderID, msg.ReceiverID, msg.GroupID, msg.Content, msg.MessageID) + _, err := config.DB.Exec(context.Background(), query, msg.SenderID, msg.ReceiverID, msg.GroupID, msg.Content, msg.MessageID, msg.ReplyToMessageID) if err != nil { log.Printf("Async group/direct write failure: %v", err) } }(incoming) - outboundBytes, _ := json.Marshal(incoming) + var outboundBytes []byte + if incoming.ReplyToMessageID != "" { + if q, err := msgRepo.GetMessagePreview(context.Background(), incoming.ReplyToMessageID); err == nil { + enriched := map[string]interface{}{ + "type": incoming.Type, + "sender_id": incoming.SenderID, + "receiver_id": incoming.ReceiverID, + "group_id": incoming.GroupID, + "content": incoming.Content, + "message_id": incoming.MessageID, + "reply_to_message_id": incoming.ReplyToMessageID, + "timestamp": incoming.Timestamp, + "quoted_message": q, + } + outboundBytes, _ = json.Marshal(enriched) + } else { + outboundBytes, _ = json.Marshal(incoming) + } + } else { + outboundBytes, _ = json.Marshal(incoming) + } if incoming.GroupID != "" { groupRepo := repository.NewGroupRepository() @@ -256,7 +283,6 @@ func (c *WSClient) ReadPump() { } } Hub.mu.RUnlock() - } else { Hub.mu.RLock() targetClient, online := Hub.clients[strings.ToLower(incoming.ReceiverID)]