|
/** |
|
* 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); |