Merge pull request #3858 from thelounge/xpaw/mentions
Track mentions and add a window to view them
This commit is contained in:
commit
999095b7df
@ -3,6 +3,7 @@
|
||||
<Sidebar v-if="$store.state.appLoaded" :overlay="$refs.overlay" />
|
||||
<div id="sidebar-overlay" ref="overlay" @click="$store.commit('sidebarOpen', false)" />
|
||||
<router-view ref="window"></router-view>
|
||||
<Mentions />
|
||||
<ImageViewer ref="imageViewer" />
|
||||
<ContextMenu ref="contextMenu" />
|
||||
<ConfirmDialog ref="confirmDialog" />
|
||||
@ -21,6 +22,7 @@ import Sidebar from "./Sidebar.vue";
|
||||
import ImageViewer from "./ImageViewer.vue";
|
||||
import ContextMenu from "./ContextMenu.vue";
|
||||
import ConfirmDialog from "./ConfirmDialog.vue";
|
||||
import Mentions from "./Mentions.vue";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
@ -29,6 +31,7 @@ export default {
|
||||
ImageViewer,
|
||||
ContextMenu,
|
||||
ConfirmDialog,
|
||||
Mentions,
|
||||
},
|
||||
computed: {
|
||||
viewportClasses() {
|
||||
|
@ -38,6 +38,11 @@
|
||||
:network="network"
|
||||
:text="channel.topic"
|
||||
/></span>
|
||||
<button
|
||||
class="mentions"
|
||||
aria-label="Open your mentions"
|
||||
@click="openMentions"
|
||||
/>
|
||||
<button
|
||||
class="menu"
|
||||
aria-label="Open the context menu"
|
||||
@ -198,6 +203,11 @@ export default {
|
||||
network: this.network,
|
||||
});
|
||||
},
|
||||
openMentions() {
|
||||
this.$root.$emit("mentions:toggle", {
|
||||
event: event,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
173
client/components/Mentions.vue
Normal file
173
client/components/Mentions.vue
Normal file
@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
id="mentions-popup-container"
|
||||
@click="containerClick"
|
||||
@contextmenu.prevent="containerClick"
|
||||
>
|
||||
<div class="mentions-popup">
|
||||
<div class="mentions-popup-title">
|
||||
Recent mentions
|
||||
<span v-if="isLoading">- Loading…</span>
|
||||
</div>
|
||||
<template v-if="resolvedMessages.length === 0">
|
||||
<p>There are no recent mentions.</p>
|
||||
</template>
|
||||
<template v-for="message in resolvedMessages" v-else>
|
||||
<div :key="message.id" :class="['msg', message.type]">
|
||||
<span class="from">
|
||||
<Username :user="message.from" />
|
||||
<template v-if="message.channel">
|
||||
in {{ message.channel.channel.name }} on
|
||||
{{ message.channel.network.name }}
|
||||
</template>
|
||||
<template v-else>
|
||||
in unknown channel
|
||||
</template>
|
||||
</span>
|
||||
<span :title="message.time | localetime" class="time">
|
||||
{{ messageTime(message.time) }}
|
||||
</span>
|
||||
<button
|
||||
class="msg-hide"
|
||||
aria-label="Hide this mention"
|
||||
@click="hideMention(message)"
|
||||
></button>
|
||||
<div class="content" dir="auto">
|
||||
<ParsedMessage :network="null" :message="message" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.mentions-popup {
|
||||
background-color: var(--window-bg-color);
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
right: 80px;
|
||||
top: 55px;
|
||||
max-height: 400px;
|
||||
overflow-y: scroll;
|
||||
z-index: 2;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mentions-popup > .mentions-popup-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.mentions-popup .msg {
|
||||
margin-bottom: 15px;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.mentions-popup .msg:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mentions-popup .msg .content {
|
||||
background-color: var(--highlight-bg-color);
|
||||
border-radius: 5px;
|
||||
padding: 6px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.mentions-popup .msg-hide::before {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
content: "×";
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mentions-popup {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
max-height: none;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
top: 45px; /* header height */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Username from "./Username.vue";
|
||||
import ParsedMessage from "./ParsedMessage.vue";
|
||||
import socket from "../js/socket";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export default {
|
||||
name: "Mentions",
|
||||
components: {
|
||||
Username,
|
||||
ParsedMessage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
resolvedMessages() {
|
||||
const messages = this.$store.state.mentions.slice().reverse();
|
||||
|
||||
for (const message of messages) {
|
||||
message.channel = this.$store.getters.findChannel(message.chanId);
|
||||
}
|
||||
|
||||
return messages;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"$store.state.mentions"() {
|
||||
this.isLoading = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on("mentions:toggle", this.openPopup);
|
||||
},
|
||||
destroyed() {
|
||||
this.$root.$off("mentions:toggle", this.openPopup);
|
||||
},
|
||||
methods: {
|
||||
messageTime(time) {
|
||||
return dayjs(time).fromNow();
|
||||
},
|
||||
hideMention(message) {
|
||||
this.$store.state.mentions.splice(
|
||||
this.$store.state.mentions.findIndex((m) => m.msgId === message.msgId),
|
||||
1
|
||||
);
|
||||
|
||||
socket.emit("mentions:hide", message.msgId);
|
||||
},
|
||||
containerClick(event) {
|
||||
if (event.currentTarget === event.target) {
|
||||
this.isOpen = false;
|
||||
}
|
||||
},
|
||||
openPopup() {
|
||||
this.isOpen = !this.isOpen;
|
||||
|
||||
if (this.isOpen) {
|
||||
this.isLoading = true;
|
||||
socket.emit("mentions:get");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -283,6 +283,7 @@ p {
|
||||
|
||||
#viewport .lt::before,
|
||||
#viewport .rt::before,
|
||||
#chat button.mentions::before,
|
||||
#chat button.menu::before,
|
||||
.channel-list-item::before,
|
||||
#footer .icon,
|
||||
@ -336,6 +337,7 @@ p {
|
||||
#viewport .lt::before { content: "\f0c9"; /* http://fontawesome.io/icon/bars/ */ }
|
||||
#viewport .rt::before { content: "\f0c0"; /* https://fontawesome.com/icons/users?style=solid */ }
|
||||
#chat button.menu::before { content: "\f142"; /* http://fontawesome.io/icon/ellipsis-v/ */ }
|
||||
#chat button.mentions::before { content: "\f1fa"; /* https://fontawesome.com/icons/at?style=solid */ }
|
||||
|
||||
.context-menu-join::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ }
|
||||
.context-menu-user::before { content: "\f007"; /* http://fontawesome.io/icon/user/ */ }
|
||||
@ -547,6 +549,7 @@ p {
|
||||
|
||||
#viewport .lt,
|
||||
#viewport .rt,
|
||||
#chat button.mentions,
|
||||
#chat button.menu {
|
||||
color: #607992;
|
||||
display: flex;
|
||||
@ -560,6 +563,7 @@ p {
|
||||
|
||||
#viewport .lt::before,
|
||||
#viewport .rt::before,
|
||||
#chat button.mentions::before,
|
||||
#chat button.menu::before {
|
||||
width: 36px;
|
||||
line-height: 36px; /* Fix alignment in Microsoft Edge */
|
||||
@ -2166,6 +2170,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#mentions-popup-container,
|
||||
#context-menu-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -2176,6 +2181,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.mentions-popup,
|
||||
#context-menu,
|
||||
.textcomplete-menu {
|
||||
position: absolute;
|
||||
|
@ -24,3 +24,4 @@ import "./configuration";
|
||||
import "./changelog";
|
||||
import "./setting";
|
||||
import "./history_clear";
|
||||
import "./mentions";
|
||||
|
8
client/js/socket-events/mentions.js
Normal file
8
client/js/socket-events/mentions.js
Normal file
@ -0,0 +1,8 @@
|
||||
"use strict";
|
||||
|
||||
import socket from "../socket";
|
||||
import store from "../store";
|
||||
|
||||
socket.on("mentions:list", function (data) {
|
||||
store.commit("mentions", data);
|
||||
});
|
@ -26,6 +26,7 @@ const store = new Vuex.Store({
|
||||
isAutoCompleting: false,
|
||||
isConnected: false,
|
||||
networks: [],
|
||||
mentions: [],
|
||||
hasServiceWorker: false,
|
||||
pushNotificationState: "unsupported",
|
||||
serverConfiguration: null,
|
||||
@ -60,6 +61,9 @@ const store = new Vuex.Store({
|
||||
networks(state, networks) {
|
||||
state.networks = networks;
|
||||
},
|
||||
mentions(state, mentions) {
|
||||
state.mentions = mentions;
|
||||
},
|
||||
removeNetwork(state, networkId) {
|
||||
state.networks.splice(
|
||||
store.state.networks.findIndex((n) => n.uuid === networkId),
|
||||
|
@ -112,6 +112,7 @@ body {
|
||||
|
||||
#viewport .lt,
|
||||
#viewport .rt,
|
||||
#chat button.mentions,
|
||||
#chat button.menu,
|
||||
#form #submit {
|
||||
color: #b7c5d1;
|
||||
|
@ -56,6 +56,7 @@ function Client(manager, name, config = {}) {
|
||||
idMsg: 1,
|
||||
name: name,
|
||||
networks: [],
|
||||
mentions: [],
|
||||
manager: manager,
|
||||
messageStorage: [],
|
||||
highlightRegex: null,
|
||||
|
@ -179,5 +179,21 @@ module.exports = function (irc, network) {
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// Keep track of all mentions in channels for this client
|
||||
if (msg.highlight && chan.type === Chan.Type.CHANNEL) {
|
||||
client.mentions.push({
|
||||
chanId: chan.id,
|
||||
msgId: msg.id,
|
||||
type: msg.type,
|
||||
time: msg.time,
|
||||
text: msg.text,
|
||||
from: msg.from,
|
||||
});
|
||||
|
||||
if (client.mentions.length > 100) {
|
||||
client.mentions.splice(0, client.mentions.length - 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -536,6 +536,22 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("mentions:get", () => {
|
||||
socket.emit("mentions:list", client.mentions);
|
||||
});
|
||||
|
||||
socket.on("mentions:hide", (msgId) => {
|
||||
if (typeof msgId !== "number") {
|
||||
return;
|
||||
}
|
||||
|
||||
client.mentions.splice(
|
||||
client.mentions.findIndex((m) => m.msgId === msgId),
|
||||
1
|
||||
);
|
||||
// TODO: emit to other clients?
|
||||
});
|
||||
|
||||
if (!Helper.config.public) {
|
||||
socket.on("push:register", (subscription) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(client.config.sessions, token)) {
|
||||
|
Loading…
Reference in New Issue
Block a user