skynet/src/index.html

185 lines
5.1 KiB
HTML

<div id="app"></div>
<style>
.messages li {
list-style-type: none;
}
.internal {
color: gray;
}
.past {
color: slateblue;
}
.user {
color: blue;
}
.user::before {
content: "> ";
}
.remote::before {
content: "< ";
}
#app input {
width: 100%;
}
</style>
<script src="https://unpkg.com/hyperapp@1.2.9/dist/hyperapp.js"></script>
<script src="https://unpkg.com/@hyperapp/html@1.1.1/dist/hyperappHtml.js"></script>
<script src="https://unpkg.com/moment@2.22.2/min/moment.min.js"></script>
<script>
const h = hyperappHtml;
const push = (xs, x) => xs.concat([x]);
const state = {
messages: [],
websocket: null,
URL: (window.location.href + "connect").replace("http", "ws"),
channel: "default"
};
let windowVisible = true;
let doNotify = false;
window.onfocus = () => { windowVisible = true; doNotify = false; };
window.onblur = () => { windowVisible = false; };
const blinkTime = 1000;
// Blink title a bit by adding then removing ***.
setInterval(() => {
if (doNotify && !windowVisible) {
let title = document.title;
document.title = "*** " + title;
setTimeout(() => {
document.title = title;
}, blinkTime);
}
}, blinkTime * 2);
const notify = () => { doNotify = !windowVisible; }; // do not notify if window is visible
const stringifyMessage = m => {
let out = "";
if (m.time) {
out += moment(m.time).format("hh:mm:ss") + ": ";
}
if (m.channel) {
out += m.channel + ": ";
}
if (m.message) {
const msg = m.message;
if (typeof msg === "string") {
out += msg;
} else {
out += JSON.stringify(msg);
}
}
return out;
}
const actions = {
connect: () => (state, actions) => {
console.log("CONNECT", state.URL);
if (state.websocket != null && state.websocket.close) state.websocket.close();
const ws = new WebSocket(state.URL);
ws.addEventListener("message", ev => {
actions.handleMessage(ev.data);
});
ws.addEventListener("close", ev => actions.message(["internal", "Connection closed."]));
ws.addEventListener("open", ev => {
actions.message(["internal", "Connected."]);
actions.sendJSON({
type: "open",
channel: "*" // wildcard
});
actions.sendJSON({
type: "log"
});
});
return {websocket: ws}
},
handleMessage: data => (state, actions) => {
const message = JSON.parse(data);
const type = message.type;
console.log("RECV", message)
if (type === "message") {
actions.normalMessage(["remote", message])
} else if (type === "result" && message["for"] === "log") {
const pastMessages = message.log.reverse(); // Messages are sent to us newest-first
pastMessages.forEach(m => {
actions.normalMessage(["remote past", m]);
});
} else if (type === "error") {
console.warn(message);
}
},
message: value => state => ({messages: push(state.messages, value)}),
normalMessage: value => (state, actions) => actions.message([value[0], stringifyMessage(value[1])]),
sendJSON: value => state => {
console.log("SEND", value);
if (state.websocket !== null && state.websocket.readyState === 1) {
state.websocket.send(JSON.stringify(value));
} else {
actions.message(["internal", "Not connected."]);
}
},
sendMessage: ([channel, message]) => (state, actions) => {
actions.sendJSON({
type: "message",
channel,
message
});
},
msgInput: event => (state, actions) => {
if (event.keyCode == 13) { // enter key
let val = event.target.value;
const channel = state.channel;
actions.sendMessage([channel, val]);
actions.normalMessage(["user", {
channel,
message: val
}]);
event.target.value = "";
}
},
channelInput: event => (state, actions) => {
return { channel: event.target.value };
},
urlInput: event => (state, actions) => {
const val = event.target.value;
if (event.keyCode == 13) { // enter key
actions.connect();
}
return { URL: val };
}
};
const cls = x => ({ class: x });
const scrollDown = () => {
const scrollEl = document.scrollingElement;
scrollEl.scrollTop = scrollEl.scrollHeight;
};
const viewMessage = m => h.li(cls(m[0]), m[1]);
const view = (state, actions) => h.div([
h.div([
h.input({ onkeyup: actions.urlInput, placeholder: "URL", value: state.URL })
]),
h.ul({class: "messages", onupdate: (element, old) => scrollDown()}, state.messages.map(viewMessage)),
h.input({ onkeyup: actions.channelInput, placeholder: "Channel", value: state.channel }),
h.input({ onkeyup: actions.msgInput, placeholder: "Message" }),
]);
const main = hyperapp.app(state, actions, view, document.getElementById("app"));
main.connect();
</script>