Skip to content

Commit

Permalink
Add 'typing ...' status support
Browse files Browse the repository at this point in the history
  • Loading branch information
knadh committed Mar 8, 2020
1 parent 33c3c94 commit 4164af1
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 31 deletions.
1 change: 1 addition & 0 deletions internal/hub/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

// Types of messages sent to peers.
const (
TypeTyping = "typing"
TypeMessage = "message"
TypePeerList = "peer.list"
TypePeerInfo = "peer.info"
Expand Down
6 changes: 5 additions & 1 deletion internal/hub/peer.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@ func (p *Peer) processMessage(b []byte) {
// TODO: Respond
return
}
p.room.Broadcast(p.room.makeMessagePayload(msg, p))
p.room.Broadcast(p.room.makeMessagePayload(msg, p), true)

// "Typing" status.
case TypeTyping:
p.room.Broadcast(p.room.makePeerUpdatePayload(p, TypeTyping), false)

// Request for peers list
case TypePeerList:
Expand Down
12 changes: 7 additions & 5 deletions internal/hub/room.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,11 @@ func (r *Room) Dispose() {
}

// Broadcast broadcasts a message to all connected peers.
func (r *Room) Broadcast(data []byte) {
r.recordMsgPayload(data)
func (r *Room) Broadcast(data []byte, record bool) {
r.broadcastQ <- data
if record {
r.recordMsgPayload(data)
}

// Extend the room's expiry.
// if time.Since(r.timestamp) > time.Duration(30)*time.Second {
Expand Down Expand Up @@ -140,13 +142,13 @@ loop:
}

// Notify all peers of the new addition.
r.Broadcast(r.makePeerUpdatePayload(req.peer, TypePeerJoin))
r.Broadcast(r.makePeerUpdatePayload(req.peer, TypePeerJoin), true)
r.hub.log.Printf("%s@%s joined %s", req.peer.Handle, req.peer.ID, r.ID)

// A peer has left.
case TypePeerLeave:
r.removePeer(req.peer)
r.Broadcast(r.makePeerUpdatePayload(req.peer, TypePeerLeave))
r.Broadcast(r.makePeerUpdatePayload(req.peer, TypePeerLeave), true)
r.hub.log.Printf("%s@%s left %s", req.peer.Handle, req.peer.ID, r.ID)

// A peer has requested the room's peer list.
Expand Down Expand Up @@ -221,7 +223,7 @@ func (r *Room) removePeer(p *Peer) {
delete(r.peers, p)

// Notify all peers of the event.
r.Broadcast(r.makePeerUpdatePayload(p, TypePeerLeave))
r.Broadcast(r.makePeerUpdatePayload(p, TypePeerLeave), true)
}

// sendPeerList sends the peer list to the given peer.
Expand Down
1 change: 1 addition & 0 deletions theme/static/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var Client = new function () {
"Disconnect": "disconnect",
"DisposeRoom": "room.dispose",
"Message": "message",
"Typing": "typing",
"PeerList": "peer.list",
"PeerInfo": "peer.info",
"PeerJoin": "peer.join",
Expand Down
115 changes: 112 additions & 3 deletions theme/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ a {
color: #f74600;
}
a:hover {
color: #333;
color: #222;
}

ul.no {
Expand All @@ -95,6 +95,50 @@ input:focus {
box-shadow: 2px 2px 0px #aaa;
}

/* Dot spinner */
.dot-spinner {
display: inline-block;
}
.dot-spinner i {
display: inline-block;
width: 6px;
height: 6px;

border-radius: 50%;
background: #999;
vertical-align: middle;
}
.dot-spinner i:first-child {
transform: translate(-5px);
animation: dot-spinner-ani2 0.5s linear infinite;
opacity: 0;
}
.dot-spinner i:nth-child(2),
.dot-spinner i:nth-child(3) {
animation: dot-spinner-ani3 0.5s linear infinite;
}
.dot-spinner i:last-child {
animation: dot-spinner-ani1 0.5s linear infinite;
}

@keyframes dot-spinner-ani1 {
100% {
transform: translate(10px);
opacity: 0;
}
}
@keyframes dot-spinner-ani2 {
100% {
transform: translate(5px);
opacity: 1;
}
}
@keyframes dot-spinner-ani3 {
100% {
transform: translate(5px);
}
}

/* Helpers */
button,
.button {
Expand Down Expand Up @@ -172,7 +216,7 @@ form .charlimit-counter {
form .help {
display: block;
font-size: 0.75em;
color: #bbb;
color: #999;
}

/* Expand link component */
Expand Down Expand Up @@ -220,7 +264,7 @@ form .help {
text-align: center;
}
.footer a {
color: #aaa;
color: #999;
margin: 0 15px;
}
.footer a:hover {
Expand Down Expand Up @@ -317,6 +361,15 @@ form .help {
left: 0;
right: 0;
}
.form-chat .typing {
background: #fff;
color: #999;
font-size: 0.775em;
}
.form-chat .typing .handle {
margin-left: 10px;
display: inline-block;
}
.form-chat fieldset {
position: relative;
margin: 0;
Expand Down Expand Up @@ -377,3 +430,59 @@ form .help {
width: 100%;
}
}

.lds-ellipsis {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ellipsis span {
position: absolute;
top: 33px;
width: 13px;
height: 13px;
border-radius: 50%;
background: #fff;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.lds-ellipsis span:nth-child(1) {
left: 8px;
animation: lds-ellipsis1 0.6s infinite;
}
.lds-ellipsis span:nth-child(2) {
left: 8px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis span:nth-child(3) {
left: 32px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis span:nth-child(4) {
left: 56px;
animation: lds-ellipsis3 0.6s infinite;
}
@keyframes lds-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes lds-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes lds-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(24px, 0);
}
}
98 changes: 77 additions & 21 deletions theme/static/vue.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const notifType = {
notice: "notice",
error: "error"
};
const typingDebounceInterval = 3000;

Vue.component("expand-link", {
props: ["link"],
Expand Down Expand Up @@ -33,13 +34,21 @@ var app = new Vue({
sidebarOn: true,
disposed: false,
hasSound: true,

// Global flash / notifcation properties.
notifTimer: null,
notifMessage: "",
notifType: "",

// New activity animation in title bar. Page title is cached on load
// to use in the animation.
newActivity: false,
newActivityCounter: 0,
pageTitle: document.title,

typingTimer: null,
typingPeers: new Map(),

// Form fields.
roomName: "",
handle: "",
Expand All @@ -53,23 +62,7 @@ var app = new Vue({
},
created: function () {
this.initClient();

// Title bar "new activity" animation.
window.setInterval(() => {
if(!this.newActivity) {
return;
}
if(this.newActivityCounter % 2 === 0) {
document.title = "[•] " + this.pageTitle;
} else {
document.title = this.pageTitle;
}
this.newActivityCounter++;
}, 2500);
window.onfocus = () => {
this.newActivity = false;
document.title = this.pageTitle;
};
this.initTimers();
},
computed: {
Client() {
Expand Down Expand Up @@ -133,17 +126,38 @@ var app = new Vue({
});
},

// Send message.
handleEnter(e) {
// Capture keypresses to send message on Enter key and to broadcast
// "typing" statuses.
handleChatKeyPress(e) {
if (e.keyCode == 13 && !e.shiftKey) {
e.preventDefault();
this.handleSendMessage();
return;
}

// If it's a non "text" key, ignore.
if (!String.fromCharCode(e.keyCode).match(/(\w|\s)/g)) {
return;
}

// Debounce and wait for N seconds before sending a typing status.
if (this.typingTimer) {
return;
}

// Send the 'typing' status.
Client.sendMessage(Client.MsgType.Typing);

this.typingTimer = window.setTimeout(() => {
this.typingTimer = null;
}, typingDebounceInterval);
},

handleSendMessage() {
Client.sendMessage(Client.MsgType.Message, this.message);
this.message = "";
window.clearTimeout(this.typingTimer);
this.typingTimer = null;
},

handleDisposeRoom() {
Expand Down Expand Up @@ -295,14 +309,23 @@ var app = new Vue({
this.peers = peers;
},

onTyping(data) {
if (data.data.id === this.self.id) {
return;
}
this.typingPeers.set(data.data.id, { ...data.data, time: Date.now() });
this.$forceUpdate();
},

onMessage(data) {
// If the window isn't in focus, start the "new activity" animation
// in the title bar.
if(!document.hasFocus()) {
if (!document.hasFocus()) {
this.newActivity = true;
this.beep();
}

this.typingPeers.delete(data.data.peer_id);
this.messages.push({
type: Client.MsgType.Message,
timestamp: data.timestamp,
Expand All @@ -315,7 +338,6 @@ var app = new Vue({
});
this.$nextTick().then(function () {
this.$refs["messages"].scrollTop = this.$refs["messages"].scrollHeight;
console.log("scroll")
}.bind(this));
},

Expand All @@ -341,7 +363,41 @@ var app = new Vue({
Client.on(Client.MsgType.PeerLeave, (data) => { this.onPeerJoinLeave(data, Client.MsgType.PeerLeave); });
Client.on(Client.MsgType.PeerRateLimited, this.onRateLimited);
Client.on(Client.MsgType.Message, this.onMessage);
Client.on(Client.MsgType.Typing, this.onTyping);
Client.on(Client.MsgType.Dispose, this.onDispose);
},

initTimers() {
// Title bar "new activity" animation.
window.setInterval(() => {
if (!this.newActivity) {
return;
}
if (this.newActivityCounter % 2 === 0) {
document.title = "[•] " + this.pageTitle;
} else {
document.title = this.pageTitle;
}
this.newActivityCounter++;
}, 2500);
window.onfocus = () => {
this.newActivity = false;
document.title = this.pageTitle;
};

// Sweep "typing" statuses at regular intervals.
window.setInterval(() => {
let changed = false;
this.typingPeers.forEach((p) => {
if ((p.time + typingDebounceInterval) < Date.now()) {
this.typingPeers.delete(p.id);
changed = true;
}
});
if(changed) {
this.$forceUpdate();
}
}, typingDebounceInterval);
}
}
});
Loading

0 comments on commit 4164af1

Please sign in to comment.