Emoji reactions

ipad-testing
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.
func (s *Server) OnFile(sub *Subscriber, msg Message) {
if sub.Username == "" {

View File

@ -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"

View File

@ -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.")
}

View File

@ -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;
}

View File

@ -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 = [];

View File

@ -777,7 +777,7 @@
</div>
<!-- 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-left">
<a :href="profileURLForUsername(msg.username)" @click.prevent="openProfile({username: msg.username})"
@ -882,10 +882,39 @@
</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 -->
<div class="content pl-5 py-3 mb-0">
<em v-if="msg.action === 'presence'">[[msg.message]]</em>
<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>