added mentioning

This commit is contained in:
supertiger1234 2020-02-12 21:02:35 +00:00
parent 5c76f628d4
commit c1509654d8
17 changed files with 450 additions and 74 deletions

View file

@ -1,7 +1,7 @@
<template>
<div class="emoji-suggetions-list">
<div
v-for="(emoji, index) in $props.emojiArray.slice(0, 10)"
v-for="(emoji, index) in $props.emojiArray"
:key="emoji.hexcode || emoji.emojiID"
:class="{ emojiItem: true, selected: index === emojiIndex }"
@mouseenter="hoverEvent"
@ -83,6 +83,7 @@ export default {
<style scoped>
.selected {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.emoji-suggetions-list {
position: absolute;
@ -92,9 +93,10 @@ export default {
overflow-y: auto;
z-index: 2;
background: rgba(0, 0, 0, 0.7);
box-shadow: 0px 0px 20px 5px #000000bd;
backdrop-filter: blur(5px);
border-radius: 4px;
color: white;
color: rgba(255, 255, 255, 0.7);
transition: 0.3s;
user-select: none;
cursor: default;

View file

@ -36,7 +36,8 @@
class="item material-icons"
:class="{
selected: currentTab == 2,
notifyAnimation: serverNotification
notifyAnimation: serverNotification.notification,
mentioned: serverNotification.mentioned
}"
@click="switchTab(2)"
@mouseenter="localToolTipEvent('Servers', $event)"
@ -197,7 +198,7 @@ export default {
serverNotification() {
const notifications = this.$store.getters.notifications;
const channels = this.$store.getters.channels;
const notification = notifications.find(e => {
const notificationsFiltered = notifications.filter(e => {
return (
channels[e.channelID] &&
channels[e.channelID].server_id &&
@ -206,7 +207,11 @@ export default {
this.currentTab !== 2)
);
});
return notification;
const mentioned = notifications.find(m => m.mentioned);
return {
notification: !!notificationsFiltered.length,
mentioned: !!mentioned
};
},
DMNotification() {
const notifications = this.$store.getters.notifications;
@ -307,7 +312,12 @@ export default {
background: #ee3e34;
flex-shrink: 0;
}
.mentioned:before {
content: "@";
margin-bottom: 10px;
font-size: 13px;
background: #ff6947;
}
.tool-tip {
color: white;
position: absolute;
@ -325,4 +335,5 @@ export default {
cursor: default;
transition: 0.2s;
}
</style>

View file

@ -36,6 +36,7 @@
:timeEdited="msg.timeEdited"
:color="msg.color"
:isServer="isServer"
:mentions="msg.mentions"
/>
<uploadsQueue v-if="uploadQueue !== undefined" :queue="uploadQueue" />

View file

@ -55,6 +55,7 @@
</div>
</transition>
<emoji-suggestions v-if="emojiArray" :emojiArray="emojiArray" />
<mentions-popout v-if="mentionsArray" :list="mentionsArray" />
<emoji-panel v-if="showEmojiPanel" @close="showEmojiPanel = false" />
</div>
@ -192,6 +193,7 @@ import { bus } from "../../main";
import Spinner from "@/components/Spinner.vue";
import heading from "@/components/app/MessagePanel/Heading.vue";
import emojiSuggestions from "@/components/app/EmojiPanels/emojiSuggestions.vue";
import mentionsPopout from "@/components/app/mentionsPopout.vue";
import MessageLogs from "@/components/app/MessageLogs.vue";
import emojiParser from "@/utils/emojiParser.js";
import windowProperties from "@/utils/windowProperties";
@ -211,7 +213,8 @@ export default {
heading,
EditPanel,
MessageLogs,
TypingStatus
TypingStatus,
mentionsPopout
},
data() {
return {
@ -283,6 +286,19 @@ export default {
return ("" + number).substring(add);
},
replaceMentions(message) {
const regex = /@([\w\d\s_-]+):([\w\d_-]+)/g;
return message.replace(regex, word => {
const [username, tag] = word.split(":");
if (tag.length !== 4 || !username || !tag) return word;
const member = Object.values(this.members).find(
m => "@" + m.username === username && m.tag === tag
);
if (!member) return word;
return `<@${member.uniqueID}>`;
});
},
async sendMessage() {
this.$refs["input-box"].focus();
this.message = this.message.trim();
@ -293,7 +309,8 @@ export default {
clearInterval(this.postTimerID);
this.postTimerID = null;
const msg = emojiParser.replaceShortcode(this.message);
let msg = emojiParser.replaceShortcode(this.message);
msg = this.replaceMentions(msg);
const tempID = this.generateNum(25);
@ -409,6 +426,19 @@ export default {
}
bus.$emit("scrollDown");
},
mentionsSwitchKey(event) {
if (!this.mentionsArray) return;
if (event.keyCode === 38) {
bus.$emit("mentions:key", "up");
event.preventDefault();
return;
}
if (event.keyCode === 40) {
bus.$emit("mentions:key", "down");
event.preventDefault();
return;
}
},
emojiSwitchKey(event) {
if (!this.emojiArray) return;
@ -438,10 +468,10 @@ export default {
},
showEmojiPopout(event) {
if (event.keyCode == 38 || event.keyCode == 40) return; // up/down
const message = this.$refs["input-box"].value;
const cursorPosition = event.target.selectionStart;
const cursorWord = this.ReturnWord(this.message, cursorPosition);
const cursorLetter = this.message.substring(
const cursorWord = this.ReturnWord(message, cursorPosition);
const cursorLetter = message.substring(
cursorPosition - 1,
cursorPosition
);
@ -452,11 +482,49 @@ export default {
if (!cursorWord.startsWith(":") || cursorWord.length <= 2)
return this.$store.dispatch("setEmojiArray", null);
const searchArr = emojiParser.searchEmoji(cursorWord.slice(1, -1));
const searchArr = emojiParser.searchEmoji(
cursorWord.slice(1, cursorWord.length)
);
if (searchArr.length <= 0)
return this.$store.dispatch("setEmojiArray", null);
this.$store.dispatch("setEmojiArray", searchArr);
this.$store.dispatch("setEmojiArray", searchArr.slice(0, 10));
},
showMentionsPopout(event) {
if (event.keyCode == 38 || event.keyCode == 40) return; // up/down
const message = this.$refs["input-box"].value;
const cursorPosition = event.target.selectionStart;
const cursorWord = this.ReturnWord(message, cursorPosition);
if (!cursorWord.startsWith("@")) {
this.$store.dispatch("mentionsListModule/setMentionsArray", null);
return;
}
// word without @
const wordWithoutBegining = cursorWord
.slice(1, cursorWord.length)
.toLowerCase();
let searchedMembers = [];
for (let index = 0; index < this.serverMembers.length; index++) {
const serverMember = this.serverMembers[index];
if (serverMember.server_id != this.server.server_id) continue;
const member = this.members[serverMember.uniqueID];
if (
member.username
.toLowerCase()
.replace(/\s/g, "")
.includes(wordWithoutBegining)
) {
searchedMembers.push(member);
}
}
searchedMembers = searchedMembers.slice(0, 9);
this.$store.dispatch(
"mentionsListModule/setMentionsArray",
searchedMembers
);
},
async onInput(event) {
const value = event.target.value.trim();
@ -466,7 +534,24 @@ export default {
}
},
keyUp(event) {
this.showEmojiPopout(event);
setTimeout(() => {
this.showMentionsPopout(event);
this.showEmojiPopout(event);
}, 10);
},
enterMention() {
const member = this.mentionsArray[this.mentionsListIndex];
if (!member) return;
this.$store.dispatch("mentionsListModule/setMentionsArray", null);
const cursorPosition = this.$refs["input-box"].selectionStart;
const cursorWord = this.ReturnWord(this.message, cursorPosition);
const start = cursorPosition - cursorWord.length;
const end = cursorPosition;
this.message =
this.message.substring(0, start) +
`@${member.username}:${member.tag} ` +
this.message.substring(end);
},
enterEmojiSuggestion() {
const emoji = this.emojiArray[this.emojiIndex];
@ -505,6 +590,7 @@ export default {
this.$store.dispatch("settingsModule/addRecentEmoji", shortcode);
},
keyDown(event) {
this.mentionsSwitchKey(event);
this.emojiSwitchKey(event);
// when enter is press
if (event.keyCode == 13) {
@ -518,6 +604,13 @@ export default {
this.enterEmojiSuggestion();
return;
}
if (
this.mentionsArray &&
this.mentionsArray[this.mentionsListIndex]
) {
this.enterMention();
return;
}
if (this.editMessage) {
return this.updateMessage();
} else {
@ -662,6 +755,7 @@ export default {
bus.$on("newMessage", this.hideTypingStatus);
bus.$on("emojiSuggestions:Selected", this.enterEmojiSuggestion);
bus.$on("mentions:Selected", this.enterMention);
bus.$on("emojiPanel:Selected", this.enterEmojiPanel);
bus.$on("scrolledDown", scrolledDown => {
@ -678,7 +772,8 @@ export default {
bus.$off("newMessage", this.hideTypingStatus);
bus.$off("emojiSuggestions:Selected", this.enterEmojiSuggestion);
bus.$on("emojiPanel:Selected", this.enterEmojiPanel);
bus.$off("emojiPanel:Selected", this.enterEmojiPanel);
bus.$off("mentions:Selected", this.enterMention);
window.removeEventListener("focus", this.onFocus);
window.removeEventListener("blur", this.onBlur);
@ -722,6 +817,14 @@ export default {
undefined
);
},
members() {
const members = this.$store.getters["members/members"];
return members;
},
serverMembers() {
const serverMembers = this.$store.getters["servers/serverMembers"];
return serverMembers.reverse();
},
serverMember() {
return this.$store.getters["servers/serverMembers"].find(
sm =>
@ -783,9 +886,15 @@ export default {
emojiArray() {
return this.$store.getters.emojiArray;
},
mentionsArray() {
return this.$store.getters["mentionsListModule/mentionsArray"];
},
emojiIndex() {
return this.$store.getters.getEmojiIndex;
},
mentionsListIndex() {
return this.$store.getters["mentionsListModule/getMentionIndex"];
},
recipients() {
const selectedChannel = this.$store.getters.selectedChannelID;
const channel = this.$store.getters.channels[selectedChannel];

View file

@ -37,6 +37,13 @@
{{ this.$props.username }}
</div>
<div class="date">{{ getDate }}</div>
<div
class="mentioned material-icons"
v-if="mentioned"
title="You were mentioned"
>
alternate_email
</div>
</div>
<SimpleMarkdown
class="content-message"
@ -116,8 +123,8 @@
<span class="username" @click="openUserInformation">{{
this.$props.username
}}</span>
<span v-if="type === 1" class="text">has joined the server!</span>
<span v-if="type === 2" class="text">has left the server.</span>
<span v-if="type === 1" class="text">joined the server!</span>
<span v-if="type === 2" class="text">left the server.</span>
<span v-if="type === 3" class="text">has been kicked.</span>
<span v-if="type === 4" class="text">has been banned.</span>
<span class="date">{{ getDate }}</span>
@ -161,7 +168,8 @@ export default {
"channelID",
"timeEdited",
"color",
"isServer"
"isServer",
"mentions"
],
components: {
ProfilePicture,
@ -171,7 +179,8 @@ export default {
data() {
return {
hover: false,
isGif: false
isGif: false,
mentioned: false
};
},
methods: {
@ -263,14 +272,28 @@ export default {
},
onResize() {
this.imageSize();
},
checkMentioned() {
if (
this.mentions &&
this.mentions.find(u => u.uniqueID === this.user.uniqueID)
) {
this.mentioned = true;
} else {
this.mentioned = false;
}
}
},
watch: {
getWindowWidth(dimentions) {
this.onResize(dimentions);
},
mentions() {
this.checkMentioned();
}
},
mounted() {
this.checkMentioned();
this.isGif = this.userAvatar.endsWith(".gif");
const files = this.files;
if (!files || files.length === 0 || !files[0].dimensions) return undefined;
@ -603,7 +626,24 @@ $message-color: rgba(0, 0, 0, 0.3);
cursor: pointer;
margin-top: 5px;
}
.mentioned {
margin-left: 5px;
margin-right: 5px;
flex-shrink: 0;
color: white;
background: rgba(255, 59, 59, 0.9);
font-size: 13px;
display: flex;
align-content: center;
align-items: center;
justify-content: center;
margin-top: -2px;
padding: 2px;
height: 15px;
width: 15px;
border-radius: 50%;
cursor: default;
}
@media (max-width: 468px) {
}
</style>

View file

@ -194,48 +194,8 @@ export default {
},
selectedServerID() {
return this.$store.getters["servers/selectedServerID"];
},
serverNotification() {
const notifications = this.$store.getters.notifications;
const channels = this.$store.getters.channels;
const notification = notifications.find(e => {
return (
channels[e.channelID] &&
channels[e.channelID].server_id &&
(e.channelID !== this.$store.getters.selectedChannelID ||
!document.hasFocus() ||
this.currentTab !== 2)
);
});
return notification;
},
DMNotification() {
const notifications = this.$store.getters.notifications;
const channels = this.$store.getters.channels;
const notification = notifications.find(e => {
return (
channels[e.channelID] &&
!channels[e.channelID].server_id &&
(e.channelID !== this.$store.getters.selectedChannelID ||
!document.hasFocus() ||
this.currentTab !== 1)
);
});
// unopened dm
if (!notification) {
return notifications.find(e => {
return !channels[e.channelID];
});
}
return notification;
},
friendRequestExists() {
const allFriend = this.$store.getters.user.friends;
const result = Object.keys(allFriend).map(function(key) {
return allFriend[key];
});
return result.find(friend => friend.status === 1);
}
},
mounted() {
bus.$on("server-tool-tip", this.serverToolTipEvent);

View file

@ -4,7 +4,8 @@
:data-servername="serverData.name"
:class="{
selected: selectedServerID === serverData.server_id,
notifyAnimation: notification
notifyAnimation: notification.notification,
mentioned: notification.mentioned
}"
@contextmenu.prevent="contextEvent"
@mouseenter="hoverEvent"
@ -47,7 +48,7 @@ export default {
const notifications = this.$store.getters.notifications;
const channels = this.$store.getters.channels;
const notification = notifications.find(e => {
const filteredNotifications = notifications.filter(e => {
return (
channels[e.channelID] &&
channels[e.channelID].server_id &&
@ -56,7 +57,11 @@ export default {
(this.selectedChannelID !== e.channelID || !document.hasFocus())
);
});
return notification;
const mentioned = filteredNotifications.find(n => n.mentioned);
return {
mentioned: !!mentioned,
notification: !!filteredNotifications.length
};
}
},
methods: {
@ -125,7 +130,6 @@ export default {
flex-direction: column;
align-items: center;
align-content: center;
justify-content: center;
font-size: 15px;
position: absolute;
z-index: 115651;
@ -136,4 +140,10 @@ export default {
border-radius: 50%;
background: #ee3e34;
}
.mentioned:after {
content: "@";
margin-bottom: 10px;
font-size: 13px;
background: #ff6947;
}
</style>

View file

@ -1,5 +1,10 @@
<template>
<div class="formatted-content" v-html="markdown"></div>
<div
class="formatted-content"
ref="content"
@click="textClicked"
v-html="markdown"
></div>
</template>
<script>
@ -9,6 +14,14 @@ export default {
props: {
message: String
},
methods: {
textClicked(event) {
if (event.target.classList[0] === "mention") {
const id = event.target.id.split("-")[1];
this.$store.dispatch("setUserInformationPopout", id);
}
}
},
computed: {
markdown: function() {
return messageFormatter(this.message);
@ -47,4 +60,16 @@ pre {
.link {
color: #68aaff;
}
.mention {
background: rgba(0, 0, 0, 0.3);
color: rgb(94, 164, 255);
font-weight: bold;
border-radius: 7px;
padding: 3px;
cursor: pointer;
transition: 0.2s;
}
.mention:hover {
background: rgba(0, 0, 0, 0.6);
}
</style>

View file

@ -0,0 +1,118 @@
<template>
<div class="list">
<div
v-for="(member, index) in list"
:key="member.uniqueID"
:class="{ item: true, selected: index === mentionIndex }"
@mouseenter="hoverEvent"
@click="clickEvent"
>
<img class="avatar" :src="avatarPath + member.avatar + '?type=webp'" />
<div class="username">{{ member.username }}</div>
<div class="tag">:{{ member.tag }}</div>
</div>
</div>
</template>
<script>
import { bus } from "@/main";
import config from "@/config.js";
export default {
props: ["list"],
data() {
return {
avatarPath: config.domain + "/avatars/"
};
},
computed: {
mentionIndex() {
return this.$store.getters["mentionsListModule/getMentionIndex"];
}
},
watch: {
list() {
this.changeIndex(0);
}
},
mounted() {
bus.$on("mentions:key", this.KeySwitch);
},
destroyed() {
bus.$off("mentions:key", this.KeySwitch);
this.$store.dispatch("mentionsListModule/setMentionsArray", null);
},
methods: {
changeIndex(index) {
this.$store.dispatch("mentionsListModule/changeIndex", index);
},
hoverEvent(event) {
const mention = event.target.closest(".item");
const parent = event.target.parentElement.children;
if (!mention) return;
const index = [...parent].findIndex(el => el === mention);
if (index >= 0) this.changeIndex(index);
},
KeySwitch(key) {
if (key == "up") {
if (this.mentionIndex == 0)
return this.changeIndex(this.$props.list.length - 1);
this.changeIndex(this.mentionIndex - 1);
}
if (key == "down") {
if (this.mentionIndex == this.$props.list.length - 1)
return this.changeIndex(0);
this.changeIndex(this.mentionIndex + 1);
}
},
clickEvent() {
bus.$emit("mentions:Selected");
}
}
};
</script>
<style scoped>
.selected {
color: white;
background: rgba(255, 255, 255, 0.2);
}
.list {
position: absolute;
bottom: -35px;
left: 50px;
max-height: 400px;
overflow-y: auto;
z-index: 2;
background: rgba(0, 0, 0, 0.7);
box-shadow: 0px 0px 20px 5px #000000bd;
backdrop-filter: blur(5px);
border-radius: 4px;
color: rgba(255, 255, 255, 0.7);
transition: 0.3s;
user-select: none;
cursor: default;
}
.avatar {
width: 25px;
height: 25px;
margin-right: 5px;
border-radius: 50%;
}
.item {
display: flex;
padding: 5px;
align-content: center;
align-items: center;
cursor: pointer;
}
.tag {
color: rgba(255, 255, 255, 0.3);
}
@media (max-height: 441px) {
.list {
max-height: 150px;
}
}
</style>

View file

@ -9,6 +9,7 @@ import settingsModule from "./modules/settingsModule";
import uploadFilesModule from "./modules/uploadFilesModule";
import popoutsModule from "./modules/popoutsModule/popoutsModule.js";
import emojiSuggestionModule from "./modules/emojiSuggestionModule";
import mentionsListModule from "./modules/mentionsListModule";
import serversModule from "./modules/serversModule";
import membersModule from "./modules/membersModule";
Vue.use(Vuex);
@ -24,6 +25,7 @@ export const store = new Vuex.Store({
uploadFilesModule,
popoutsModule,
emojiSuggestionModule,
mentionsListModule,
servers: serversModule,
members: membersModule
},

View file

@ -0,0 +1,41 @@
import Vue from "vue";
const state = {
array: null,
index: 0
};
const getters = {
mentionsArray(state) {
return state.array;
},
getMentionIndex(state) {
return state.index;
}
};
const actions = {
setMentionsArray(context, array) {
context.commit("setMentionsArray", array);
},
changeIndex(context, index) {
context.commit("changeIndex", index);
}
};
const mutations = {
setMentionsArray(state, array) {
Vue.set(state, "array", array);
},
changeIndex(state, index) {
Vue.set(state, "index", index);
}
};
export default {
namespaced: true,
state,
getters,
actions,
mutations
};

View file

@ -16,7 +16,7 @@ const actions = {
context.commit("addAllNotifications", notifications);
},
messageCreatedNotification(context, notification) {
const { channelID, lastMessageID, sender } = notification;
const { channelID, lastMessageID, sender, mentioned } = notification;
const currentTab = context.rootGetters.currentTab;
// dont display a notification if the channel is selected.
@ -33,12 +33,12 @@ const actions = {
if (find) {
return context.commit("messageCreatedNotification", {
exists: true,
notification: { channelID, lastMessageID, sender }
notification: { channelID, lastMessageID, sender, mentioned }
});
}
context.commit("messageCreatedNotification", {
exists: false,
notification: { channelID, lastMessageID, sender, count: 1 }
notification: { channelID, lastMessageID, sender, count: 1, mentioned }
});
},
dismissNotification(context, channelID) {
@ -66,6 +66,13 @@ const mutations = {
if (state.notifications[i].channelID === notification.channelID) {
const count = state.notifications[i].count;
Vue.set(state.notifications[i], "count", count + 1);
if (!state.notifications[i].mentioned) {
Vue.set(
state.notifications[i],
"mentioned",
notification.mentioned
);
}
Vue.set(
state.notifications[i],
"lastMessageID",

View file

@ -217,10 +217,14 @@ const actions = {
);
desktopNotification();
}
const notification = {
channelID: data.message.channelID,
lastMessageID: data.message.messageID,
sender: data.message.creator
sender: data.message.creator,
mentioned: !!data.message.mentions.find(
m => m.uniqueID === context.rootState.user.user.uniqueID
)
};
context.dispatch("messageCreatedNotification", notification);
function desktopNotification() {
@ -496,9 +500,16 @@ const actions = {
context.dispatch("servers/addMemberRole", { role_id, uniqueID, server_id });
},
// eslint-disable-next-line prettier/prettier
["socket_serverMember:removeRole"](context, { role_id, uniqueID, server_id }) {
["socket_serverMember:removeRole"](
context,
{ role_id, uniqueID, server_id }
) {
// eslint-disable-next-line prettier/prettier
context.dispatch("servers/removeMemberRole", { role_id, uniqueID, server_id });
context.dispatch("servers/removeMemberRole", {
role_id,
uniqueID,
server_id
});
},
["socket_server:updateRoles"](context, { roles }) {
// eslint-disable-next-line prettier/prettier

View file

@ -1,4 +1,13 @@
const config = [
{
version: 9.8,
title: "Wake up @Fishie!",
shortTitle: "",
date: "12/02/2020",
new: [
"Is someone not responding to you? Wake them up by mentioning them by typing '@' followed by their name in the message input box."
],
},
{
version: 9.7,
title: "You have 99 mails!",

View file

@ -54,10 +54,12 @@ export default {
const customEmojis = store.state["settingsModule"].customEmojis;
return [
...matchSorter(customEmojis, shortCode, {
keys: ["name"]
keys: ["name"],
threshold: matchSorter.rankings.CONTAINS
}),
...matchSorter(emojis, shortCode, {
keys: ["shortcodes"]
keys: ["shortcodes"],
threshold: matchSorter.rankings.CONTAINS
})
];
},

View file

@ -0,0 +1,26 @@
import * as SimpleMarkdown from "simple-markdown";
import { store } from "@/store/index";
export default order => {
return {
order: order++,
match: function(source) {
return /^<@([\d]+)>/.exec(source);
},
parse: function(capture) {
return {
id: capture[1],
orig: capture[0]
};
},
html: function(node) {
const member = store.getters["members/members"][node.id];
if (!member) return node.orig;
return SimpleMarkdown.htmlTag("span", "@" + member.username, {
class: "mention",
id: "mention-" + member.uniqueID
});
}
};
};

View file

@ -8,6 +8,7 @@ import strikeout from "./markdown-rules/strikeout";
import inlineCodeblock from "./markdown-rules/inlineCodeblock";
import link from "./markdown-rules/link";
import customEmoji from "./markdown-rules/customEmoji";
import mentions from "./markdown-rules/mentions";
let order = 0; // order the below rules as declared below rather than by the original defaultRules order:
@ -17,6 +18,7 @@ const rules = {
underline: underline(order++),
strikeout: strikeout(order++),
link: link(order++),
mentions: mentions(order++),
customEmoji: customEmoji(order++),
strong: Object.assign({}, SimpleMarkdown.defaultRules.strong, {