demo: add files for demo
parent
558b489e4f
commit
04f1eaa9d4
@ -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()
|
||||||
|
}
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
@ -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;}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
hello world!
|
Loading…
Reference in New Issue