Emoji reactions
This commit is contained in:
parent
5c2a1d6246
commit
5f4b14ecc4
|
@ -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 == "" {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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 = [];
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user