@ 57cd687b81d1640c584c2673280ec456a5463380 | history
<html>
<!-- Questions? Comments? Concerns? Let us know what you think: 1-800-TMP-CHAT@nmyk.io -->
<head>
<link href="https://fonts.googleapis.com/css?family=Inconsolata" rel="stylesheet">
<link href="https://cdn.nmyk.io/assets/style.css" rel="stylesheet">
<link href="https://cdn.nmyk.io/assets/favicon.ico" rel="icon">
<meta charset="UTF-8">
<meta name="viewport" content="width=500">
<meta name="description"
content="tmpchat is a WebRTC-based group text messaging application designed and built by Nick Mykins (nmyk.io)"/>
<title>tmpch.at - {{.ChannelName}}</title>
<script type="text/javascript" src="https://cdn.nmyk.io/assets/he.js"></script>
<script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script>
const signalingURL = "{{.SignalingURL|safeURL}}";
const myUserID = "{{.UserID}}";
const SEPARATOR = " โข ";
const EMOJI = ["๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐ฅฅ", "๐ฅ", "๐
", "๐", "๐ฅ", "๐ฅฆ", "๐ฅ", "๐ถ", "๐ฝ", "๐ฅ", "๐ฅ", "๐ ", "๐ฅ", "๐", "๐ฅ", "๐ฅจ", "๐ง", "๐ณ", "๐ฅ", "๐ฅ", "๐ฅฉ", "๐", "๐", "๐ญ", "๐", "๐", "๐", "๐ฅช", "๐ฅ", "๐ฎ", "๐ฏ", "๐ฅ", "๐ฅ", "๐ฅซ", "๐", "๐", "๐ฒ", "๐", "๐ฃ", "๐ฑ", "๐ฅ", "๐ค", "๐", "๐", "๐", "๐ฅ", "๐ฅ ", "๐ข", "๐ก", "๐ง", "๐จ", "๐ฆ", "๐ฅง", "๐ฐ", "๐", "๐ฎ", "๐ญ", "๐ฌ", "๐ซ", "๐ฟ", "๐ฉ", "๐ช", "๐ฐ", "๐ฅ", "๐ฏ", "๐ฅ", "๐ผ", "โ", "๏ธ๐ต", "๐ฅค", "๐ถ", "๐บ", "๐ป", "๐ฅ", "๐ท", "๐ฅ", "๐ธ", "๐น", "๐พ", "๐ฅก", "โฝ", "๐", "๐", "โ", "๐พ", "๐", "๐", "๐ฑ", "๐", "๐ธ", "๐", "๐", "๐", "๐ฅ
", "โณ", "๐ฅ", "๐ฅ", "๐ฝ", "๐", "๐ฅ", "๐ญ", "๐จ", "๐ฌ", "๐ค", "๐ง", "๐ผ", "๐น", "๐ฅ", "๐ท", "๐บ", "๐ธ", "๐ป", "๐ฒ", "๐", "๐ฏ", "๐ณ", "๐ฎ", "๐ฐ", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐ฃ", "๐ค", "๐ฅ", "๐ฆ", "๐จ", "๐ช", "๐ซ", "๐ฉ", "๐", "๐", "๐ก", "๐", "๐", "๐
", "๐", "๐ ", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐ง", "๐จ", "โ", "๐ฌ", "๐", "โ๏ธ", "๐ซ", "๐ฎ", "๐ฟ", "๐", "๐", "๐", "๐ธ", "๐ฐ", "๐ฆ", "๐ฏ", "๐", "๐ฃ", "๐ฟ", "๐ฝ", "๐ผ", "๐ฐ", "๐ฏ", "๐", "๐ก", "๐ข", "๐ ", "๐ฒ", "๐บ", "๐ธ", "๐ผ", "๐ป", "๐", "๐ณ", "๐ด", "๐ฑ", "๐ฟ", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐พ", "๐", "๐ท", "๐น", "๐ฅ", "๐", "๐", "๐ฟ", "๐ฆ", "๐พ", "๐", "๐", "๐ต", "๐", "๐", "๐", "๐ฆ", "๐ฆ", "๐ช", "๐ซ", "๐ฆ", "๐ก", "๐ ", "๐", "๐ฌ", "๐ณ", "๐", "๐ฆ", "๐", "๐
", "๐", "๐ฆ", "๐ฆ", "๐", "๐", "๐", "๐", "๐", "๐", "๐", "๐ฆ", "๐ข", "๐", "๐ฆ", "๐ฆ", "๐ฆ", "๐", "๐ฆ", "๐ฆ", "๐ฆ", "๐ฆ", "๐", "๐", "๐", "๐ฆ", "๐ฆ
", "๐ฆ", "๐ฆ", "๐บ", "๐", "๐ด", "๐ฆ", "๐", "๐", "๐ฃ", "๐ถ", "๐ฑ", "๐ญ", "๐น", "๐ฐ", "๐ฆ", "๐ป", "๐ผ", "๐จ", "๐ฏ", "๐ฆ", "๐ฎ", "๐ท", "๐ธ", "๐ต", "๐", "๐น", "๐บ", "๐คก", "๐ฉ", "๐ป", "๐", "๐", "๐ฝ", "๐พ", "๐ค", "๐", "๐น", "๐ป", "๐ง", "๐ ", "๐", "๐", "๐ฉ", "๐", "๐งข", "โ", "๐", "๐", "๐ผ", "๐โ"];
const SignalingEvent = {
"Entrance": 0,
"Exit": 1,
"RTCOffer": 2,
"RTCAnswer": 3,
"RTCICECandidate": 4,
"TURNCredRequest": 5,
"TURNCredResponse": 6
};
const TmpchatEvent = {
"Message": 7,
"Clear": 8,
"NameChange": 9
};
const chooseOne = arr => arr[Math.floor(Math.random() * arr.length)];
let myName = chooseOne(EMOJI);
const newMessage = (type, content) => {
return {
"from": myUserID,
"type": type,
"content": content
};
};
const broadcast = message => {
for (let id in rtcPeerConns) {
let dc = rtcPeerConns[id]["dataChannel"];
if (dc && dc.readyState === "open") {
dc.send(JSON.stringify(message));
}
}
};
const nameTag = (message, isFromMe) => {
let tag = document.createElement("div");
let name = document.createElement("span");
name.className = message["from"];
name.innerHTML = isFromMe ? myName : userNames[message["from"]];
tag.appendChild(name);
if (isFromMe) {
tag.className = "myname";
tag.innerHTML = SEPARATOR + tag.innerHTML;
} else {
tag.className = "theirname";
tag.innerHTML = tag.innerHTML + SEPARATOR;
}
return tag;
};
const shouldStackMsg = (message, lastMsgElement) => {
if (message["type"] !== TmpchatEvent.Message || !lastMsgElement) {
return false;
}
if (lastMsgElement.className === "systemmessage") {
return false;
}
let lastMsgUserId = lastMsgElement.firstElementChild.firstElementChild.className;
return message["from"] === lastMsgUserId;
};
const announceEntrance = user => {
let name = document.createElement("span");
name.className = user["id"];
name.innerHTML = user["name"];
announce(name.outerHTML + " joined");
};
const announceExit = user => {
let name = document.createElement("span");
name.className = user["id"];
name.innerHTML = userNames[user["id"]];
announce(name.outerHTML + " left");
};
const announce = announcementHTML => {
let announcement = document.createElement("div");
announcement.className = "systemmessage";
announcement.innerHTML = announcementHTML;
document.getElementById("messagelog").appendChild(announcement);
};
const appendToRoll = user => {
userNames[user["id"]] = user["name"];
let tag = document.createElement("div");
let name = document.createElement("span");
name.className = user["id"];
name.innerHTML = user["name"];
tag.appendChild(name);
tag.style.display = "inline";
tag.innerHTML = SEPARATOR + tag.innerHTML;
document.getElementById("namechange").appendChild(tag);
};
const doClear = () => {
let node = document.getElementById("messagelog");
while (node.firstChild) {
node.removeChild(node.firstChild);
}
document.getElementById("messagetext").focus();
};
const doNameChange = message => {
let userId = message["from"];
let newName = he.escape(message["content"]);
let toChange = document.getElementsByClassName(userId);
for (let i = 0; i < toChange.length; i++) {
toChange[i].innerHTML = newName;
}
if (userId === myUserID) {
myName = newName;
resetNameChangeInput();
} else {
userNames[userId] = newName;
}
};
const resetNameChangeInput = () => {
document.getElementById("myname").value = he.unescape(myName);
document.getElementById("myname").size = he.unescape(myName).length;
};
const newNameIsOk = newName => {
if (newName === "" || newName === myName) {
return false
}
for (let id in userNames) {
if (userNames[id] === newName) {
return false;
}
}
return true;
};
const shuffle = array => {
let i = array.length, tmp, r;
while (0 !== i) {
r = Math.floor(Math.random() * i);
i -= 1;
tmp = array[i];
array[i] = array[r];
array[r] = tmp;
}
return array;
};
const getNewName = () => {
if (Object.keys(userNames).length >= EMOJI.length) {
let n = 0;
while (!newNameIsOk(String(n))) {
n += 1;
}
return String(n);
}
let e = shuffle(EMOJI);
for (let i = 0; i < EMOJI.length; i++) {
if (newNameIsOk(e[i])) {
return e[i];
}
}
};
const parseAndValidate = (event, dataChannel) => {
let message = JSON.parse(event.data), nothing = {};
let userID = message && message["from"];
let isValid = (
message["type"] &&
userID &&
userID !== myUserID &&
rtcPeerConns.hasOwnProperty(userID) &&
rtcPeerConns[userID]["dataChannel"] === dataChannel
);
return isValid ? message : nothing;
};
const handleTmpchatEvent = (event, dataChannel) => {
let message = parseAndValidate(event, dataChannel);
switch (message.type) {
case TmpchatEvent.Message:
write(message);
break;
case TmpchatEvent.Clear:
doClear();
break;
case TmpchatEvent.NameChange:
doNameChange(message);
break;
}
};
const write = message => {
let messageLog = document.getElementById("messagelog");
const lastMsgElement = messageLog.lastElementChild;
if (shouldStackMsg(message, lastMsgElement)) {
let currentText = lastMsgElement.getElementsByClassName("chatmessage")[0];
currentText.textContent += "\n" + message["content"];
} else {
let isFromMe = message["from"] === myUserID;
let name = nameTag(message, isFromMe);
let msg = document.createElement("div");
msg.className = isFromMe ? "mymessage" : "theirmessage";
let pre = document.createElement("pre");
pre.className = "chatmessage";
pre.textContent = message["content"];
msg.appendChild(pre);
msg.insertAdjacentHTML("afterbegin", name.outerHTML);
messageLog.appendChild(msg);
}
if (document.activeElement === document.getElementById("messagetext")) {
messageLog.scrollTop = messageLog.scrollHeight;
}
};
const info = txt => {
document.getElementById("info").innerText = txt;
};
const rtcPeerConns = {};
const userNames = {};
const getMember = id => {
return {"id": id, "name": userNames[id]}
};
let ws = new WebSocket(`${signalingURL}/?userID=${myUserID}&channelName={{.ChannelName}}`);
ws.sendMessage = message => ws.send(JSON.stringify(message));
ws.onopen = () => {
ws.sendMessage(newMessage(SignalingEvent.TURNCredRequest, myName));
};
ws.onerror = event => {
info(event);
ws.close();
};
const addNewRTCPeerConn = (turnCreds, member, isLocal) => {
// isLocal: true if we're already in the chat and adding an
// RTCPeerConnection for a new arrival. false if we're adding
// RTCPeerConnections for existing members because we're new.
let pc = new RTCPeerConnection({
iceServers: [{
urls: "turns:turn.tmpch.at:443",
username: turnCreds["username"],
credential: turnCreds["credential"]
}]
});
pc.onicecandidate = event => {
if (event.candidate !== null) {
let desc = btoa(JSON.stringify(event.candidate));
let msg = newMessage(SignalingEvent.RTCICECandidate, desc);
msg["to"] = member["id"];
ws.sendMessage(msg);
}
};
pc.onnegotiationneeded = () => pc.createOffer()
.then(d => pc.setLocalDescription(d))
.then(() => {
if (isLocal) {
let desc = btoa(JSON.stringify(pc.localDescription));
let msg = newMessage(SignalingEvent.RTCOffer, {"name": myName, "desc": desc});
msg["to"] = member["id"];
ws.sendMessage(msg);
}
})
.catch(info);
pc.ondatachannel = event => {
event.channel.onopen = () => {
rtcPeerConns[member["id"]]["dataChannel"] = event.channel;
if (!isLocal && userNames[member["id"]] === myName) {
let newName = getNewName();
let message = newMessage(TmpchatEvent.NameChange, newName);
doNameChange(message);
broadcast(message);
}
};
event.channel.onmessage = e => handleTmpchatEvent(e, event.channel);
};
rtcPeerConns[member["id"]] = {
"conn": pc,
};
};
const answerRTCOffer = message => {
let offerDesc = JSON.parse(atob(message["content"]["desc"]));
let member = getMember(message["from"]);
rtcPeerConns.add(member, false);
let peerConn = rtcPeerConns[message["from"]]["conn"];
peerConn.setRemoteDescription(new RTCSessionDescription(offerDesc))
.then(() => peerConn.createAnswer())
.then(answer => peerConn.setLocalDescription(answer))
.then(() => {
let desc = btoa(JSON.stringify(peerConn.localDescription));
let response = newMessage(SignalingEvent.RTCAnswer, desc);
response["to"] = message["from"];
ws.sendMessage(response);
})
.catch(info);
};
const addNewDataChannel = member => {
let dc = rtcPeerConns[member["id"]]["conn"]
.createDataChannel(unescape(window.location.pathname.substr(1)));
dc.onclose = () => {
console.log(`dataChannel for ${member["id"]} has closed`);
delete rtcPeerConns[member["id"]];
};
dc.onopen = () => rtcPeerConns[member["id"]]["dataChannel"] = dc;
dc.onmessage = event => handleTmpchatEvent(event, dc);
rtcPeerConns[member["id"]]["dataChannel"] = dc;
};
ws.onmessage = event => {
let message = JSON.parse(event.data);
switch (message.type) {
case SignalingEvent.Entrance:
handleEntrance(message);
break;
case SignalingEvent.Exit:
handleExit(message);
break;
case SignalingEvent.RTCOffer:
handleRTCOffer(message);
break;
case SignalingEvent.RTCAnswer:
handleRTCAnswer(message);
break;
case SignalingEvent.RTCICECandidate:
handleICECandidate(message);
break;
case SignalingEvent.TURNCredResponse:
handleTURNCredResponse(message);
}
};
const handleEntrance = message => {
let member = message["content"];
if (rtcPeerConns[member["id"]]) {
return;
}
if (member["id"] !== myUserID) {
rtcPeerConns.add(member, true);
addNewDataChannel(member);
appendToRoll(member);
}
announceEntrance(member);
};
const handleExit = message => {
let member = getMember(message["from"]);
if (member["id"] !== myUserID) {
let element = document.getElementById("namechange").getElementsByClassName(member["id"])[0];
element.parentElement.outerHTML = "";
announceExit(member);
delete userNames[member["id"]];
delete rtcPeerConns[member["id"]];
}
};
const handleRTCOffer = message => {
userNames[message["from"]] = message["content"]["name"];
appendToRoll(getMember(message["from"]));
answerRTCOffer(message);
};
const handleRTCAnswer = message => {
let answerDesc = JSON.parse(atob(message["content"]));
rtcPeerConns[message["from"]]["conn"]
.setRemoteDescription(new RTCSessionDescription(answerDesc))
.catch(info);
};
const handleICECandidate = message => {
let candidate = JSON.parse(atob(message["content"]));
rtcPeerConns[message["from"]]["conn"]
.addIceCandidate(candidate)
.catch(info);
};
const handleTURNCredResponse = message => {
rtcPeerConns.add = (member, isLocal) =>
addNewRTCPeerConn(message["content"], member, isLocal)
};
window.onload = () => {
const input = document.getElementById("messagetext");
document.getElementById("send").onclick = () => {
if (input.value === "") {
return false;
}
let msg = newMessage(TmpchatEvent.Message, input.value);
write(msg);
broadcast(msg);
input.value = "";
return false;
};
document.getElementById("myname").onblur =
document.getElementById("namechange").onsubmit = () => {
let newName = document.getElementById("myname").value;
if (!newNameIsOk(newName)) {
input.focus();
resetNameChangeInput();
return false;
}
let message = newMessage(TmpchatEvent.NameChange, newName);
doNameChange(message);
broadcast(message);
input.focus();
return false;
};
document.getElementById("myname").onfocus = () => {
document.getElementById("myname").value = "";
document.getElementById("myname").size = 10;
return false;
};
document.getElementById("clear").onclick = () => {
if (!ws) {
doClear();
return false;
}
doClear();
broadcast(newMessage(TmpchatEvent.Clear, null));
return false;
};
input.onfocus = () => {
let m = document.getElementById("messagelog");
m.scrollTop = m.scrollHeight;
};
let doubleEnterTs; // Clear chat by double-pressing Enter with no text in the input field
input.onkeypress = event => {
if (event.key === "Enter" && !event.shiftKey) {
if (input.value === "" && (Date.now() - doubleEnterTs < 150)) {
document.getElementById("clear").click();
return false;
}
if (input.value === "") {
doubleEnterTs = Date.now();
return false;
}
document.getElementById("send").click();
return false;
}
};
document.getElementById("myname").value = myName;
input.focus();
};
</script>
</head>
<body>
<div class="vspace50px"></div>
<div class="container" style="width: 440px; text-align: center;">
<a href="/" type="home" style="margin:auto;">tmpchat</a>
<br>
#{{.ChannelName}}
</div>
<br>
<div class="container" style="width: 440px;">
<div class="chatcontainer">
<div class="messagelog" id="messagelog"></div>
</div>
</div>
<div class="chatui">
<div style="margin-top: 2px;">
<form id="namechange">
online: <input id="myname" autocomplete="off" type="nametext" size=2 maxlength=16>
</form>
<div style="display:inline-block;float: right;">
<input type="button" id="clear" value="clear">
</div>
</div>
</div>
<div class="chatui">
<form id="message">
<div class="messagetextcontainer">
<textarea rows=1 id="messagetext"></textarea>
</div>
<div style="float: right;">
<input type="submit" id="send" value="send">
</div>
</form>
</div>
<br>
<div id="info" class="chatui" style="text-align: center;"></div>
</body>
</html>