Skip to content

Commit

Permalink
Merge pull request #23 from igh9410/dev
Browse files Browse the repository at this point in the history
Chat Room client one-to-one almost done
  • Loading branch information
igh9410 authored Nov 9, 2023
2 parents db381ae + 3884a6a commit 76e708b
Show file tree
Hide file tree
Showing 25 changed files with 434 additions and 108 deletions.
2 changes: 2 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ index2.html
dockerbuild.sh
*.sh

./myenv

# Editor directories and files
.vscode/*
!.vscode/extensions.json
Expand Down
50 changes: 50 additions & 0 deletions backend/internal/chat/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"fmt"
"log"
"net/http"
"strconv"
"time"

"github.com/gin-gonic/gin"
)
Expand Down Expand Up @@ -70,3 +72,51 @@ func (h *Handler) JoinChatRoom(c *gin.Context) {
c.JSON(http.StatusOK, res)

}

func (h *Handler) GetChatMessages(c *gin.Context) {
// Extract ChatRoomID from the context
chatRoomID, err := common.ChatRoomIDValidator(c)
if err != nil {
log.Printf("Error occurred with chat room ID: %v", err.Error())
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid chat room ID"})
return
}

// Extract the cursor and pageSize from query parameters
cursorStr := c.DefaultQuery("cursor", "") // Cursor is provided by the client
pageSize := c.DefaultQuery("pageSize", "50")

// Parse pageSize to an integer
pageSizeInt, err := strconv.Atoi(pageSize)
if err != nil || pageSizeInt <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid page size"})
return
}

// Parse the cursor string to a time.Time
var cursorTime *time.Time
if cursorStr != "" {
parsedCursor, err := time.Parse(time.RFC3339, cursorStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid cursor"})
return
}
cursorTime = &parsedCursor
}

// Call the service function to get paginated messages
messages, err := h.Service.GetPaginatedMessages(c.Request.Context(), chatRoomID, cursorTime, pageSizeInt)
if err != nil {
log.Printf("Error fetching messages: %v", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error fetching messages"})
return
}

// Determine the next cursor
var nextCursor string
if len(messages) > 0 {
nextCursor = messages[len(messages)-1].CreatedAt.Format(time.RFC3339)
}

c.JSON(http.StatusOK, gin.H{"messages": messages, "nextCursor": nextCursor})
}
16 changes: 8 additions & 8 deletions backend/internal/chat/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ type ChatRoom struct {
}

type Message struct {
ID int `json:"id"`
ChatRoomID uuid.UUID `json:"chat_room_id"`
SenderID uuid.UUID `json:"sender_id"`
Content string `json:"content"`
MediaURL string `json:"media_url"`
CreatedAt time.Time `json:"created_at"`
ReadAt time.Time `json:"read_at"`
DeletedByUserID uuid.UUID `json:"deleted_by_user_id"`
ID int `json:"id"`
ChatRoomID uuid.UUID `json:"chat_room_id"`
SenderID uuid.UUID `json:"sender_id"`
Content string `json:"content"`
MediaURL string `json:"media_url"`
CreatedAt time.Time `json:"created_at"`
ReadAt *time.Time `json:"read_at,omitempty"`
DeletedByUserID *uuid.UUID `json:"deleted_by_user_id,omitempty"`
}

type UserInChatRoom struct {
Expand Down
94 changes: 50 additions & 44 deletions backend/internal/chat/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"log"
"time"

Expand All @@ -17,8 +18,7 @@ type Repository interface {
FetchRecentMessages(ctx context.Context, chatRoomID uuid.UUID, limit int) ([]Message, error)
JoinChatRoomByID(ctx context.Context, chatRoomID uuid.UUID, userID uuid.UUID) (*ChatRoom, error)
SaveMessage(ctx context.Context, message *Message) error
//FetchMessagesFromRedis(ctx context.Context, chatRoomID uuid.UUID, limit int) ([]Message, error)
FetchMessagesFromDatabase(ctx context.Context, chatRoomID uuid.UUID, page int, limit int) ([]Message, error)
GetPaginatedMessages(ctx context.Context, chatRoomID uuid.UUID, cursor *time.Time, pageSize int) ([]Message, error)
}

type DBTX interface {
Expand Down Expand Up @@ -172,65 +172,71 @@ func (r *repository) SaveMessage(ctx context.Context, message *Message) error {
return nil
}

/*
func (r *repository) FetchMessagesFromRedis(ctx context.Context, chatRoomID uuid.UUID, limit int) ([]Message, error) {
redisKey := "chatroom:" + chatRoomID.String() + ":messages"
messagesJSON, err := r.redisClient.LRange(ctx, redisKey, -limit, -1).Result()
if err != nil {
if err == redis.Nil {
// Redis key does not exist
return nil, nil
}
// Some other error occurred
return nil, err
}
var messages []Message
for _, mJSON := range messagesJSON {
var msg Message
err := json.Unmarshal([]byte(mJSON), &msg)
if err != nil {
log.Printf("Failed to unmarshal message: %v", err)
// Decide how you want to handle partial failure
continue
// GetPaginatedMessages retrieves messages from the database with pagination.
func (r *repository) GetPaginatedMessages(ctx context.Context, chatRoomID uuid.UUID, cursor *time.Time, pageSize int) ([]Message, error) {
/*
offset := (pageNum - 1) * pageSize // calculate the offset
if offset < 0 {
offset = 0 // ensure offset is not negative
}
messages = append(messages, msg)
}
return messages, nil
} */

// FetchMessagesFromDatabase retrieves chat messages from PostgreSQL
func (r *repository) FetchMessagesFromDatabase(ctx context.Context, chatRoomID uuid.UUID, page int, limit int) ([]Message, error) {
offset := page * limit
query := `
SELECT id, chat_room_id, sender_id, content, media_url, created_at, read_at, deleted_by_user_id
FROM messages
WHERE chat_room_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
` */
query := `
SELECT id, chat_room_id, sender_id, content, created_at
SELECT id, chat_room_id, sender_id, content, media_url, created_at, read_at, deleted_by_user_id
FROM messages
WHERE chat_room_id = $1
WHERE chat_room_id = $1 AND created_at < $2
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
LIMIT $3
`
rows, err := r.db.QueryContext(ctx, query, chatRoomID, limit, offset)
rows, err := r.db.QueryContext(ctx, query, chatRoomID, cursor, pageSize)
if err != nil {
log.Printf("Failed to fetch messages from database: %v", err)
return nil, err
return nil, fmt.Errorf("querying for paginated messages: %w", err)
}
defer rows.Close()

var messages []Message
for rows.Next() {
var msg Message
if err := rows.Scan(&msg.ID, &msg.ChatRoomID, &msg.SenderID, &msg.Content, &msg.CreatedAt); err != nil {
log.Printf("Failed to scan message: %v", err)
// Decide how you want to handle partial failure
continue
var readAt sql.NullTime
var deletedByUserID sql.NullString // Use sql.NullString for UUID fields that can be NULL

if err := rows.Scan(
&msg.ID,
&msg.ChatRoomID,
&msg.SenderID,
&msg.Content,
&msg.MediaURL,
&msg.CreatedAt,
&readAt,
&deletedByUserID,
); err != nil {
return nil, fmt.Errorf("scanning message: %w", err)
}

// Check if readAt is valid, if so, assign it to the struct
if readAt.Valid {
msg.ReadAt = &readAt.Time
}

// Check if deletedByUserID is valid, if so, convert to uuid.UUID and assign it to the struct
if deletedByUserID.Valid {
uid, err := uuid.Parse(deletedByUserID.String)
if err != nil {
return nil, fmt.Errorf("parsing UUID: %w", err)
}
msg.DeletedByUserID = &uid
}

messages = append(messages, msg)
}

// Check for any error that occurred during iteration
if err = rows.Err(); err != nil {
return nil, err
return nil, fmt.Errorf("iterating rows: %w", err)
}

return messages, nil
Expand Down
40 changes: 40 additions & 0 deletions backend/internal/chat/repository_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package chat

import (
"context"
"testing"

"github.com/DATA-DOG/go-sqlmock"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)

func TestCreateChatRoom(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("An error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()

repo := NewRepository(db)

chatRoom := &ChatRoom{}
expectedID := uuid.New()

// Set up the expectations
mock.ExpectQuery("INSERT INTO chat_rooms").
WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedID))

// Call the method under test
result, err := repo.CreateChatRoom(context.Background(), chatRoom)

// Assert the expectations
assert.NoError(t, err)
assert.Equal(t, expectedID, result.ID)

// Ensure all expectations were met
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
23 changes: 11 additions & 12 deletions backend/internal/chat/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Service interface {
GetChatRoomInfoByID(ctx context.Context, chatRoomID uuid.UUID) (*ChatRoomInfo, error)
JoinChatRoomByID(ctx context.Context, chatRoomID uuid.UUID, userID uuid.UUID) (*ChatRoom, error)
RegisterClient(ctx context.Context, hub *Hub, conn *websocket.Conn, chatroomID uuid.UUID, userID uuid.UUID, kafkaProducer *confluentKafka.Producer) (*Client, error)
GetPaginatedMessages(ctx context.Context, chatRoomID uuid.UUID, cursorTime *time.Time, pageSize int) ([]Message, error)
}

type service struct {
Expand Down Expand Up @@ -88,18 +89,6 @@ func (s *service) GetChatRoomInfoByID(ctx context.Context, chatRoomID uuid.UUID)
return chatRoomInfo, nil
}

/*
func (s *service) GetChatMessages(ctx context.Context, chatRoomID uuid.UUID, page int) ([]Message, error) {
// Attempt to fetch from Redis
messages, err := s.Repository.FetchMessagesFromRedis(ctx, chatRoomID, page)
if err == nil && len(messages) > 0 {
return messages, nil
}
// Fallback to another method if Redis is empty or there's an error
return s.Repository.FetchMessagesFromDatabase(ctx, chatRoomID, page, 0)
} */

func (s *service) RegisterClient(ctx context.Context, hub *Hub, conn *websocket.Conn, chatroomID uuid.UUID, userID uuid.UUID, kafkaProducer *confluentKafka.Producer) (*Client, error) {

senderID := userID
Expand All @@ -110,3 +99,13 @@ func (s *service) RegisterClient(ctx context.Context, hub *Hub, conn *websocket.
client.hub.register <- client
return client, nil
}

func (s *service) GetPaginatedMessages(ctx context.Context, chatRoomID uuid.UUID, cursorTime *time.Time, pageSize int) ([]Message, error) {

res, err := s.Repository.GetPaginatedMessages(ctx, chatRoomID, cursorTime, pageSize)
if err != nil {
log.Printf("Error occured with getting paginated messages for chat room ID %v: %v", chatRoomID, err)
return nil, err
}
return res, nil
}
1 change: 1 addition & 0 deletions backend/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func InitRouter(cfg *RouterConfig) {
chatRoutes.POST("/", cfg.ChatHandler.CreateChatRoom)
chatRoutes.GET("/:id", cfg.ChatHandler.GetChatRoom)
chatRoutes.POST("/:id", cfg.ChatHandler.JoinChatRoom)
chatRoutes.GET("/:id/messages", cfg.ChatHandler.GetChatMessages)
// etc...
}

Expand Down
12 changes: 12 additions & 0 deletions backend/scripts/query-analyze.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
EXPLAIN ANALYZE SELECT id, chat_room_id, sender_id, content, media_url, created_at, read_at, deleted_by_user_id
FROM messages
WHERE chat_room_id = '21e546be-629d-4e84-9236-b05c69becb18'
ORDER BY created_at DESC
LIMIT 50 OFFSET 0;

EXPLAIN ANALYZE SELECT id, chat_room_id, sender_id, content, media_url, created_at, read_at, deleted_by_user_id
FROM messages
WHERE chat_room_id = '21e546be-629d-4e84-9236-b05c69becb18'
AND id < 12000
ORDER BY id DESC
LIMIT 50;
5 changes: 5 additions & 0 deletions frontend/src/components/Layout/Layout.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.layout {
display: flex;
flex-direction: column;
height: 100vh; /* This ensures the container takes the full height of the viewport */
}
16 changes: 16 additions & 0 deletions frontend/src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import styles from './Layout.module.scss';
import { Header } from '@components/Header';

type LayoutProps = {
children: React.ReactNode;
};

export const Layout = ({ children }: LayoutProps) => {
return (
<div className={styles.layout}>
<Header />
{children}
</div>
);
};
Loading

0 comments on commit 76e708b

Please sign in to comment.