Emoji reactions

This commit is contained in:
Noah 2023-06-30 20:00:21 -07:00
parent 5c2a1d6246
commit 5f4b14ecc4
6 changed files with 119 additions and 2 deletions

View File

@ -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. // OnFile handles a picture shared in chat with a channel.
func (s *Server) OnFile(sub *Subscriber, msg Message) { func (s *Server) OnFile(sub *Subscriber, msg Message) {
if sub.Username == "" { if sub.Username == "" {

View File

@ -57,6 +57,7 @@ const (
ActionUnwatch = "unwatch" // user has closed your video ActionUnwatch = "unwatch" // user has closed your video
ActionFile = "file" // image sharing in chat ActionFile = "file" // image sharing in chat
ActionTakeback = "takeback" // user takes back (deletes) their message for everybody ActionTakeback = "takeback" // user takes back (deletes) their message for everybody
ActionReact = "react" // emoji reaction to a chat message
// Actions sent by server only // Actions sent by server only
ActionPing = "ping" ActionPing = "ping"

View File

@ -105,6 +105,8 @@ func (sub *Subscriber) ReadLoop(s *Server) {
s.OnUnwatch(sub, msg) s.OnUnwatch(sub, msg)
case ActionTakeback: case ActionTakeback:
s.OnTakeback(sub, msg) s.OnTakeback(sub, msg)
case ActionReact:
s.OnReact(sub, msg)
default: default:
sub.ChatServer("Unsupported message type.") sub.ChatServer("Unsupported message type.")
} }

View File

@ -274,3 +274,17 @@ div.feed.popped-out {
.cursor-notallowed { .cursor-notallowed {
cursor: not-allowed; 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;
}

View File

@ -62,7 +62,18 @@ const app = Vue.createApp({
ready: false, ready: false,
audioContext: null, audioContext: null,
audioTracks: {}, audioTracks: {},
} },
reactions: [
'❤️',
'😂',
// '😉',
// '😢',
// '😡',
'🔥',
// '😈',
'🍑',
'🍆',
]
}, },
// User JWT settings if available. // User JWT settings if available.
@ -166,6 +177,14 @@ const app = Vue.createApp({
autoscroll: true, // scroll to bottom on new messages autoscroll: true, // scroll to bottom on new messages
fontSizeClass: "", // font size magnification fontSizeClass: "", // font size magnification
DMs: {}, DMs: {},
messageReactions: {
// Will look like:
// "123": { (message ID)
// "❤️": [ (reaction emoji)
// "username" // users who reacted
// ]
// }
},
// Responsive CSS controls for mobile. // Responsive CSS controls for mobile.
responsive: { responsive: {
@ -390,6 +409,35 @@ const app = Vue.createApp({
// TODO // 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 // Sync the current user state (such as video broadcasting status) to
// the backend, which will reload everybody's Who List. // the backend, which will reload everybody's Who List.
sendMe() { sendMe() {
@ -641,6 +689,9 @@ const app = Vue.createApp({
case "takeback": case "takeback":
this.onTakeback(msg); this.onTakeback(msg);
break; break;
case "react":
this.onReact(msg);
break;
case "presence": case "presence":
this.onPresence(msg); this.onPresence(msg);
break; 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() { activeChannels() {
// List of current channels, unread indicators etc. // List of current channels, unread indicators etc.
let result = []; let result = [];

View File

@ -777,7 +777,7 @@
</div> </div>
<!-- Normal chat message: full size card w/ avatar --> <!-- Normal chat message: full size card w/ avatar -->
<div v-else class="box mb-2 px-4 pt-3 pb-1"> <div v-else class="box mb-2 px-4 pt-3 pb-1 position-relative">
<div class="media mb-0"> <div class="media mb-0">
<div class="media-left"> <div class="media-left">
<a :href="profileURLForUsername(msg.username)" @click.prevent="openProfile({username: msg.username})" <a :href="profileURLForUsername(msg.username)" @click.prevent="openProfile({username: msg.username})"
@ -882,10 +882,39 @@
</div> </div>
</div> </div>
<!-- Emoji reactions menu -->
<div v-if="msg.msgID" class="dropdown is-up is-right emoji-button" onclick="this.classList.toggle('is-active')">
<div class="dropdown-trigger">
<button class="button is-small px-2" aria-haspopup="true" :aria-controls="`react-menu-${msg.msgID}`">
<span>
<i class="fa fa-heart has-text-grey"></i>
<i class="fa fa-plus has-text-grey pl-1"></i>
</span>
</button>
</div>
<div class="dropdown-menu" :id="`react-menu-${msg.msgID}`" role="menu">
<div class="dropdown-content">
<a v-for="i in config.reactions" href="#" class="dropdown-item" @click.prevent="sendReact(msg, i)">
[[i]]
</a>
</div>
</div>
</div>
<!-- Message box --> <!-- Message box -->
<div class="content pl-5 py-3 mb-0"> <div class="content pl-5 py-3 mb-0">
<em v-if="msg.action === 'presence'">[[msg.message]]</em> <em v-if="msg.action === 'presence'">[[msg.message]]</em>
<div v-else v-html="msg.message"></div> <div v-else v-html="msg.message"></div>
<!-- Reactions so far? -->
<div v-if="hasReactions(msg)" class="mt-1">
<span v-for="(users, emoji) in getReactions(msg)"
class="tag is-secondary mr-1"
:title="emoji + ' by: ' + users.join(', ')"
onclick="window.alert(this.title)">
[[emoji]] <small class="ml-1">[[users.length]]</small>
</span>
</div>
</div> </div>
</div> </div>