Skip to content

Instantly share code, notes, and snippets.

@andersevenrud
Last active December 24, 2019 23:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andersevenrud/4833d44de7749e482ed4641ddc00fd15 to your computer and use it in GitHub Desktop.
Save andersevenrud/4833d44de7749e482ed4641ddc00fd15 to your computer and use it in GitHub Desktop.
Very basic OS.js Chat application

Based on example from https://manual.os-js.org/v3/guide/framework/#react

Very basic chat application:

  • Uses a custom websocket server
  • Uses React to render UI
  • Uses event subscription model to handle websocket messages

THIS IS JUST FOR DEMONSTRATION PURPOSES

/**
* A very basic chat application
*
* - Uses a custom websocket server
* - Uses React to render UI
* - Uses event subscription model to handle websocket messages
*/
import './index.scss';
import osjs from 'osjs';
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import {name as applicationName} from './metadata.json';
/**
* User Interface for a chat dialogue
*/
const ChatWindow = ({proc, win, username}) => {
const [message, setMessage] = useState('');
const [messages, setMessages] = useState([]);
// Subscribe to our internal window events
useEffect(() => {
const onMessageRecieved = (msg) => {
setMessages([
`${msg.from}: ${msg.message}`,
...messages
]);
};
win.on('render-message', onMessageRecieved);
return () => {
win.off('render-message', onMessageRecieved)
};
});
// Event handling
const onChangeMessage = (ev) => {
setMessage(ev.target.value);
};
const onSendClick = () => {
if (message) {
proc.emit('send-message', {
to: username,
message
});
setMessages([
`Me: ${message}`,
...messages
]);
}
setMessage('');
};
return (
<div>
<h1>{username}</h1>
<div>
<textarea value={messages.join('\n')} readOnly></textarea>
</div>
<div>
<input type="text" placeholder="send message" value={message} onInput={onChangeMessage} />
<button type="button" onClick={onSendClick}>Send</button>
</div>
</div>
);
};
/**
* A factory to create new dialogue windows
*/
const chatWindowFactory = (proc, metadata) => (username, cb = () => {}) => {
// Makes sure we only have one window open per person
const id = `SimpleChatWindow-${username}`;
const alreadyCreated = proc.windows
.find(w => w.id === id);
if (alreadyCreated) {
alreadyCreated.focus();
cb(alreadyCreated);
return alreadyCreated;
}
// Create window and render react application
return proc.createWindow({
id,
title: metadata.title.en_EN,
dimension: {width: 400, height: 400}
})
.on('render', cb)
.render(($content, win) => ReactDOM.render(
React.createElement(ChatWindow, {
win,
proc,
username
}),
$content
));
};
/**
* Main Window User Interface
*/
const MainWindow = ({proc, metadata}) => {
const [connected, setConnected] = useState(false);
const [users, setUsers] = useState([]);
const [username, setUsername] = useState('');
const createChatWindow = chatWindowFactory(proc, metadata);
// Subscribe to our internal application events
useEffect(() => {
const onUpdateUserList = (list) => setUsers(list);
proc.on('update-user-list', onUpdateUserList);
return () => {
proc.off('update-user-list', onUpdateUserList)
};
}, []);
// Event handling
const onChangeUsername = (ev) => {
setUsername(ev.target.value);
proc.emit('username-changed', ev.target.value);
};
const onUserClick = (otherUsername) => () => {
createChatWindow(otherUsername);
};
const onConnectClick = () => {
setConnected(true);
proc.emit('connect-clicked', username);
};
return (
<>
{connected ? (
<div>
<ol>
{users.map(u => (
<li key={u}>
<button type="button" onClick={onUserClick(u)}>{`Chat with: ${u}`}</button>
</li>
))}
</ol>
</div>
): (
<div>
<div>
<input type="text" placeholder="username" value={username} onInput={onChangeUsername} />
</div>
<div>
<button type="button" disabled={username.length === 0} onClick={onConnectClick}>Connect</button>
</div>
</div>
)}
</>
);
};
/**
* Creates our main window
*/
const createMainWindow = (proc, metadata) => {
// Create window and render react application
proc.createWindow({
id: 'SimpleChatWindow',
title: metadata.title.en_EN,
dimension: {width: 400, height: 400}
})
.on('destroy', () => proc.destroy())
.render($content => ReactDOM.render(
React.createElement(MainWindow, {
proc,
metadata
}),
$content
));
};
// Our launcher
const register = (core, args, options, metadata) => {
// Create a new Application instance
const proc = core.make('osjs/application', {args, options, metadata});
// Creates a new WebSocket connection (see server.js)
const sock = proc.socket('/socket');
// Creates a new main window
createMainWindow(proc, metadata, sock);
// Listen for websocket messages
const createChatWindow = chatWindowFactory(proc, metadata);
sock.on('message', ({data}) => {
const msg = JSON.parse(data);
if (msg.type === 'users') {
proc.emit('update-user-list', msg.data);
} else if (msg.type === 'message') {
createChatWindow(msg.data.from, (win) => {
win.emit('render-message', msg.data);
});
}
});
// An internal event to set username
let myUsername;
proc.on('username-changed', (username) => {
myUsername = username;
});
// An internal event to send messages to websocket server
proc.on('send-message', data => {
sock.send(JSON.stringify({
type: 'message',
data: Object.assign({ from: myUsername }, data)
}));
});
// An internal event to send a connection signal to websocket server
proc.on('connect-clicked', (username) => {
sock.send(JSON.stringify({
type: 'connect',
data: username
}));
});
return proc;
};
// Creates the internal callback function when OS.js launches an application
osjs.register(applicationName, register);
const WebSocket = require('ws');
// Methods OS.js server requires
module.exports = (core, proc) => {
let users = [];
let connectionCounter = 0;
const broadcast = msg => core.wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
if (client._simplechat) {
client.send(JSON.stringify(msg));
}
}
});
const sendUserList = () => {
const list = users
.filter(user => !!user)
.map(user => user.username);
broadcast({
type: 'users',
data: list
});
};
return {
// When server initializes
init: async () => {
core.app.ws(proc.resource('/socket'), (ws, req) => {
let connectionId = connectionCounter++;
users[connectionId] = null;
ws._simplechat = true;
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.type === 'connect') {
users[connectionId] = {
socket: ws,
username: msg.data
};
sendUserList();
} else if (msg.type === 'message') {
const foundConnection = users.find(user => {
return user && user.username === msg.data.to;
});
if (foundConnection) {
foundConnection.socket.send(JSON.stringify({
type: 'message',
data: msg.data
}));
}
}
});
ws.on('close', () => {
users[connectionId] = null;
sendUserList();
});
});
},
// When server starts
start: () => {
},
// When server goes down
destroy: () => {
},
// When using an internally bound websocket, messages comes here
onmessage: (ws, respond, args) => {}
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment