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" />
|
<Sidebar v-if="$store.state.appLoaded" :overlay="$refs.overlay" />
|
||||||
<div id="sidebar-overlay" ref="overlay" @click="$store.commit('sidebarOpen', false)" />
|
<div id="sidebar-overlay" ref="overlay" @click="$store.commit('sidebarOpen', false)" />
|
||||||
<router-view ref="window"></router-view>
|
<router-view ref="window"></router-view>
|
||||||
|
<Mentions />
|
||||||
<ImageViewer ref="imageViewer" />
|
<ImageViewer ref="imageViewer" />
|
||||||
<ContextMenu ref="contextMenu" />
|
<ContextMenu ref="contextMenu" />
|
||||||
<ConfirmDialog ref="confirmDialog" />
|
<ConfirmDialog ref="confirmDialog" />
|
||||||
@ -21,6 +22,7 @@ import Sidebar from "./Sidebar.vue";
|
|||||||
import ImageViewer from "./ImageViewer.vue";
|
import ImageViewer from "./ImageViewer.vue";
|
||||||
import ContextMenu from "./ContextMenu.vue";
|
import ContextMenu from "./ContextMenu.vue";
|
||||||
import ConfirmDialog from "./ConfirmDialog.vue";
|
import ConfirmDialog from "./ConfirmDialog.vue";
|
||||||
|
import Mentions from "./Mentions.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
@ -29,6 +31,7 @@ export default {
|
|||||||
ImageViewer,
|
ImageViewer,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
Mentions,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
viewportClasses() {
|
viewportClasses() {
|
||||||
|
@ -38,6 +38,11 @@
|
|||||||
:network="network"
|
:network="network"
|
||||||
:text="channel.topic"
|
:text="channel.topic"
|
||||||
/></span>
|
/></span>
|
||||||
|
<button
|
||||||
|
class="mentions"
|
||||||
|
aria-label="Open your mentions"
|
||||||
|
@click="openMentions"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
class="menu"
|
class="menu"
|
||||||
aria-label="Open the context menu"
|
aria-label="Open the context menu"
|
||||||
@ -198,6 +203,11 @@ export default {
|
|||||||
network: this.network,
|
network: this.network,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
openMentions() {
|
||||||
|
this.$root.$emit("mentions:toggle", {
|
||||||
|
event: event,
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</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 .lt::before,
|
||||||
#viewport .rt::before,
|
#viewport .rt::before,
|
||||||
|
#chat button.mentions::before,
|
||||||
#chat button.menu::before,
|
#chat button.menu::before,
|
||||||
.channel-list-item::before,
|
.channel-list-item::before,
|
||||||
#footer .icon,
|
#footer .icon,
|
||||||
@ -336,6 +337,7 @@ p {
|
|||||||
#viewport .lt::before { content: "\f0c9"; /* http://fontawesome.io/icon/bars/ */ }
|
#viewport .lt::before { content: "\f0c9"; /* http://fontawesome.io/icon/bars/ */ }
|
||||||
#viewport .rt::before { content: "\f0c0"; /* https://fontawesome.com/icons/users?style=solid */ }
|
#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.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-join::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ }
|
||||||
.context-menu-user::before { content: "\f007"; /* http://fontawesome.io/icon/user/ */ }
|
.context-menu-user::before { content: "\f007"; /* http://fontawesome.io/icon/user/ */ }
|
||||||
@ -547,6 +549,7 @@ p {
|
|||||||
|
|
||||||
#viewport .lt,
|
#viewport .lt,
|
||||||
#viewport .rt,
|
#viewport .rt,
|
||||||
|
#chat button.mentions,
|
||||||
#chat button.menu {
|
#chat button.menu {
|
||||||
color: #607992;
|
color: #607992;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -560,6 +563,7 @@ p {
|
|||||||
|
|
||||||
#viewport .lt::before,
|
#viewport .lt::before,
|
||||||
#viewport .rt::before,
|
#viewport .rt::before,
|
||||||
|
#chat button.mentions::before,
|
||||||
#chat button.menu::before {
|
#chat button.menu::before {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
line-height: 36px; /* Fix alignment in Microsoft Edge */
|
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;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#mentions-popup-container,
|
||||||
#context-menu-container {
|
#context-menu-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -2176,6 +2181,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mentions-popup,
|
||||||
#context-menu,
|
#context-menu,
|
||||||
.textcomplete-menu {
|
.textcomplete-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -24,3 +24,4 @@ import "./configuration";
|
|||||||
import "./changelog";
|
import "./changelog";
|
||||||
import "./setting";
|
import "./setting";
|
||||||
import "./history_clear";
|
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,
|
isAutoCompleting: false,
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
networks: [],
|
networks: [],
|
||||||
|
mentions: [],
|
||||||
hasServiceWorker: false,
|
hasServiceWorker: false,
|
||||||
pushNotificationState: "unsupported",
|
pushNotificationState: "unsupported",
|
||||||
serverConfiguration: null,
|
serverConfiguration: null,
|
||||||
@ -60,6 +61,9 @@ const store = new Vuex.Store({
|
|||||||
networks(state, networks) {
|
networks(state, networks) {
|
||||||
state.networks = networks;
|
state.networks = networks;
|
||||||
},
|
},
|
||||||
|
mentions(state, mentions) {
|
||||||
|
state.mentions = mentions;
|
||||||
|
},
|
||||||
removeNetwork(state, networkId) {
|
removeNetwork(state, networkId) {
|
||||||
state.networks.splice(
|
state.networks.splice(
|
||||||
store.state.networks.findIndex((n) => n.uuid === networkId),
|
store.state.networks.findIndex((n) => n.uuid === networkId),
|
||||||
|
@ -112,6 +112,7 @@ body {
|
|||||||
|
|
||||||
#viewport .lt,
|
#viewport .lt,
|
||||||
#viewport .rt,
|
#viewport .rt,
|
||||||
|
#chat button.mentions,
|
||||||
#chat button.menu,
|
#chat button.menu,
|
||||||
#form #submit {
|
#form #submit {
|
||||||
color: #b7c5d1;
|
color: #b7c5d1;
|
||||||
|
@ -56,6 +56,7 @@ function Client(manager, name, config = {}) {
|
|||||||
idMsg: 1,
|
idMsg: 1,
|
||||||
name: name,
|
name: name,
|
||||||
networks: [],
|
networks: [],
|
||||||
|
mentions: [],
|
||||||
manager: manager,
|
manager: manager,
|
||||||
messageStorage: [],
|
messageStorage: [],
|
||||||
highlightRegex: null,
|
highlightRegex: null,
|
||||||
|
@ -179,5 +179,21 @@ module.exports = function (irc, network) {
|
|||||||
true
|
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) {
|
if (!Helper.config.public) {
|
||||||
socket.on("push:register", (subscription) => {
|
socket.on("push:register", (subscription) => {
|
||||||
if (!Object.prototype.hasOwnProperty.call(client.config.sessions, token)) {
|
if (!Object.prototype.hasOwnProperty.call(client.config.sessions, token)) {
|
||||||
|
Loading…
Reference in New Issue
Block a user