Compare commits

..

No commits in common. "master" and "design" have entirely different histories.

11 changed files with 10478 additions and 445 deletions

View File

@ -1,49 +0,0 @@
InstantChat WebSocket Protocol
==============================
Messages from client to server (commands)
-----------------------------------------
JSON objects. Key `action` determines type of message.
### init
Initialize the connection, set nickname and join/create a chat.
**Parameters:**
* `action` = `"init"`
* `chat_id`: either an existing ID of a chat instance or *[TODO: ?]* empty string to
create a new chat
* `nickname`: user's chosen nickname *[TODO: valid characters?]*
**Example:**
{
"action": "init",
"chat_id": "42",
"nickname": "Alice"
}
### send (?)
Send a message...
*[TODO]*
Messages from server to client (responses/events)
-------------------------------------------------
JSON objects. Key `type` determines type of message.
### init
Response to `init` command. Confirms initialization and chat join.
**Parameters:**
* `type` = `"init"`
* ***[TODO]***
**Example:**
{
"type": "init",
TODO ???
}

View File

@ -1,8 +1,3 @@
body {
font-family: Roboto,sans-serif;
font-size: 1rem;
}
img { img {
max-width: 100%; max-width: 100%;
} }
@ -25,16 +20,6 @@ img {
overflow: hidden; overflow: hidden;
} }
@media (min-width: 768px) {
.messaging_heading {
display: none;
}
#sidebarCollapse {
display: none;
}
}
.inbox_msg { .inbox_msg {
border: 1px solid #c4c4c4; border: 1px solid #c4c4c4;
clear: both; clear: both;
@ -60,11 +45,10 @@ img {
@media (min-width: 768px) { @media (min-width: 768px) {
#sidebar { #sidebar {
border-left: 1px solid #c4c4c4; border-left: 1px solid #c4c4c4;
float: left; /*float: right;*/
/*position: relative;*/
overflow: hidden; overflow: hidden;
position: relative; /*width: 40%;*/
transform: none;
width: 40%;
} }
} }
@ -102,22 +86,15 @@ img {
width: 60%; width: 60%;
} }
.search_bar .stylish-input-group {
position: relative;
}
.search_bar input { .search_bar input {
background: none; background: none;
border-bottom: 1px solid #cdcdcd; border-bottom: 1px solid #cdcdcd;
font-size: 1rem;
height: 25px;
padding: 2px 30px 4px 6px;
width: 80%; width: 80%;
padding: 2px 0 4px 6px;
} }
.search_bar .input-group-addon { .search_bar .input-group-addon {
position: absolute; margin: 0 0 0 -30px;
right: 28px;
} }
.search_bar .input-group-addon button { .search_bar .input-group-addon button {
@ -126,17 +103,12 @@ img {
color: #707070; color: #707070;
font-size: 18px; font-size: 18px;
padding: 0; padding: 0;
position: absolute;
}
.search_bar .input-group-addon button svg {
padding-top: 5px;
} }
.chat_ib { .chat_ib {
float: left; float: left;
padding: 0 0 0 15px; padding: 0 0 0 15px;
width: 75%; width: 88%;
} }
.chat_ib h5 { .chat_ib h5 {
@ -152,7 +124,7 @@ img {
.chat_ib p { .chat_ib p {
font-size: 14px; font-size: 14px;
color: #2b2b2b; color: #989898;
margin: auto; margin: auto;
} }
@ -173,23 +145,22 @@ img {
} }
.inbox_chat { .inbox_chat {
height: 540px; height: 550px;
overflow-y: scroll; overflow-y: scroll;
} }
.active_chat { .active_chat {
background: #dae3f2; background: #ebebeb;
} }
h5.incoming_msg_user { .incoming_msg_img {
color: #464646; display: inline-block;
font-size: 15px; width: 6%;
margin: 0 0 8px 30px;
} }
.received_msg { .received_msg {
display: inline-block; display: inline-block;
padding-left: 30px; padding: 0 0 0 10px;
vertical-align: top; vertical-align: top;
width: 92%; width: 92%;
} }
@ -199,23 +170,25 @@ h5.incoming_msg_user {
} }
.received_withd_msg p { .received_withd_msg p {
background: #dae3f2 none repeat scroll 0 0; background: #ebebeb none repeat scroll 0 0;
border-radius: 3px; border-radius: 3px;
color: #2b2b2b; color: #646464;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
padding: 10px 10px 10px 12px; padding: 5px 10px 5px 12px;
width: 100%; width: 100%;
} }
.messages { .messages {
padding: 30px; padding: 30px 15px 0 25px;
width: 100%; width: 100%;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.messages { .messages {
width: auto; /*float: left;*/
padding: 30px 15px 0 25px;
/*width: 60%;*/
} }
} }
@ -226,10 +199,8 @@ h5.incoming_msg_user {
margin: 8px 0 0; margin: 8px 0 0;
} }
/*Messages*/
.sent_msg { .sent_msg {
float: right; float: right;
margin-right: 20px;
width: 46%; width: 46%;
} }
@ -238,13 +209,13 @@ h5.incoming_msg_user {
border-radius: 3px; border-radius: 3px;
font-size: 14px; font-size: 14px;
margin: 0; color:#fff; margin: 0; color:#fff;
padding: 10px 12px 10px 12px; padding: 5px 10px 5px 12px;
/*width: 100%;*/ width: 100%;
} }
.outgoing_msg { .outgoing_msg {
overflow: hidden; overflow: hidden;
margin: 12px 0 12px; margin: 26px 0 26px;
} }
.input_msg_write input { .input_msg_write input {
@ -253,33 +224,30 @@ h5.incoming_msg_user {
color: #4c4c4c; color: #4c4c4c;
font-size: 15px; font-size: 15px;
min-height: 48px; min-height: 48px;
padding-left: 30px;
width: 100%; width: 100%;
} }
.type_msg { .type_msg {
border-top: 1px solid #c4c4c4; border-top: 1px solid #c4c4c4;
float: left;
position: relative; position: relative;
width: 60%;
} }
.msg_send_btn { .msg_send_btn {
background: #05728f none repeat scroll 0 0; background: #05728f none repeat scroll 0 0;
border: medium none; border: medium none;
border-radius: 3px; border-radius: 50%;
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
height: 35px; font-size: 17px;
height: 33px;
position: absolute; position: absolute;
right: 0; right: 0;
top: 7px; top: 11px;
width: 52px; width: 33px;
} }
.msg_send_btn svg { .msg_send_btn svg {
fill: #ffffff; fill: #ffffff;
padding-top: 4px;
} }
.msg_history { .msg_history {

View File

@ -4,6 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>InstantChat</title> <title>InstantChat</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
crossorigin="anonymous">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
</head> </head>
<body> <body>
@ -118,7 +121,7 @@
<div class="messages"> <div class="messages">
<div class="msg_history"> <div class="msg_history">
<div class="incoming_msg"> <div class="incoming_msg">
<h5 class="incoming_msg_user">Sunil Rajput</h5> <div class="incoming_msg_img"> <img src="https://ptetutorials.com/images/user-profile.png" alt="sunil"> </div>
<div class="received_msg"> <div class="received_msg">
<div class="received_withd_msg"> <div class="received_withd_msg">
<p>Test which is a new approach to have all <p>Test which is a new approach to have all
@ -134,7 +137,7 @@
<span class="time_date"> 11:01 AM | June 9</span> </div> <span class="time_date"> 11:01 AM | June 9</span> </div>
</div> </div>
<div class="incoming_msg"> <div class="incoming_msg">
<h5 class="incoming_msg_user">Sunil Rajput</h5> <div class="incoming_msg_img"> <img src="https://ptetutorials.com/images/user-profile.png" alt="sunil"> </div>
<div class="received_msg"> <div class="received_msg">
<div class="received_withd_msg"> <div class="received_withd_msg">
<p>Test, which is a new approach to have</p> <p>Test, which is a new approach to have</p>
@ -148,7 +151,7 @@
<span class="time_date"> 11:01 AM | Today</span> </div> <span class="time_date"> 11:01 AM | Today</span> </div>
</div> </div>
<div class="incoming_msg"> <div class="incoming_msg">
<h5 class="incoming_msg_user">Sunil Rajput</h5> <div class="incoming_msg_img"> <img src="https://ptetutorials.com/images/user-profile.png" alt="sunil"> </div>
<div class="received_msg"> <div class="received_msg">
<div class="received_withd_msg"> <div class="received_withd_msg">
<p>We work directly with our designers and suppliers, <p>We work directly with our designers and suppliers,
@ -163,8 +166,11 @@
<div class="input_msg_write"> <div class="input_msg_write">
<input type="text" class="write_msg" placeholder="Type a message" /> <input type="text" class="write_msg" placeholder="Type a message" />
<button class="msg_send_btn" type="button"> <button class="msg_send_btn" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="21" viewBox="0 0 25 21"> <svg xmlns="http://www.w3.org/2000/svg"
<path fill="#FFF" d="M24.0085292,2.21031867 L20.3673237,19.2383094 C20.099588,20.4698936 19.4034751,20.7376293 18.3860795,20.2021579 L12.870724,16.1325752 L10.1933669,18.702838 C9.92563124,18.9705737 9.65789553,19.2383094 9.06887699,19.2383094 L9.49725411,13.6158596 L19.724758,4.40575143 C20.1531351,3.97737431 19.6176637,3.76318575 19.0286452,4.13801573 L6.39151995,12.1165397 L0.929711603,10.4030312 C-0.248325492,10.0282012 -0.248325492,9.2249941 1.19744731,8.63597555 L22.4556622,0.496810165 C23.4730578,0.175527321 24.3298121,0.764545868 24.0085292,2.26386581 L24.0085292,2.21031867 Z"/> width="24"
height="24"
viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0V0z"/><path d="M4.01 6.03l7.51 3.22-7.52-1 .01-2.22m7.5 8.72L4 17.97v-2.22l7.51-1M2.01 3L2 10l15 2-15 2 .01 7L23 12 2.01 3z"/>
</svg> </svg>
</button> </button>
</div> </div>
@ -175,10 +181,16 @@
</div> </div>
</div> </div>
<script src="js/settings.js"></script>
<script src="js/events.js"></script>
<script src="js/jquery-3.3.1.js"></script>
<!--Bootstrap Tooltip Library, not needed yet-->
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>-->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
<script src="js/client.js"></script> <script src="js/client.js"></script>
<script src="js/ui.js"></script>
<script src="js/sidebar.js"></script> <script src="js/sidebar.js"></script>
</body> </body>

View File

@ -1,122 +1,47 @@
"use strict"; // Constants and variables
//const wsUri = "ws://localhost:32715";
const wsUri = "wss://chat.glitch-in.space:443/ws/";
let websocket;
/**
* Client class that implements the InstantChat protocol using WebSockets.
*
* Dispatches the following events which can be subscribed/unsubscribed using .on() and .off():
* - initialized: Connection to server has been established and initialized, ready to send messages.
* - disconnected: Connection has been closed.
* - connectionError: Some connection error occurred.
* - receivedMessage: Chat message has been received. Data: object {from: 'username', text: 'text'}
*/
class Client extends EventDispatcher {
constructor(wsUri, chatID, nickname) {
super();
this.wsUri = wsUri; // Initialization
this.chatID = chatID; function init() {
this.nickname = nickname; console.log("Init...");
openWebSocket();
// Create WebSocket and set internal callbacks
console.log("Initialize Client...")
this.webSocket = new WebSocket(wsUri);
this.webSocket.onopen = this._onSocketOpen.bind(this);
this.webSocket.onclose = this._onSocketClose.bind(this);
this.webSocket.onerror = this._onSocketError.bind(this);
this.webSocket.onmessage = this._onSocketMessage.bind(this);
} }
// Internal WebSocket event handlers // Open WebSocket
_onSocketOpen(evt) { function openWebSocket() {
console.log("Connected to " + this.wsUri); websocket = new WebSocket(wsUri);
this.sendInit(this.chatID, this.nickname); websocket.onopen = function(evt) { onOpen(evt) };
websocket.onclose = function(evt) { onClose(evt) };
websocket.onmessage = function(evt) { onMessage(evt) };
websocket.onerror = function(evt) { onError(evt) };
} }
_onSocketClose(evt) { // WebSocket event handlers
console.log("Connection closed (code " + evt.code + ")."); function onOpen(evt) {
this.dispatch("disconnected"); console.log('Connected to ' + wsUri + '.');
let text = "Meow";
console.log('Sending "' + text + '".');
websocket.send(text);
} }
_onSocketError(evt) { function onClose(evt) {
console.error("Connection error: ", evt); console.log("Connection closed (code " + evt.code + "): ", evt);
this.dispatch("connectionError");
} }
_onSocketMessage(evt) { function onMessage(evt) {
console.log("Received: " + evt.data); console.log('Received: "' + evt.data + '".');
this._parseMessage(evt.data);
} }
/** function onError(evt) {
* Sends an arbitrary command as JSON. console.error('Connection error: ', evt);
*
* commandObj: The command as an object ('action' specifies type of command).
*/
sendCommand(commandObj) {
const commandJson = JSON.stringify(commandObj);
console.log("Sending command: " + commandJson);
this.webSocket.send(commandJson);
} }
/**
* Sends the 'init' command which sets up the session. Also sets the chat ID and nickname. // Run script after page is loaded
* $(function() {
* chatID: The ID of the chat instance. init();
* nickname: The user's nickname.
*/
sendInit(chatID, nickname) {
this.sendCommand({
action: "init",
chat_id: chatID,
nickname: nickname,
}); });
}
/**
* Sends the 'message' command which sends a chat message to the chat.
*
* msgText: The text of the chat message.
*/
sendChatMessage(msgText) {
this.sendCommand({
action: "message",
text: msgText,
});
}
/**
* Parses an incoming JSON message and dispatches specific events.
*
* msgText: The content of the message as a JSON string.
*/
_parseMessage(msgString) {
try {
const msg = JSON.parse(msgString);
switch (msg.type) {
// Response to 'init' command (doesn't have much content, I guess)
case "init":
console.log("Got init response: ", msg);
this.dispatch("initialized");
break;
// Incoming chat message
case "message":
console.log("Received chat message from '" + msg.from + "', text '" + msg.text + "'");
this.dispatch("receivedMessage", {
from: msg.from,
text: msg.text,
});
break;
// TODO Topic change, user join/leave, error, ...
default:
console.error("Unknown message type '" + msg.type + "'");
}
}
catch (e) {
console.error("Error parsing message JSON: " + e.message);
}
}
}

View File

@ -1,42 +0,0 @@
"use strict";
/**
* Base class to implement an event system in any class. Events can be dispatched and
* subscribed/unsubscribed. Events can have multiple registered callbacks.
*/
class EventDispatcher {
constructor() {
this._events = {};
}
/**
* Dispatch a named event. Calls all callbacks that are subscribed to this event.
*/
dispatch(eventName, data = null) {
if (this._events[eventName]) {
this._events[eventName].forEach((callback) => {
callback(data);
});
}
}
/**
* Subscribe to an event.
*/
on(eventName, callback) {
if (!this._events[eventName]) {
this._events[eventName] = [];
}
this._events[eventName].push(callback);
}
/**
* Unsubscribe from an event. Not sure how to unsubscribe anonymous functions though.
* If callback is not found, nothing happens.
*/
off(eventName, callback) {
if (this._events[eventName]) {
this._events[eventName] = this._events[eventName].filter(item => item !== callback);
}
}
}

10364
public_html/js/jquery-3.3.1.js vendored Normal file

File diff suppressed because it is too large Load Diff

2
public_html/js/jquery-3.3.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,10 +0,0 @@
"use strict";
/**
* Global settings object.
*/
const AppSettings = {
// Alternative WebSocket URI for local testing:
// serverWsUri: "ws://localhost:32715",
serverWsUri: "wss://chat.glitch-in.space:443/ws/",
};

View File

@ -1,10 +1,12 @@
// https://bootstrapious.com/p/bootstrap-sidebar // https://bootstrapious.com/p/bootstrap-sidebar
document.addEventListener('DOMContentLoaded', function(){ $(document).ready(function () {
document.querySelector('#sidebarCollapse').addEventListener('click', function () {
document.querySelector('#sidebar').classList.remove('active'); $('#sidebarCollapse').on('click touch', function () {
$('#sidebar').removeClass('active');
}); });
document.querySelector('#sidebarShow').addEventListener('click', function () { $('#sidebarShow').on('click touch', function () {
document.querySelector('#sidebar').classList.add('active'); $('#sidebar').addClass('active');
}); });
}); });

View File

@ -1,72 +0,0 @@
"use strict";
// Global objects for debugging purposes
let client;
let ui;
/**
* Class for handling the UI.
*/
class UI {
constructor() {
this.client = null;
// Initialize the UI
this.initUI();
// TODO start client only after the user entered their nickname
let chatID = "42";
let nickname = "binaryDiv";
this.initClient(chatID, nickname);
}
/**
* Initialize the web UI.
*/
initUI() {
// TODO stub
}
/**
* Create instance of Client and initialize connection.
*
* chatID: The ID of the chat instance.
* nickname: The user's nickname.
*/
initClient(chatID, nickname) {
const wsUri = AppSettings.serverWsUri;
this.client = new Client(wsUri, chatID, nickname);
// Subscribe to Client events
this.client.on("initialized", this._onClientInit.bind(this));
this.client.on("disconnected", this._onClientDisconnect.bind(this));
this.client.on("connectionError", this._onClientError.bind(this));
this.client.on("receivedMessage", this._onClientReceivedMessage.bind(this));
}
_onClientInit() {
console.log("UI: Connection initialized!");
// Send a test message
this.client.sendChatMessage("Meow meow! :3");
}
_onClientDisconnect() {
console.log("UI: Connection closed!");
}
_onClientError() {
console.log("UI: Connection error! :(");
}
_onClientReceivedMessage(msg) {
console.log("UI: Message from '" + msg.from + "', text: '" + msg.text + "'");
}
}
// Execute this on start (wrapped in an anonymous function)
(function() {
// TODO
ui = new UI();
client = ui.client;
})();

View File

@ -1,89 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python
# Example code from:
# https://websockets.readthedocs.io/en/stable/intro.html
import asyncio import asyncio
import json
import websockets import websockets
def log_message_error(error, message_text): async def hello(websocket, path):
print(f"[E] {error}") name = await websocket.recv()
print(f" Message: {message_text}") print(f"< {name}")
greeting = f"Hello {name}!"
async def parse_client_message(websocket, message_text): await websocket.send(greeting)
"""Parse a message (JSON object) from a client.""" print(f"> {greeting}")
try: start_server = websockets.serve(hello, '0.0.0.0', 32715)
# Parse JSON
message = json.loads(message_text)
# Handle message types
if message['action'] == 'init':
await handle_client_init(websocket, message)
else:
log_message_error(f"Unknown action '{message['action']}'", message_text)
except json.decoder.JSONDecodeError as e:
log_message_error(f"JSON decode error: {e}", message_text)
except KeyError as e:
log_message_error(f"Missing key {e} in JSON message", message_text)
async def handle_client_init(websocket, message):
"""Handle client 'init' message."""
# TODO input check for nickname
print(f"< init: chat_id='{message['chat_id']}', nickname='{message['nickname']}'")
await send_client_init_response(websocket)
await send_client_previous_messages(websocket)
async def send_client_init_response(websocket):
"""Send an init response message to a newly connected client."""
response = json.dumps({
"type": "init"
})
print(f"> {response}")
await websocket.send(response)
async def send_client_previous_messages(websocket):
"""Sends previously written chat messages to a newly connected client."""
# For now: send test messages
for i in range(1, 4):
testmsg = json.dumps({
"type": "message",
"from": "Alice",
"text": f"Message number {i}, hello!"
})
print(f"> {testmsg}")
await websocket.send(testmsg)
async def client_handler(websocket, path):
"""Handle client connection."""
print(f"++ New client {websocket.remote_address}")
try:
# Read and parse messages until connection is closed
async for message_text in websocket:
await parse_client_message(websocket, message_text)
except websockets.exceptions.ConnectionClosed:
# Ignore ConnectionClosed exceptions because we handle this in finally
pass
finally:
print(f"-- Client connection closed {websocket.remote_address}")
# Create WebSocket listener
start_server = websockets.serve(client_handler, '0.0.0.0', 32715)
try:
asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever() asyncio.get_event_loop().run_forever()
except KeyboardInterrupt:
print("[received SIGINT]")
pass
finally:
print("Exiting...")