diff --git a/code/static/index.html b/code/static/index.html index 786fcb4..060f94d 100644 --- a/code/static/index.html +++ b/code/static/index.html @@ -4,7 +4,6 @@ Go Chat Application -
diff --git a/demo/hub.go b/demo/hub.go new file mode 100644 index 0000000..0e770ef --- /dev/null +++ b/demo/hub.go @@ -0,0 +1,116 @@ +package main + +import ( + "strings" + "time" + + "github.com/gorilla/websocket" +) + +var usernames = make(map[*websocket.Conn]string) +var nameToConn = make(map[string]*websocket.Conn) + +func handleIncomingMessage(sender *websocket.Conn, msg string) { + if _, ok := usernames[sender]; ok { + sendChatMessage(sender, msg) + return + } + + // Registering a new user + username := strings.TrimSpace(msg) + if username == "" || username == "server" { + sender.WriteJSON(newError("You have an illegal username.")) + return + } + + if _, ok := nameToConn[username]; ok { + sender.WriteJSON(newError("The specified username is already taken.")) + return + } + + sendUserList(sender) + + usernames[sender] = username + nameToConn[username] = sender + + m := newMessage(msgJoin, "server", username) + m.dispatch() +} + +func handleDisconnection(sender *websocket.Conn) { + username, ok := usernames[sender] + if !ok { + return + } + + m := newMessage(msgLeave, "server", username) + m.dispatch() + delete(usernames, sender) + delete(nameToConn, username) +} + +type messageType string + +const ( + msgChat messageType = "message" + msgJoin messageType = "join" + msgLeave messageType = "leave" + msgErr messageType = "error" + msgUserList messageType = "users" +) + +type message struct { + Type messageType `json:"type"` + Sender string `json:"sender"` + Content interface{} `json:"content"` + Date time.Time `json:"date"` + Success bool `json:"success"` +} + +func newError(content string) message { + return message{ + Type: msgErr, + Sender: "", + Content: content, + Date: time.Now().UTC(), + Success: false, + } +} + +func newMessage(msgType messageType, sender string, content string) message { + return message{ + Type: msgType, + Sender: sender, + Content: content, + Date: time.Now().UTC(), + Success: true, + } +} + +func (m message) dispatch() { + for client := range usernames { + _ = client.WriteJSON(m) + } +} + +func sendUserList(who *websocket.Conn) { + list := []string{} + for _, username := range usernames { + list = append(list, username) + } + + m := message{ + Type: msgUserList, + Sender: "", + Content: list, + Date: time.Now().UTC(), + Success: true, + } + + _ = who.WriteJSON(m) +} + +func sendChatMessage(sender *websocket.Conn, msg string) { + m := newMessage(msgChat, usernames[sender], msg) + m.dispatch() +} diff --git a/demo/main.go b/demo/main.go new file mode 100644 index 0000000..3bce483 --- /dev/null +++ b/demo/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gorilla/websocket" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +func websocketConnection(w http.ResponseWriter, r *http.Request) { + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + _, _ = fmt.Fprint(w, "You must use the web socket protocol to connect to this endpoint. ", err) + return + } + + defer ws.Close() + + for { + _, p, err := ws.ReadMessage() + if err != nil { + handleDisconnection(ws) + break + } + + msg := string(p) + handleIncomingMessage(ws, msg) + } +} + +const port = 8080 + +func main() { + http.HandleFunc("/websocket", websocketConnection) + http.Handle("/", http.FileServer(http.Dir("./static"))) + + fmt.Println("Chat server opening on port", port) + err := http.ListenAndServe(":"+strconv.Itoa(port), nil) + if err != nil { + _ = fmt.Errorf("Server could not start up: %s", err.Error()) + } +} diff --git a/demo/static/css/style.css b/demo/static/css/style.css new file mode 100644 index 0000000..9354941 --- /dev/null +++ b/demo/static/css/style.css @@ -0,0 +1,253 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;0,900;1,400&display=swap'); + +body { + width: 100vw; + height: 100vh; + padding: 0; + margin: 0; + font-family: 'Roboto', sans-serif; +} + +#snackbar { + visibility: hidden; + min-width: 250px; + margin-left: -125px; + background-color: #4D5359; + color: #BBC4FF; + text-align: center; + border-radius: 4px; + padding: 16px; + position: fixed; + z-index: 2; + left: 50%; + bottom: 30px; + font-size: 15px; +} + +#snackbar.snackshow { + visibility: visible; + -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; + animation: fadein 0.5s, fadeout 0.5s 2.5s; +} + +.container { + width: 100%; + height: 100%; + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} + +.lhs { + width: calc(100% - 300px); + display: flex; + flex-direction: column-reverse; + background-color: #39363F; +} + +.rhs { + width: calc(300px - 24px); + background-color: #28272D; + padding: 12px; + color: #eeeeee; +} + +.messages { + overflow: auto; + max-height: calc(100vh - 128px); + flex-grow: 1; +} + +.messages::-webkit-scrollbar { + width: 8px; +} + +.messages::-webkit-scrollbar-track { + background: #322F37; + border-radius: 12px; +} + +.messages::-webkit-scrollbar-thumb { + background: #504D56; + border-radius: 12px; +} + +.messages::-webkit-scrollbar-thumb:hover { + background: #5C5766; +} + + +/* */ +.chat { + height: 64px; + padding: 32px; +} + +.chat-inner { + height:64px; + display: flex; + align-items: center; +} + +#chatI { + height: 64px; + width: 100%; + padding: 0 12px; + font-size: 20px; + color: #eeeeee; + border: none; + border-radius: 6px 0 0 6px; + background: #28272D; +} + +#chatI:focus { + outline: none; +} + +#chatI::placeholder { + color: #908D9B; +} + +#chatB { + height: 64px; + padding: 0; + width: 128px; + font-size: 24px; + color: #000; + border: none; + border-radiuS: 0 6px 6px 0; + background: #8EFFD6; +} + +i { + font-size: 22px; + transition: color 0.25s; + color: #AAB3FF; +} + +i:hover { + color: #EAF0CE; +} + +/* Chat Messages */ +.message { + margin: 32px 32px; +} + +.server-message { + color: #908D9B; + font-style: italic; + font-size: 21px; +} + +.author { + color: #fcfcfc; + font-weight: 700; + display: inline-block; + margin-right: 8px; +} + +.author.self { + color: #8EFFD6; +} + +.timestamp { + display: inline-block; + font-weight: 400; + font-size: 14px; + color: #7D7A87; +} + +.content { + color: #eee; + margin-top: 2px; +} + +.modal { + display: none; + position: fixed; + z-index: 1; + padding-top: 100px; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: #00000088; + animation-name: animatetop; + animation-duration: 0.6s; +} + +.modal-content { + color: #eeeeee; + position: relative; + background-color: #39363F; + margin: auto; + padding: 18px; + padding-bottom: 36px; + border-radius: 4px; + width: 60%; + -webkit-animation-name: animatetop; + -webkit-animation-duration: 0.6s; +} + +.modal-content h1 { + padding: 18px 0; +} + +#nickI { + height: 48px; + padding: 0 12px; + font-size: 16px; + color: #eeeeee; + border: none; + border-radius: 6px; + background: #28272D; +} + +#nickI:focus { + outline: none; +} + +#nickI::placeholder { + color: #908D9B; +} + +#nickB { + height: 48px; + padding: 0; + width: 96px; + font-size: 18px; + color: #000; + border: none; + border-radiuS: 6px; + background: #8EFFD6; +} +@-webkit-keyframes animatetop { + from {top:-300px; opacity:0} + to {top:0; opacity:1} +} + +@keyframes animatetop { + from {top:-300px; opacity:0} + to {top:0; opacity:1} +} + +@-webkit-keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: 30px; opacity: 1;} +} + +@keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: 30px; opacity: 1;} +} + +@-webkit-keyframes fadeout { + from {bottom: 30px; opacity: 1;} + to {bottom: 0; opacity: 0;} +} + +@keyframes fadeout { + from {bottom: 30px; opacity: 1;} + to {bottom: 0; opacity: 0;} +} diff --git a/demo/static/index.html b/demo/static/index.html new file mode 100644 index 0000000..060f94d --- /dev/null +++ b/demo/static/index.html @@ -0,0 +1,34 @@ + + + + + Go Chat Application + + +
+ + +
+
+
+
+ + +
+
+
+
+
+
+

Online Users

+
+
+ + + diff --git a/demo/static/js/index.js b/demo/static/js/index.js new file mode 100644 index 0000000..2da2748 --- /dev/null +++ b/demo/static/js/index.js @@ -0,0 +1,196 @@ +//Saved nickname from previous usage +let nickname = window.localStorage.getItem("nickname"); + +//Nickname button and input +let nickI = document.getElementById("nickI"); +let nickB = document.getElementById("nickB"); + +//Chatting button and input +let chatI = document.getElementById("chatI"); +let chatB = document.getElementById("chatB"); + +let registered = false; +let modal = document.getElementById("modal"); + +if (nickname != null) { + nickI.value = nickname; +} + +//Gets websocket url by replacing http with ws and https with wss, and appending /websocket to the host +let protocol = location.protocol == "http:" ? "ws:" : "wss:"; +let url = protocol + "//" + location.host + "/websocket"; + +let ws = new WebSocket(url); + +ws.onopen = function() { + modal.style.display = "block"; + nickI.focus(); +} + +ws.onmessage = function(info) { + let data = JSON.parse(info.data); + if(!data.success) { + toast("Error: " + data.content); + return; + } + + // Not Registered + Non error message means we have a new message! + if (!registered) { + registered = true; + window.localStorage.setItem("nickname", nickname); + modal.style.display = "none"; + chatI.focus() + let users = data.content; + for(let i = 0; i < users.length; i++) { + userJoin(users[i]); + } + return; + } + + newMessage(data); +} + +/* + * Message receive handling + */ + +function newMessage(data) { + switch(data.type) { + case "join": + userJoin(data.content, data.date); + break; + case "leave": + userLeave(data.content, data.date); + break; + case "message": + chatMessage(data.sender, data.content, data.date) + break; + } +} + +/** + * Join/leave events + */ +function userJoin(user, date) { + let p = document.createElement("p"); + p.className = user; + p.innerText = user; + document.getElementById("users").appendChild(p); + serverMessage(data.content + " has joined the server", data.date); +} + +function userLeave(user, date) { + users = document.getElementById("users"); + users.removeChild(users.getElementsByClassName(user)[0]); + serverMessage(user + " has left the server", date); +} + +function chatMessage(sender, content, date) { + let msg = create("div", "message"); + + aClasses = "author" + if (sender === nickname) { + aClasses += " self" + } + let author = create("div", aClasses); + author.innerText = sender; + + let timestamp = create("div", "timestamp"); + timestamp.innerText = dateToString(new Date(date)); + + let text = create("div", "content"); + text.innerText = content; + + msg.append(author, timestamp, text); + insertMessage(msg); +} + +function serverMessage(content, date) { + let msg = create("div", "message"); + + let text = create("div", "server-message"); + text.innerText = content; + + msg.append(text); + insertMessage(msg); +} + +function insertMessage(div) { + //If message scrolling at the bottom, move to bottom again to update + let scroller = document.getElementsByClassName("messages")[0]; + let currentScroll = scroller.scrollTop; + let maxScroll = scroller.scrollHeight - scroller.clientHeight; + + document.getElementById("messages").append(div); + + if (currentScroll === maxScroll) { + div.scrollIntoView(); + } +} + +/* + * Registration + */ +function register() { + let nick = nickI.value; + if (nick.trim().length == 0) return; + nickname = nick; + ws.send(nick); +} + +nickI.onkeydown = function(ev) { + if(ev.key !== "Enter") { + return; + } + register(); +} + +nickB.onclick = function() { + register(); +} + +/** + * Message sending + */ + +chatI.onkeydown = function(ev) { + if(ev.key !== "Enter") { + return; + } + if(chatI.value.trim().length == 0) return; + + ws.send(chatI.value); + chatI.value = ""; +} +chatB.onclick = function() { + if(chatI.value.trim().length == 0) return; + + ws.send(chatI.value); + chatI.value = ""; +} + +/* + * Util functions + */ +function toast(message) { + var x = document.getElementById("snackbar"); + x.innerText = message; + x.className = "snackshow"; + setTimeout(function(){ x.className = x.className.replace("snackshow", ""); }, 3000); +} + +let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] +function dateToString(date) { + let year = date.getFullYear(); + let month = months[date.getMonth()]; + let day = date.getDate(); + let hour = date.getHours(); + let minute = date.getMinutes(); + return ` ${day} ${month} ${year} at ${hour}:${minute}`; +} + +function create(name, classes) { + let x = document.createElement(name); + x.className = classes; + return x; +} diff --git a/demo/static/verify.txt b/demo/static/verify.txt new file mode 100644 index 0000000..a042389 --- /dev/null +++ b/demo/static/verify.txt @@ -0,0 +1 @@ +hello world!