From 5f4b14ecc4fe087d38fd0a6b4f7c7f2cdc075692 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Fri, 30 Jun 2023 20:00:21 -0700 Subject: [PATCH] Emoji reactions --- pkg/handlers.go | 11 +++++++ pkg/messages.go | 1 + pkg/websocket.go | 2 ++ web/static/css/chat.css | 14 +++++++++ web/static/js/BareRTC.js | 62 +++++++++++++++++++++++++++++++++++++++- web/templates/chat.html | 31 +++++++++++++++++++- 6 files changed, 119 insertions(+), 2 deletions(-) diff --git a/pkg/handlers.go b/pkg/handlers.go index f0bafb3..766f1b0 100644 --- a/pkg/handlers.go +++ b/pkg/handlers.go @@ -170,6 +170,17 @@ func (s *Server) OnTakeback(sub *Subscriber, msg Message) { }) } +// OnReact handles emoji reactions for chat messages. +func (s *Server) OnReact(sub *Subscriber, msg Message) { + // Forward the reaction to everybody. + s.Broadcast(Message{ + Action: ActionReact, + Username: sub.Username, + Message: msg.Message, + MessageID: msg.MessageID, + }) +} + // OnFile handles a picture shared in chat with a channel. func (s *Server) OnFile(sub *Subscriber, msg Message) { if sub.Username == "" { diff --git a/pkg/messages.go b/pkg/messages.go index a84da37..7ce68e7 100644 --- a/pkg/messages.go +++ b/pkg/messages.go @@ -57,6 +57,7 @@ const ( ActionUnwatch = "unwatch" // user has closed your video ActionFile = "file" // image sharing in chat ActionTakeback = "takeback" // user takes back (deletes) their message for everybody + ActionReact = "react" // emoji reaction to a chat message // Actions sent by server only ActionPing = "ping" diff --git a/pkg/websocket.go b/pkg/websocket.go index ff8988a..dcaba2b 100644 --- a/pkg/websocket.go +++ b/pkg/websocket.go @@ -105,6 +105,8 @@ func (sub *Subscriber) ReadLoop(s *Server) { s.OnUnwatch(sub, msg) case ActionTakeback: s.OnTakeback(sub, msg) + case ActionReact: + s.OnReact(sub, msg) default: sub.ChatServer("Unsupported message type.") } diff --git a/web/static/css/chat.css b/web/static/css/chat.css index ac0d6d0..2d40f46 100644 --- a/web/static/css/chat.css +++ b/web/static/css/chat.css @@ -274,3 +274,17 @@ div.feed.popped-out { .cursor-notallowed { cursor: not-allowed; } + +/* Emoji reaction button support */ +.position-relative { + position: relative; +} +.emoji-button { + position: absolute; + right: 12px; + bottom: 12px; +} +.emoji-button button { + background-color: rgba(255, 255, 255, 0.5) !important; + color: rgba(0, 0, 0, 0.5) !important; +} diff --git a/web/static/js/BareRTC.js b/web/static/js/BareRTC.js index f467d25..664e52a 100644 --- a/web/static/js/BareRTC.js +++ b/web/static/js/BareRTC.js @@ -62,7 +62,18 @@ const app = Vue.createApp({ ready: false, audioContext: null, audioTracks: {}, - } + }, + reactions: [ + '❤️', + '😂', + // '😉', + // '😢', + // '😡', + '🔥', + // '😈', + '🍑', + '🍆', + ] }, // User JWT settings if available. @@ -166,6 +177,14 @@ const app = Vue.createApp({ autoscroll: true, // scroll to bottom on new messages fontSizeClass: "", // font size magnification DMs: {}, + messageReactions: { + // Will look like: + // "123": { (message ID) + // "❤️": [ (reaction emoji) + // "username" // users who reacted + // ] + // } + }, // Responsive CSS controls for mobile. responsive: { @@ -390,6 +409,35 @@ const app = Vue.createApp({ // TODO }, + // Emoji reactions + sendReact(message, emoji) { + this.ws.conn.send(JSON.stringify({ + action: 'react', + msgID: message.msgID, + message: emoji, + })); + }, + onReact(msg) { + // Search all channels for this message ID and append the reaction. + let msgID = msg.msgID, + who = msg.username, + emoji = msg.message; + + if (this.messageReactions[msgID] == undefined) { + this.messageReactions[msgID] = {}; + } + if (this.messageReactions[msgID][emoji] == undefined) { + this.messageReactions[msgID][emoji] = []; + } + + // don't count double reactions of same emoji from same chatter + for (let reactor of this.messageReactions[msgID][emoji]) { + if (reactor === who) return; + } + + this.messageReactions[msgID][emoji].push(who); + }, + // Sync the current user state (such as video broadcasting status) to // the backend, which will reload everybody's Who List. sendMe() { @@ -641,6 +689,9 @@ const app = Vue.createApp({ case "takeback": this.onTakeback(msg); break; + case "react": + this.onReact(msg); + break; case "presence": this.onPresence(msg); break; @@ -1001,6 +1052,15 @@ const app = Vue.createApp({ }) }, + /* message reaction emojis */ + hasReactions(msg) { + return this.messageReactions[msg.msgID] != undefined; + }, + getReactions(msg) { + if (!this.hasReactions(msg)) return []; + return this.messageReactions[msg.msgID]; + }, + activeChannels() { // List of current channels, unread indicators etc. let result = []; diff --git a/web/templates/chat.html b/web/templates/chat.html index c58a421..1ca0183 100644 --- a/web/templates/chat.html +++ b/web/templates/chat.html @@ -777,7 +777,7 @@ -
+
+ + +
[[msg.message]]
+ + +
+ + [[emoji]] [[users.length]] + +