diff --git a/Project/Firebase/database.go b/Project/Firebase/database.go index cee665ecc5a5cb7d41812dc9ab29fe14f8c5dcc8..ff4da4e9864239843620dc8d8aef4d68c41936c2 100644 --- a/Project/Firebase/database.go +++ b/Project/Firebase/database.go @@ -1,5 +1,12 @@ package firebase +import "cloud.google.com/go/firestore" + +type Sort struct { + Field string + Order firestore.Direction +} + type Document struct { Id string Data map[string]interface{} @@ -8,7 +15,7 @@ type Document struct { type Database interface { //Close() error - PostDoc(path string, values map[string]interface{}) error + PostDoc(path string, values map[string]interface{}) (string, error) PostDocId(path string, id string, values map[string]interface{}) error GetDoc(path string) (Document, error) @@ -22,7 +29,7 @@ type Database interface { IncrementDocField(path string, fields map[string]interface{}) error - GetPaginatedDocs(path string, pageNum int, pageSize int) ([]Document, error) + GetPaginatedDocs(path string, pageNum int, pageSize int, offset int, sortBy *Sort) ([]Document, error) SearchDocs(path string, searchTerm string, pageNum int, pageSize int) ([]Document, error) } diff --git a/Project/Firebase/firestore.go b/Project/Firebase/firestore.go index 0a6065e3cf4558d86072ad539d5f6d6169610cb2..a03c47daed627d219d7dd4793be499db60a231f8 100644 --- a/Project/Firebase/firestore.go +++ b/Project/Firebase/firestore.go @@ -85,10 +85,6 @@ func (fs Firestore) UpdateDoc(path string, newValues map[string]interface{}) err // Get documents with fields equal to specified values. With no conditions(nil), this function will return all documents func (fs Firestore) GetDocsWhere(path string, conditionValues map[string]interface{}) ([]Document, error) { - /*if len(conditionValues) == 0 { - return []Document{}, errors.New("no conditions") - }*/ - collection, id, subCollection, _, err := SplitPath(path) if err != nil { return []Document{}, err @@ -245,10 +241,10 @@ func (fs Firestore) UpdateDocsWhere(path string, newValues map[string]interface{ } // Post document -func (fs Firestore) PostDoc(path string, values map[string]interface{}) error { +func (fs Firestore) PostDoc(path string, values map[string]interface{}) (string, error) { collection, id, subCollection, _, err := SplitPath(path) if err != nil { - return err + return "", err } ref := fs.Client.Collection(collection).Doc(id) @@ -260,10 +256,10 @@ func (fs Firestore) PostDoc(path string, values map[string]interface{}) error { doc := ref.Parent.NewDoc() _, err = doc.Set(Ctx, values) if err != nil { - return err + return "", err } - return nil + return doc.ID, nil } // Post a document with specified id @@ -328,21 +324,27 @@ func (fs Firestore) IncrementDocField(path string, fields map[string]interface{} } // GetPaginatedDocs gets all documents from a specified collection based on pagination -func (fs Firestore) GetPaginatedDocs(path string, pageNum int, pageSize int) ([]Document, error) { +func (fs Firestore) GetPaginatedDocs(path string, pageNum int, pageSize int, offset int, sortBy *Sort) ([]Document, error) { // Validate parameters if pageNum < 0 || pageSize < 0 { return nil, errors.New("invalid pagination values") } - collection, _, _, _, err := SplitPath(path) + collection, id, subCollection, subId, err := SplitPath(path) if err != nil { - return nil, err + return []Document{}, err } - query := fs.Client.Collection(collection).OrderBy("name", firestore.Asc). - Limit(pageSize). - Offset(pageNum * pageSize) + ref := fs.Client.Collection(collection).Doc(id) + if subCollection != "" { + ref = ref.Collection(subCollection).Doc(subId) + } + query := ref.Parent.Query + if sortBy != nil { + query = query.OrderBy(sortBy.Field, sortBy.Order) + } + query = query.Limit(pageSize).Offset(offset + pageNum*pageSize) iter := query.Documents(Ctx) defer iter.Stop() diff --git a/Project/Firebase/firestoreMock.go b/Project/Firebase/firestoreMock.go index 8bd96bfae00dc0598d5528e9d480cd960473bf74..aa7f0e129afe2812e28d9dc67c80dd969ff40f1b 100644 --- a/Project/Firebase/firestoreMock.go +++ b/Project/Firebase/firestoreMock.go @@ -77,7 +77,7 @@ func InitializeMockEnvironment() { "name": "Gam3", "ratingCount": int64(100), "ratingSum": int64(700), - "verticalImg": "vimg2.jpg", + "verticalImg": "vimg3.jpg", }, }, }, @@ -333,10 +333,10 @@ func (db MockFS) UpdateDocsWhere(path string, newValues map[string]interface{}, return nil } -func (db MockFS) PostDoc(path string, values map[string]interface{}) error { +func (db MockFS) PostDoc(path string, values map[string]interface{}) (string, error) { collection, _, err := db.checkForSubcollectionDoc(path) if err != nil { - return err + return "", err } // Generate id @@ -344,7 +344,7 @@ func (db MockFS) PostDoc(path string, values map[string]interface{}) error { collection[id] = values - return nil + return id, nil } // Post a document with specified id @@ -407,7 +407,7 @@ func (db MockFS) IncrementDocField(path string, fields map[string]interface{}) e } // GetPaginatedDocs retrieves a paginated list of documents from a collection. -func (db MockFS) GetPaginatedDocs(path string, pageSize int, pageNumber int) ([]Document, error) { +func (db MockFS) GetPaginatedDocs(path string, pageSize int, pageNumber int, offset int, sortBy *Sort) ([]Document, error) { return nil, nil } diff --git a/Project/Firebase/functions.go b/Project/Firebase/functions.go index de5f07af2c55f91de05eb7701ba8ee9c6b4916b0..39e57d21bb516fcaab578a83740cd0417c82c2e4 100644 --- a/Project/Firebase/functions.go +++ b/Project/Firebase/functions.go @@ -72,3 +72,47 @@ func RemoveFromJournal(userId string, gameId string) error { return nil } + +// Find chat between two users +func GetChat(user1 string, user2 string) (*Document, error) { + if user1 == user2 { + return nil, errors.New("sender and receiver cannot be the same") + } + + // Confirm the second user exists ( User 1 should already have been authenticated ) + /*path := "users/" + user2 + _, err := DB.GetDoc(path) + if err != nil { + return nil, errors.New("user does not exist") + }*/ + + // Check if there is an existing chat between the users + var chatDocs []Document + path := "chats" + conditionValues := map[string]interface{}{ + "user1": user1, + "user2": user2, + } + chatDocs, err := DB.GetDocsWhere(path, conditionValues) + if err != nil { + } + + // Check with switched users + if len(chatDocs) == 0 { + conditionValues = map[string]interface{}{ + "user1": user2, + "user2": user1, + } + chatDocs, err = DB.GetDocsWhere(path, conditionValues) + if err != nil { + return nil, err + } + + // Not found + if len(chatDocs) == 0 { + return nil, nil + } + } + + return &chatDocs[0], nil +} diff --git a/Project/Handlers/gameList.go b/Project/Handlers/gameList.go index 78598377f13c7adb7b1901c7b135eb5008aa7be2..4e930dc2978b2a45b6ad07db4f00cbe238b6e9f5 100644 --- a/Project/Handlers/gameList.go +++ b/Project/Handlers/gameList.go @@ -6,6 +6,8 @@ import ( "log" "net/http" "strconv" + + "cloud.google.com/go/firestore" ) // getGames gets all the games to be displayed on a page when browsing @@ -17,7 +19,11 @@ func GetGames(w http.ResponseWriter, r *http.Request) { pageNum, _ := strconv.Atoi(page) size, _ := strconv.Atoi(pageSize) - gameDocs, err := firebase.DB.GetPaginatedDocs("games", pageNum, size) + sortBy := firebase.Sort{ + Field: "name", + Order: firestore.Asc, + } + gameDocs, err := firebase.DB.GetPaginatedDocs("games", pageNum, size, 0, &sortBy) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/Project/Handlers/getMessages.go b/Project/Handlers/getMessages.go new file mode 100644 index 0000000000000000000000000000000000000000..4be6ff9d611826b083eee3da7b65cd08d6baa942 --- /dev/null +++ b/Project/Handlers/getMessages.go @@ -0,0 +1,93 @@ +package handlers + +import ( + "encoding/json" + firebase "go-firebase-auth/Firebase" + "net/http" + "strconv" + "time" + + "cloud.google.com/go/firestore" +) + +// Returns a messages from a chat between two users +func GetMessages(w http.ResponseWriter, r *http.Request) { + + // Get parameters + urlQuery := r.URL.Query() + token := urlQuery.Get("token") + secondUserId := urlQuery.Get("user") + page := r.URL.Query().Get("page") + pageSize := r.URL.Query().Get("pageSize") + offsetStr := r.URL.Query().Get("offset") + + pageNum, _ := strconv.Atoi(page) + size, _ := strconv.Atoi(pageSize) + offset, _ := strconv.Atoi(offsetStr) + + userId, err := firebase.GetUserId(token) + if err != nil { + http.Error(w, "Failed to authenticate user: "+err.Error(), http.StatusBadRequest) + return + } + + // Get the chat + chatDoc, err := firebase.GetChat(userId, secondUserId) + if err != nil { + http.Error(w, "Failed to get chat: "+err.Error(), http.StatusBadRequest) + return + } + + // Create a new chat if not found + if chatDoc == nil { + path := "chats" + docData := map[string]interface{}{ + "user1": userId, + "user2": secondUserId, + "startDate": time.Now(), + "blockedBy": "", + } + + chatId, err := firebase.DB.PostDoc(path, docData) + if err != nil { + http.Error(w, "Failed to initiate chat: "+err.Error(), http.StatusInternalServerError) + return + } + + chatDoc = &firebase.Document{ + Id: chatId, + Data: docData, + } + } + + chatId := chatDoc.Id + + path := "chats/" + chatId + "/messages" + sortBy := firebase.Sort{ + Field: "date", + Order: firestore.Desc, + } + + messageDocs, err := firebase.DB.GetPaginatedDocs(path, pageNum, size, offset, &sortBy) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var messages []Message + for _, doc := range messageDocs { + data := doc.Data + message := Message{ + Sender: data["sender"].(string), + Content: data["content"].(string), + Date: data["date"].(time.Time), + } + messages = append(messages, message) + } + + // Encode the response body and send it back to the client + err = json.NewEncoder(w).Encode(messages) + if err != nil { + http.Error(w, "Failed to encode body", http.StatusInternalServerError) + } +} diff --git a/Project/Handlers/getUser.go b/Project/Handlers/getUser.go new file mode 100644 index 0000000000000000000000000000000000000000..deb7b40b385effb314bb556ed87772702aca543a --- /dev/null +++ b/Project/Handlers/getUser.go @@ -0,0 +1,29 @@ +package handlers + +import ( + "encoding/json" + firebase "go-firebase-auth/Firebase" + "net/http" +) + +func GetUser(w http.ResponseWriter, r *http.Request) { + urlQuery := r.URL.Query() + userId := urlQuery.Get("user") + + path := "users/" + userId + userDoc, err := firebase.DB.GetDoc(path) + if err != nil { + http.Error(w, "Could not find user: "+err.Error(), http.StatusNotFound) + return + } + + response := User{ + Username: userDoc.Data["username"].(string), + } + + // Encode the response body and send it back to the client + err = json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, "Failed to encode body", http.StatusInternalServerError) + } +} diff --git a/Project/Handlers/getUserGameData.go b/Project/Handlers/getUserGameData.go index 24158c15a45a245b343cb3cd51ba35f39cb21951..12c5af8490456bd3551244fd22c56cc879f26adc 100644 --- a/Project/Handlers/getUserGameData.go +++ b/Project/Handlers/getUserGameData.go @@ -18,31 +18,37 @@ func GetUserGameData(w http.ResponseWriter, r *http.Request) { return } + var review string + var rating int + inJournal := true + path := "users/" + userId + "/journal/" + gameId doc, err := firebase.DB.GetDoc(path) if err != nil { - http.Error(w, "Game not in journal", http.StatusNotFound) - return + inJournal = false } - rating := int(doc.Data["rating"].(int64)) - path = "reviews" - conditionValues := map[string]interface{}{ - "user": userId, - "game": gameId, - } - - var review string - reviewDoc, _ := firebase.DB.GetDocsWhere(path, conditionValues) - if len(reviewDoc) > 0 { - review = reviewDoc[0].Data["content"].(string) - } else { - review = "" + if inJournal { + rating = int(doc.Data["rating"].(int64)) + + path = "reviews" + conditionValues := map[string]interface{}{ + "user": userId, + "game": gameId, + } + + reviewDoc, _ := firebase.DB.GetDocsWhere(path, conditionValues) + if len(reviewDoc) > 0 { + review = reviewDoc[0].Data["content"].(string) + } else { + review = "" + } } var userGameData = UserGameDataResponse{ - Rating: rating, - Review: review, + InJournal: inJournal, + Rating: rating, + Review: review, } // Encode the response body and send it back to the client diff --git a/Project/Handlers/sendMessage.go b/Project/Handlers/sendMessage.go new file mode 100644 index 0000000000000000000000000000000000000000..5963eb206d8b9d5746adb32b6dcebfae2cadecc2 --- /dev/null +++ b/Project/Handlers/sendMessage.go @@ -0,0 +1,73 @@ +package handlers + +import ( + "encoding/json" + firebase "go-firebase-auth/Firebase" + "go-firebase-auth/websocket" + "net/http" + "time" +) + +// Sends a message from one user to another +func SendMessage(w http.ResponseWriter, r *http.Request) { + // Decode request body + var body SendMessageBody + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&body) + if err != nil { + http.Error(w, "Failed to decode body: "+err.Error(), http.StatusBadRequest) + return + } + + // Authenticate user + userId, err := firebase.GetUserId(body.Token) + if err != nil { + http.Error(w, "Failed to authenticate user: "+err.Error(), http.StatusBadRequest) + return + } + secondUserId := body.SecondUser + + chatDoc, err := firebase.GetChat(userId, secondUserId) + if err != nil { + http.Error(w, "Failed to get chat: "+err.Error(), http.StatusBadRequest) + return + } + + // Create a new chat if not found + if chatDoc == nil { + path := "chats" + docData := map[string]interface{}{ + "user1": userId, + "user2": secondUserId, + "startDate": time.Now(), + "blockedBy": "", + } + + chatId, err := firebase.DB.PostDoc(path, docData) + if err != nil { + http.Error(w, "Failed to initiate chat: "+err.Error(), http.StatusInternalServerError) + return + } + + chatDoc = &firebase.Document{ + Id: chatId, + Data: docData, + } + } + + // Store the message + chatId := chatDoc.Id + path := "chats/" + chatId + "/messages" + _, err = firebase.DB.PostDoc(path, map[string]interface{}{ + "sender": userId, + "date": time.Now(), + "content": body.Content, + "read": false, + }) + if err != nil { + http.Error(w, "Failed to send message: "+err.Error(), http.StatusInternalServerError) + return + } + + websocket.SendMessageToUser(userId, secondUserId, body.Content) +} diff --git a/Project/Handlers/structs.go b/Project/Handlers/structs.go index b2539dd07bdc6d8c6b17f897b37e2f7391100c1c..0393f32c50220f2a5e2ce1b78dda426813685244 100644 --- a/Project/Handlers/structs.go +++ b/Project/Handlers/structs.go @@ -2,6 +2,7 @@ package handlers import ( "go-firebase-auth/apifunctions" + "time" ) type JustToken struct { @@ -15,8 +16,9 @@ type GamePageResponse struct { } type UserGameDataResponse struct { - Rating int `json:"rating"` - Review string `json:"review"` + InJournal bool `json:"inJournal"` + Rating int `json:"rating"` + Review string `json:"review"` } type Review struct { @@ -63,6 +65,22 @@ type UserRegisterBody struct { Username string `json:"username"` } +type SendMessageBody struct { + Token string `json:"token"` + SecondUser string `json:"secondUser"` + Content string `json:"content"` +} + +type Message struct { + Sender string `json:"sender"` + Content string `json:"content"` + Date time.Time `json:"date"` +} + +type User struct { + Username string `json:"username"` +} + type Comment struct { Content string `json:"content"` User string `json:"user"` diff --git a/Project/Handlers/submitUserGameData.go b/Project/Handlers/submitUserGameData.go index 82b0b973be3b3a1b71c9425d91b376f578a9c5c5..e3126da024d3c2fb6804bfc39baae14be16c2b2e 100644 --- a/Project/Handlers/submitUserGameData.go +++ b/Project/Handlers/submitUserGameData.go @@ -94,7 +94,7 @@ func SubmitUserGameData(w http.ResponseWriter, r *http.Request) { if len(reviews) == 0 { // No review found // Post review - err = firebase.DB.PostDoc(path, map[string]interface{}{ + _, err = firebase.DB.PostDoc(path, map[string]interface{}{ "content": body.Review, "game": body.GameId, "rating": body.Rating, diff --git a/Project/go.mod b/Project/go.mod index 59df33aa6c3c75e7ea2676581d07db72c6d9584e..d67370d786ac447c05cb17f4db15ba6df5af345d 100644 --- a/Project/go.mod +++ b/Project/go.mod @@ -26,6 +26,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/jarcoal/httpmock v1.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rjansen/raizel v0.1.0 // indirect diff --git a/Project/go.sum b/Project/go.sum index a264a1ee106b3d8bf5acfc97bf2a836b237b74a9..26623df805f71ac2ce24055d2e8974e03856ca6a 100644 --- a/Project/go.sum +++ b/Project/go.sum @@ -107,6 +107,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= diff --git a/Project/main.go b/Project/main.go index a904c49104c6b64f620d0fa16e33337ae29318d5..27b0469dcda6106d7646864517b210bffe012932 100644 --- a/Project/main.go +++ b/Project/main.go @@ -6,6 +6,7 @@ import ( firebase "go-firebase-auth/Firebase" handlers "go-firebase-auth/Handlers" "go-firebase-auth/apifunctions" + "go-firebase-auth/websocket" "log" "net/http" "path/filepath" @@ -35,6 +36,8 @@ func main() { http.ServeFile(w, r, filepath.Join("./static", "games.html")) }) + http.HandleFunc("/ws", websocket.HandleWebsocket) + http.HandleFunc("/gamepage", handlers.GamePage) http.HandleFunc("/getusergamedata", handlers.GetUserGameData) http.HandleFunc("/gamelist", handlers.GetGames) @@ -49,6 +52,9 @@ func main() { http.HandleFunc("/connectedtosteam", handlers.ConnectedToSteam) http.HandleFunc("/getuserjournal", handlers.GetUserJournal) http.HandleFunc("/getreviewsforgame", handlers.GetReviewsForGame) + http.HandleFunc("/sendmessage", handlers.SendMessage) + http.HandleFunc("/getmessages", handlers.GetMessages) + http.HandleFunc("/getuser", handlers.GetUser) http.HandleFunc("/submitusergamecomment", handlers.SubmitUserGameComment) http.HandleFunc("/getusergamecomments", handlers.GetUserGameComments) diff --git a/Project/static/chat.html b/Project/static/chat.html new file mode 100644 index 0000000000000000000000000000000000000000..eb41a834ef9fbb3586a16a9feca2625090e4db18 --- /dev/null +++ b/Project/static/chat.html @@ -0,0 +1,48 @@ +<!-- static/chat.html --> +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" href="/style/style_chat.css"> + <title>Chat</title> + <link rel="icon" href="img/LogoNew.png" type="image/x-icon"/> + + </head> + <body> + <header> + <div id="divHeader"> + <a href="index.html"><img src="img/LogoNew.png" alt="Joystick Journal Logo" id="imgLogo"></a> + <h1>Joystick Journal</h1> + </div> + </header> + + <nav> + <ul> + <li><a id="userPageButton" href="userpage/index.html">My Profile</a></li> + <li><a id="signOutButton" href="#">Sign out</a></li> + <li><a id="loginButton" href="login.html">Log in</a></li> + <li><a id="registerButton" href="register.html">Registrer deg</a></li> + </ul> + </nav> + + <div id="divContent"> + <p id="name"></p> + <div id = "chat"></div> + <form id="messageForm"> + <input id="content" type="text"> + <button id="sendMessage" type="button">Send</button> + + </form> + </div> + + + <footer> + <div> + <p>© 2024 Joystick Journal. All rights not reserved yet.</p> + </div> + </footer> + + <script type="module" src="js/chat.js"></script> + </body> +</html> diff --git a/Project/static/game.html b/Project/static/game.html index ff7c0342b3e79a088df3e9c25150bf575b869ba9..322ace47f97966418d2952c3dd0f9450226333c5 100644 --- a/Project/static/game.html +++ b/Project/static/game.html @@ -48,18 +48,6 @@ <button id = "addDelete">Add to journal</button> <form id="reviewForm"> - <label>Your rating: </label> - <select id="rating"> - <option value="0">Select rating</option> - </select> - - - <label>Write your review: </label> - <br> - <textarea id="review"></textarea> - <br> - <button id="submitRatingReview" type="button">Update</button> - <button id="deleteReview" type="button">Delete review</button> </form> </section> diff --git a/Project/static/js/chat.js b/Project/static/js/chat.js new file mode 100644 index 0000000000000000000000000000000000000000..3f34c42ad688a31729b9b8abe95da5a2a733bf76 --- /dev/null +++ b/Project/static/js/chat.js @@ -0,0 +1,192 @@ +import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js"; +import { getAuth, onAuthStateChanged, signOut } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js"; + +import { initFirebase, setupAuthStateListener, setupButtonHandlers, sendGETRequest, sendRequest } from "./utils.js"; + +const auth = initFirebase(); + +let socket + +const eMessageForm = document.getElementById("messageForm") +const eMessageContent = document.getElementById("content") +const eSendButton = document.getElementById("sendMessage") + +const eChat = document.getElementById("chat"); +const eName = document.getElementById("name"); + +// Get second user's id from url +const queryString = window.location.search; +const urlParams = new URLSearchParams(queryString); +const secondUserId = urlParams.get('user'); +let secondUserName = ""; + + +let currentUser; +let messages = []; +let loadedMessageCount = 0; // Messages sent or received through the websocket + + +setupAuthStateListener(auth, { + loginButton, + userPageButton, + signOutButton, + registerButton +}); + +setupButtonHandlers(auth, { + loginButton, + userPageButton, + signOutButton, + registerButton +}); + + +onAuthStateChanged(auth, async (user) => { + if (user) { + const idToken = await user.getIdToken(); + socket = new WebSocket(`ws://${window.location.hostname}:${window.location.port}/ws?token=${idToken}&user=${secondUserId}`); + + socket.addEventListener("message", (event) => receiveMessage(event)); + + currentUser = user; + + secondUserName = await getUsername(); + eName.innerHTML = "Chatting with " + secondUserName; + + if(secondUserName == "") { + document.body.innerHTML = "404 - user not found"; + } else { + loadMessages(0, 10, 0, true); + } + } else { + window.location.href = '/'; + } +}); + + +async function getUsername() { + + let name = localStorage.getItem(secondUserId) + + if(name == null) { + + try { + const request = "/getuser?user=" + secondUserId; + + let response = await fetch(request); + let data = await response.json(); + + localStorage.setItem(secondUserId, data.username); + + return data.username; + } catch (error) { + console.log("Error fetching second user: ", error); + return ""; + } + } else { + return name; + } +} + + +function receiveMessage(event) { + const content = event.data + let message = { sender: secondUserId, content: content, date: Date.now() }; + + // Determine if user has scrolled down to the last message + let scrollDown = (eChat.scrollTop == (eChat.scrollHeight - eChat.offsetHeight)); + + messages.push(message); + displayMessages(messages); + + loadedMessageCount++; + + if(scrollDown) { + eChat.scrollTop = eChat.scrollHeight; // Scroll to the bottom + } +} + +async function loadMessages(page, pageSize, offset, first) { + if(secondUserName != "") { + const idToken = await currentUser.getIdToken(); + const request = `/getmessages?token=${idToken}&user=${secondUserId}&page=${page}&pageSize=${pageSize}&offset=${offset}`; + + fetch(request) + .then(response => response.json()) + .then(data => { + messages = data.reverse().concat(messages); + displayMessages(messages); + + loadedMessageCount += data.length; + + if(first) { + eChat.scrollTop = eChat.scrollHeight; // Scroll down to the latest message + } + }) + .catch(error => console.error("Error fetching messages: ", error)); + } else { + document.body.innerHTML = "404 - not found"; + } +} + +function displayMessages(messages) { + eChat.innerHTML = ""; + //messages = messages.reverse(); + messages.forEach(m => { + // Create a message element + let message = document.createElement("div"); + message.className = "message " + (m.sender == currentUser.uid ? "you" : "other"); + let content = document.createElement("p"); + content.innerHTML = m.content; + message.appendChild(content); + + eChat.appendChild(message); + }); +} + + + +eSendButton.addEventListener('click', async function (event) { + const content = eMessageContent.value + if(content != "") { + let idToken = await currentUser.getIdToken(); + + const request = "/sendmessage" + fetch(request, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + "token": idToken, + "secondUser": secondUserId, + "content": content + }) + }).then(response => { + if(response.ok) { + let message = { sender: currentUser.uid, content: eMessageContent.value, date: Date.now() }; + + messages.push(message); + displayMessages(messages); + + loadedMessageCount++; + + // Scroll down to the sent message + eChat.scrollTop = eChat.scrollHeight; + + eMessageContent.value = ""; + } + }).catch((error) => { + console.error("Error sending message:", error); + }) + } else { + alert("You cannot send an empty message"); + } +}); + + +eChat.addEventListener("scroll", function(e) { + if(eChat.scrollTop == 0) { + loadMessages(0, 5, loadedMessageCount, false) + } +}); \ No newline at end of file diff --git a/Project/static/js/firebase-config.js b/Project/static/js/firebase-config.js index 8a13b6fa09ffcebff28a36d0550fb2ee7b37d918..9c2b145ebb3ff2784aede2a4642de1377433d616 100644 --- a/Project/static/js/firebase-config.js +++ b/Project/static/js/firebase-config.js @@ -5,4 +5,4 @@ export const firebaseConfig = { storageBucket: "joystick-journal.appspot.com", messagingSenderId: "808652272382", appId: "1:808652272382:web:0f7f71ea572316bb952baf" -}; \ No newline at end of file +}; diff --git a/Project/static/js/game.js b/Project/static/js/game.js index eb7ca97f1ba710de833705dc80073446a1cdc8df..37263ec66581bcd8915c3723cb19174b488ff7f4 100644 --- a/Project/static/js/game.js +++ b/Project/static/js/game.js @@ -1,22 +1,22 @@ import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js"; import { getAuth, onAuthStateChanged, signOut } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js"; -import { initFirebase, setupAuthStateListener, setupButtonHandlers } from "./utils.js"; -//import { json } from "sjcl"; +import { initFirebase, setupAuthStateListener, setupButtonHandlers, sendGETRequest, sendRequest } from "./utils.js"; const auth = initFirebase(); const eTitle = document.getElementById('title'); const eAvgRating = document.getElementById('avgRating'); const eDescription = document.getElementById('description'); -const eRating = document.getElementById('rating'); -const eReviewForm = document.getElementById('reviewForm'); -const eReview = document.getElementById('review'); -const eFormButton = document.getElementById('submitRatingReview'); -const eDeleteReviewButton = document.getElementById('deleteReview'); const addDeleteButton = document.getElementById('addDelete'); const eSubmitCommentButton = document.getElementById('submitComment'); const eComment = document.getElementById('comment'); +const eReviewForm = document.getElementById('reviewForm'); +let eReview = document.getElementById('review'); +let eFormButton = document.getElementById('submitRatingReview'); +let eDeleteReviewButton = document.getElementById('deleteReview'); +let eRating = document.getElementById('rating'); + const loginButton = document.getElementById('loginButton'); const userPageButton = document.getElementById('userPageButton'); const signOutButton = document.getElementById('signOutButton'); @@ -45,7 +45,15 @@ setupButtonHandlers(auth, { }); +function userRequests() { + return [ + "/getusergamedata?token=" + currentUser.uid + "&id=" + gameId, + "/getuserjournal?token=" + currentUser.uid + ]; +} + getGameData(); +showReviews(); onAuthStateChanged(auth, async (user) => { @@ -63,146 +71,149 @@ onAuthStateChanged(auth, async (user) => { // Get data for the game -function getGameData() { +async function getGameData() { + const request = "/gamepage?id=" + gameId; + let gameData = await sendGETRequest(request, true, request); + if (gameData != null) { + insertGameData(gameData); + } +} - let request = "/gamepage?id=" + gameId; +// Insert game data on the game page +function insertGameData(data) { + const game = data.game; - fetch(request).then( - response => response.json() - ).then(data => { - const game = data.game; + const ratingCount = data.ratingCount; + const ratingSum = data.ratingSum; - const ratingCount = data.ratingCount; - const ratingSum = data.ratingSum; + eTitle.innerHTML = game.name; + eDescription.innerHTML = game.detailed_description; - eTitle.innerHTML = game.name; - eDescription.innerHTML = game.detailed_description; + if(ratingCount == 0) { + eAvgRating.innerHTML = "No ratings have been given"; + } else { + eAvgRating.innerHTML = "Average rating: " + ratingSum/ratingCount; + } - if(ratingCount == 0) { - eAvgRating.innerHTML = "No ratings have been given"; - } else { - eAvgRating.innerHTML = "Average rating: " + ratingSum/ratingCount; - } + document.title = game.name +} - document.title = game.name + +// Get the user's rating and review for the game +async function getUserGameData(token) { + const request = "/getusergamedata?token=" + encodeURIComponent(token) + "&id=" + gameId; + const cacheRequest = "/getusergamedata?token=" + currentUser.uid + "&id=" + gameId; + + let userGameData = await sendGETRequest(request, true, cacheRequest); + if (userGameData != null && userGameData.inJournal) { + eReviewForm.innerHTML = ` + <label>Your rating: </label> + <select id="rating"> + <option value="0">Select rating</option> + </select> + + + <label>Write your review: </label> + <br> + <textarea id="review"></textarea> + <br> + <button id="submitRatingReview" type="button">Update</button> + <button id="deleteReview" type="button">Delete review</button> + ` + + eReview = document.getElementById('review'); + eFormButton = document.getElementById('submitRatingReview'); + eDeleteReviewButton = document.getElementById('deleteReview'); + eRating = document.getElementById('rating'); for(let i = 1; i < 11; i++) { eRating.innerHTML += `<option value=\"${i}\">${i}</option>`; } - - }).catch((error) => { - //eTitle.innerHTML = "This game is not available" - console.error("Error fetching game data: ", error); - document.getElementsByTagName('body')[0].innerHTML = "<h2> 404 - not found </h2>"; - }); -} - - -// Get the user's rating and review for the game -function getUserGameData(token) { - const request = "getusergamedata?token=" + encodeURIComponent(token) + "&id=" + gameId; - - fetch(request, { - method: "GET", - headers: { - "Content-Type": "application/json" - }, - }).then( - response => response.json() - ).then(data => { - eRating.value = (data.rating).toString(); - eReview.value = data.review; + activateFormButtons(); + eRating.value = (userGameData.rating).toString(); + eReview.value = userGameData.review; + addDeleteButton.innerHTML = "Remove from journal"; addDeleteButton.addEventListener('click', removeFromJournal); - - if(data.review == "") { + + if(userGameData.review == "") { eDeleteReviewButton.remove(); } - }).catch(error => { + } else { eReviewForm.innerHTML = ""; addDeleteButton.innerHTML = "Add to journal"; addDeleteButton.addEventListener('click', addToJournal); - }); + } } + //executing comments and reviews showReviews(); showComments(); // Submit review and rating -eFormButton.addEventListener('click', async (e) => { - e.preventDefault(); +function activateFormButtons() { + // Submit button + eFormButton.addEventListener('click', async (e) => { + e.preventDefault(); + + if(currentUser) { + const rating = parseInt(eRating.value) + const review = eReview.value + + if(rating == 0 && review != "") { + alert("Please rate the game before posting a review."); + return; + } + + const request = "/submitusergamedata"; + + let idToken = await currentUser.getIdToken(); + + const body = JSON.stringify({ + "token": idToken, + "gameId": gameId, + "rating": rating, + "review": review + }); + + let response = await sendRequest(request, "POST", body, true, userRequests()); + + if (response.ok) { + alert("Succcessfully updated entry"); + location.reload(); + } + } + }); - if(currentUser) { + + // Delete review button + eDeleteReviewButton.addEventListener('click', async (e) => { + e.preventDefault(); + // Fetch game id from url const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const gameId = urlParams.get('g'); - - const rating = parseInt(eRating.value) - const review = eReview.value - - if(rating == 0 && review != "") { - alert("Please rate the game before posting a review."); - return; - } - - const request = "/submitusergamedata"; - + let idToken = await currentUser.getIdToken(); - - fetch(request, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - "token": idToken, - "gameId": gameId, - "rating": rating, - "review": review - }) - }).then( (response) => { - alert("Succcessfully updated entry"); - location.reload(); - }).catch( (error) => { - console.error("Error updating entry: ", error); - alert("Failed to update entry"); + + const request = "/deletereview" + const body = JSON.stringify({ + "token": idToken, + "gameId": gameId }); - } -}); - - -eDeleteReviewButton.addEventListener('click', async (e) => { - e.preventDefault(); - - // Fetch game id from url - const queryString = window.location.search; - const urlParams = new URLSearchParams(queryString); - const gameId = urlParams.get('g'); - let idToken = await currentUser.getIdToken(); + let response = await sendRequest(request, "DELETE", body, true, userRequests()); - const request = "/deletereview" - fetch(request, { - method: "DELETE", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - "token": idToken, - "gameId": gameId - }) - }).then(response => { - switch(response.status) { - case 404: alert("You have no review posted for this game."); break - case 200: alert("Successfully deleted review."); location.reload(); break - default: alert("Failed to delete review. Try again."); + if (response.ok) { + alert("Succcessfully deleted review."); + location.reload(); } }); -}); +} eSubmitCommentButton.addEventListener('click', async (e) => { e.preventDefault(); @@ -246,57 +257,40 @@ async function addToJournal() { let idToken = await currentUser.getIdToken(); const request = "/addtojournal"; - fetch(request, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - "token": idToken, - "gameId": gameId - }) - }).then(response => { - switch(response.status) { - case 200: alert("Successfully added to journal."); location.reload(); break; - case 403: alert("Could not verify ownership."); break; - default: alert("Failed to add to journal."); - } + + const body = JSON.stringify({ + "token": idToken, + "gameId": gameId }); + + let response = await sendRequest(request, "POST", body, true, userRequests()); + switch(response.status) { + case 200: alert("Successfully added to journal."); location.reload(); break; + case 403: alert("Could not verify ownership."); break; + default: alert("Failed to add to journal."); + } } async function removeFromJournal() { - // Fetch game id from url - const queryString = window.location.search; - const urlParams = new URLSearchParams(queryString); - const gameId = urlParams.get('g'); - let idToken = await currentUser.getIdToken(); const request = "/removefromjournal"; - fetch(request, { - method: "DELETE", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - "token": idToken, - "gameId": gameId - }) - }).then(response => { - if(response.status == 200) { - alert("Successfully removed from journal."); - location.reload(); - } else { - alert("Failed to remove from journal."); - } + + const body = JSON.stringify({ + "token": idToken, + "gameId": gameId }); + + let response = await sendRequest(request, "POST", body, true, userRequests()); + if(response.ok) { + alert("Successfully removed from journal."); + location.reload(); + } else { + alert("Failed to remove from journal."); + } } function showReviews() { - const queryString = window.location.search; - const urlParams = new URLSearchParams(queryString); - const gameId = urlParams.get('g'); - let request = "/getreviewsforgame?id=" + gameId; fetch(request, { method: 'GET', diff --git a/Project/static/js/userpage.js b/Project/static/js/userpage.js index b38bebf655543f58f691ec35416c5eb953d4e39b..3c84de6f7046d542c4f370d80c1fb243cc32323d 100644 --- a/Project/static/js/userpage.js +++ b/Project/static/js/userpage.js @@ -1,7 +1,7 @@ // static/js/userpage.js import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js"; import { getAuth, onAuthStateChanged, signOut } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js"; -import { initFirebase, setupAuthStateListener, setupButtonHandlers } from "./utils.js"; +import { initFirebase, setupAuthStateListener, setupButtonHandlers, sendGETRequest, sendRequest } from "./utils.js"; const auth = initFirebase(); @@ -103,15 +103,10 @@ async function DisconnectFromSteam() { async function showUserJournal() { const idToken = await currentUser.getIdToken(); const request = "/getuserjournal?token=" + idToken; + const cacheRequest = "/getuserjournal?token=" + currentUser.uid; - fetch(request, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }) - .then(response => response.json()) - .then(games => { + let games = await sendGETRequest(request, true, cacheRequest); + if (games != null) { const journalContainer = document.getElementById('journalList'); journalContainer.innerHTML = ''; // Clear previous journal entries @@ -133,6 +128,5 @@ async function showUserJournal() { `; journalContainer.appendChild(gameElement); }); - }) - .catch(error => console.error('Error fetching journal:', error)); + } } diff --git a/Project/static/js/utils.js b/Project/static/js/utils.js index b08ff3cd1526393e39b0a31d2b5a879f3cd64ca4..c254f4b78aca73962514482f4a081d2212ea1082 100644 --- a/Project/static/js/utils.js +++ b/Project/static/js/utils.js @@ -55,4 +55,55 @@ export const setupButtonHandlers = (auth, elements) => { console.error("Error signing out: ", error); }); }); -} \ No newline at end of file +} + + + +// Send a GET request +export async function sendGETRequest(request, cache, cacheRequest) { + let cachedData = sessionStorage.getItem(cacheRequest) + if(!cache || cachedData==null) { + try { + console.log("Sending request..."); + let response = await fetch(request); + let data = await response.json(); + + // Cache the data if requested + if (cache) { + sessionStorage.setItem(cacheRequest, JSON.stringify(data)); + } + return data; + } catch (error) { + console.log("Failed to retrieve data: " + error); + return null; + } + } else { + console.log("Getting cached data..."); + let parsedData = JSON.parse(cachedData); + return parsedData; + } +} + +// Send a non-GET request +export async function sendRequest(request, method, body, cache, getRequests) { + try { + let response = await fetch(request, { + method: method, + headers: { + "Content-Type": "application/json", + }, + body: body + }); + + // Remove get request data from cache + if (cache) { + for(let r of getRequests) { + sessionStorage.removeItem(r); + } + } + return response; + } catch (error) { + console.error("Error: ", error); + return null; + } +} diff --git a/Project/static/style/style_chat.css b/Project/static/style/style_chat.css new file mode 100644 index 0000000000000000000000000000000000000000..b2d58741bb778d1f957b417811f2041a5b594398 --- /dev/null +++ b/Project/static/style/style_chat.css @@ -0,0 +1,259 @@ +/* Reset default margins on the body and html */ +html, body { + margin: 0; + padding: 0; + width: 100%; + background-color: rgb(34, 40, 49); + overflow-x: hidden; /* To prevent horizontal scrolling */ + overflow-y:auto; + font-family: Arial, Helvetica, sans-serif; +} +h1, h2, h4, p{ /*text for some reason has a invisible margin, removing this*/ + margin: 0; + padding: 0px; +} +h1 { + padding-top: 6px; +} + +header { + display:flex; + justify-content: space-between; + top: 0; + width: 100%; + height: auto; + + color: white; + background-color: rgb(0, 146, 202); + +} + +#searchForm { + display: flex; + align-items: center; +} +#imgLogo { + position: relative; + top: 0; + left: 0; + height: 50px; + width: 50px; + background-color: rgb(0, 146, 202); + padding: 0px; +} + + + +ul { /*list is sticky*/ + position: sticky; + display: flex; + list-style-type: none; + top: 0; + margin: 0; + padding: 0; + background-color: rgb(57, 62, 70); +} + +#divHeader { + width: 56%; + display : flex; + justify-content: space-between; +} + +main ul li a { + text-decoration: none; + background-color: rgb(0, 100, 148); + color: white; + display: block; /* Ensure the <a> is block-level */ + padding: 10px; + padding-left: 50px; + padding-right: 50px; + text-align: center; + + +} +main ul { + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +ul li a { + text-decoration: none; + background-color: rgb(0, 100, 148); + color: white; + display: block; /* Ensure the <a> is block-level */ + padding: 18px; + padding-left: 50px; + padding-right: 50px; + text-align: center; +} + +ul li a:hover {/* Change background color on hover */ + background-color: rgb(34, 40, 49); + cursor: pointer; +} + + + + +#divContent { + /*display: flex; + justify-content: center;*/ + height: 100vh; +} +main { + display: flex; + flex-direction: column; /* Organize list and section vertically */ + background-color: rgb(34, 40, 49); + width: 70%; + height:auto; + padding: 10px; + margin: 10px; + border-radius: 5px; + overflow: hidden; /* Prevent content from overflowing main */ +} + +.sectionMain { + background-color: rgb(57, 62, 70); + color: white; + flex-grow: 1; /* Allow it to grow to fill the remaining space */ + padding: 30px; + overflow-y: auto; + scrollbar-color: rgb(5, 25, 35) rgb(0, 100, 148) ; + scrollbar-width:auto; + scroll-behavior: smooth; + + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + +} + +.sectionAside { + color:white; + background-color: rgb(57, 62, 70); + +} + +aside { + overflow-y: auto; + background-color: rgb(34, 40, 49); + width: 30%; + height: auto; + padding: 10px; + margin: 10px; + border-radius: 5px; + +} + +article { + color: white; + width: auto; + + padding: 30px; + background-color: rgb(57, 62, 70); + border-radius: 5px; +} + +footer { + text-align: center; + padding: 10px; + background-color: rgb(0, 146, 202); + position: relative; /* Keeps the footer fixed at the bottom */ + bottom: 0; /* Sticks to the bottom */ + width: 100%; +} + +@media screen and (max-width: 600px) { + #divContent { + display: flex; + flex-direction: column; + flex-wrap: wrap; + } + + main { + width: 95%; + + } + aside { + width: 95%; + } + } + + + +#name { + text-align: center; + color: white; + margin: 10px; +} + +#chat { + margin: 20px; + margin-left: auto; + margin-right: auto; + border: 3px solid rgb(0, 100, 148); + flex-grow: 1; /* Allow chat to grow and fill available space */ + height: 60vh; /* Set a height for the chat box */ + width: 70vh; + background-color: rgb(230, 230, 230); + padding: 10px; + overflow-y: auto; /* Scroll if content exceeds the height */ + display: flex; + flex-direction: column; + gap: 10px; + scrollbar-color: rgb(5, 25, 35) rgb(0, 100, 148); +} + + +#messageForm { + display: flex; + justify-content: center; + padding: 10px; + background-color: rgb(34, 40, 49); +} + + +#messageForm input { + width: 40vh; + padding: 10px; + border-radius: 5px; + border: none; + margin-right: 5px; +} + +#messageForm button { + padding: 10px; + border-radius: 5px; + background-color: rgb(0, 146, 202); + color: white; + border: none; + cursor: pointer; + white-space: nowrap; +} + +#messageForm button:hover { + background-color: rgb(0, 100, 148); +} + +/* Message styles */ +.message { + display: flex; + align-items: center; + max-width: 60%; /* Limit message width */ + padding: 10px; + border-radius: 5px; + color: white; + word-wrap: break-word; /* Ensure long words don't break the layout */ + min-height: 30px; +} + + +.you { + align-self: flex-end; /* Align messages from 'You' to the right */ + background-color: rgb(0, 146, 202); +} + +.other { + align-self: flex-start; /* Align messages from 'Other' to the left */ + background-color: rgb(100, 100, 100); +} \ No newline at end of file diff --git a/Project/websocket/websocket.go b/Project/websocket/websocket.go new file mode 100644 index 0000000000000000000000000000000000000000..3df4d742bfc091b35b66f1ed1496eeed720c90e5 --- /dev/null +++ b/Project/websocket/websocket.go @@ -0,0 +1,70 @@ +package websocket + +import ( + firebase "go-firebase-auth/Firebase" + "net/http" + "sync" + + "github.com/gorilla/websocket" +) + +var clients = make(map[string]*websocket.Conn) +var mutex sync.Mutex +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +// WConnect clients when they enter a chat page +func HandleWebsocket(w http.ResponseWriter, r *http.Request) { + urlQuery := r.URL.Query() + token := urlQuery.Get("token") + toId := urlQuery.Get("user") + + fromId, err := firebase.GetUserId(token) + if err != nil { + http.Error(w, "Failed to authenticate user: "+err.Error(), http.StatusBadRequest) + return + } + + // Upgrade to websocket + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + http.Error(w, "Failed to establish websocket connection: "+err.Error(), http.StatusInternalServerError) + return + } + + socketId := SocketId(toId, fromId) + mutex.Lock() + clients[socketId] = ws + mutex.Unlock() + + // Create a go routine that runs until the client disconnects + go func() { + for { + _, _, err = ws.ReadMessage() + if err != nil { + delete(clients, fromId) + ws.Close() + + break + } + } + }() +} + +// Send a message to a user client +func SendMessageToUser(to string, from string, msg string) { + // Check if the user is connected + socketId := SocketId(to, from) + conn, ok := clients[socketId] + if ok { + // Send the message to the client + conn.WriteMessage(websocket.TextMessage, []byte(msg)) + } +} + +// Create an id for a receiver and a sender +func SocketId(to string, from string) string { + return from + " -> " + to +}