Merge pull request #3778 from thelounge/xpaw/clear-history
Clear channel history (and a new confirmation dialog)
This commit is contained in:
commit
a4ef328d8d
@ -5,6 +5,7 @@
|
||||
<router-view ref="window"></router-view>
|
||||
<ImageViewer ref="imageViewer" />
|
||||
<ContextMenu ref="contextMenu" />
|
||||
<ConfirmDialog ref="confirmDialog" />
|
||||
<div id="upload-overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
@ -18,6 +19,7 @@ import storage from "../js/localStorage";
|
||||
import Sidebar from "./Sidebar.vue";
|
||||
import ImageViewer from "./ImageViewer.vue";
|
||||
import ContextMenu from "./ContextMenu.vue";
|
||||
import ConfirmDialog from "./ConfirmDialog.vue";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
@ -25,6 +27,7 @@ export default {
|
||||
Sidebar,
|
||||
ImageViewer,
|
||||
ContextMenu,
|
||||
ConfirmDialog,
|
||||
},
|
||||
computed: {
|
||||
viewportClasses() {
|
||||
|
84
client/components/ConfirmDialog.vue
Normal file
84
client/components/ConfirmDialog.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div id="confirm-dialog-overlay" :class="{opened: data !== null}">
|
||||
<div v-if="data !== null" id="confirm-dialog">
|
||||
<div class="confirm-text">
|
||||
<div class="confirm-text-title">{{ data.title }}</div>
|
||||
<p>{{ data.text }}</p>
|
||||
</div>
|
||||
<div class="confirm-buttons">
|
||||
<button class="btn btn-cancel" @click="close(false)">Cancel</button>
|
||||
<button class="btn btn-danger" @click="close(true)">{{ data.button }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#confirm-dialog {
|
||||
background: var(--body-bg-color);
|
||||
color: #fff;
|
||||
margin: 10px;
|
||||
border-radius: 5px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
#confirm-dialog .confirm-text {
|
||||
padding: 15px;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
#confirm-dialog .confirm-text-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#confirm-dialog .confirm-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 15px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#confirm-dialog .confirm-buttons .btn {
|
||||
margin-bottom: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#confirm-dialog .confirm-buttons .btn-cancel {
|
||||
border-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ConfirmDialog",
|
||||
data() {
|
||||
return {
|
||||
data: null,
|
||||
callback: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on("escapekey", this.close);
|
||||
this.$root.$on("confirm-dialog", this.open);
|
||||
},
|
||||
destroyed() {
|
||||
this.$root.$off("escapekey", this.close);
|
||||
this.$root.$off("confirm-dialog", this.open);
|
||||
},
|
||||
methods: {
|
||||
open(data, callback) {
|
||||
this.data = data;
|
||||
this.callback = callback;
|
||||
},
|
||||
close(result) {
|
||||
this.data = null;
|
||||
|
||||
if (this.callback) {
|
||||
this.callback(!!result);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -38,11 +38,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
generateUserContextMenu,
|
||||
generateChannelContextMenu,
|
||||
generateRemoveNetwork,
|
||||
} from "../js/helpers/contextMenu.js";
|
||||
import {generateUserContextMenu, generateChannelContextMenu} from "../js/helpers/contextMenu.js";
|
||||
|
||||
export default {
|
||||
name: "ContextMenu",
|
||||
@ -65,21 +61,15 @@ export default {
|
||||
this.$root.$on("escapekey", this.close);
|
||||
this.$root.$on("contextmenu:user", this.openUserContextMenu);
|
||||
this.$root.$on("contextmenu:channel", this.openChannelContextMenu);
|
||||
this.$root.$on("contextmenu:removenetwork", this.openRemoveNetworkContextMenu);
|
||||
},
|
||||
destroyed() {
|
||||
this.$root.$off("escapekey", this.close);
|
||||
this.$root.$off("contextmenu:user", this.openUserContextMenu);
|
||||
this.$root.$off("contextmenu:channel", this.openChannelContextMenu);
|
||||
this.$root.$off("contextmenu:removenetwork", this.openRemoveNetworkContextMenu);
|
||||
|
||||
this.close();
|
||||
},
|
||||
methods: {
|
||||
openRemoveNetworkContextMenu(data) {
|
||||
const items = generateRemoveNetwork(this.$root, data.lobby);
|
||||
this.open(data.event, items);
|
||||
},
|
||||
openChannelContextMenu(data) {
|
||||
const items = generateChannelContextMenu(this.$root, data.channel, data.network);
|
||||
this.open(data.event, items);
|
||||
|
@ -349,6 +349,7 @@ p {
|
||||
.context-menu-action-voice::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ }
|
||||
.context-menu-network::before { content: "\f233"; /* https://fontawesome.com/icons/server?style=solid */ }
|
||||
.context-menu-edit::before { content: "\f303"; /* https://fontawesome.com/icons/pencil-alt?style=solid */ }
|
||||
.context-menu-clear-history::before { content: "\f1f8"; /* https://fontawesome.com/icons/trash?style=solid */ }
|
||||
|
||||
.channel-list-item .not-secure-icon::before {
|
||||
content: "\f071"; /* https://fontawesome.com/icons/exclamation-triangle?style=solid */
|
||||
@ -2709,6 +2710,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
|
||||
/* Image viewer and drag-and-drop overlay */
|
||||
|
||||
#confirm-dialog-overlay,
|
||||
#upload-overlay,
|
||||
#image-viewer,
|
||||
#image-viewer .open-btn,
|
||||
@ -2720,6 +2722,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#confirm-dialog-overlay,
|
||||
#upload-overlay,
|
||||
#image-viewer {
|
||||
position: fixed;
|
||||
@ -2735,12 +2738,14 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#confirm-dialog-overlay.opened,
|
||||
#upload-overlay.is-dragover,
|
||||
#image-viewer.opened {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#confirm-dialog-overlay,
|
||||
#image-viewer {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
@ -129,6 +129,33 @@ export function generateChannelContextMenu($root, channel, network) {
|
||||
});
|
||||
}
|
||||
|
||||
if (channel.type === "channel" || channel.type === "query") {
|
||||
items.push({
|
||||
label: "Clear history",
|
||||
type: "item",
|
||||
class: "clear-history",
|
||||
action() {
|
||||
$root.$emit(
|
||||
"confirm-dialog",
|
||||
{
|
||||
title: "Clear history",
|
||||
text: `Are you sure you want to clear history for ${channel.name}? This cannot be undone.`,
|
||||
button: "Clear history",
|
||||
},
|
||||
(result) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit("history:clear", {
|
||||
target: channel.id,
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add close menu item
|
||||
items.push({
|
||||
label: closeMap[channel.type],
|
||||
@ -261,31 +288,3 @@ export function generateUserContextMenu($root, channel, network, user) {
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export function generateRemoveNetwork($root, lobby) {
|
||||
return [
|
||||
{
|
||||
label: lobby.name,
|
||||
type: "item",
|
||||
class: "network",
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
label: "Yes, remove this",
|
||||
type: "item",
|
||||
action() {
|
||||
lobby.closed = true;
|
||||
socket.emit("input", {
|
||||
target: Number(lobby.id),
|
||||
text: "/quit",
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Cancel",
|
||||
type: "item",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
14
client/js/socket-events/history_clear.js
Normal file
14
client/js/socket-events/history_clear.js
Normal file
@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
|
||||
import socket from "../socket";
|
||||
import store from "../store";
|
||||
|
||||
socket.on("history:clear", function(data) {
|
||||
const {channel} = store.getters.findChannel(data.target);
|
||||
|
||||
channel.messages = [];
|
||||
channel.unread = 0;
|
||||
channel.highlight = 0;
|
||||
channel.firstUnread = 0;
|
||||
channel.moreHistoryAvailable = false;
|
||||
});
|
@ -23,3 +23,4 @@ import "./sessions_list";
|
||||
import "./configuration";
|
||||
import "./changelog";
|
||||
import "./setting";
|
||||
import "./history_clear";
|
||||
|
@ -30,20 +30,25 @@ const vueApp = new Vue({
|
||||
},
|
||||
closeChannel(channel) {
|
||||
if (channel.type === "lobby") {
|
||||
const el = document.querySelector(
|
||||
`#sidebar .channel-list-item[aria-controls="#chan-${channel.id}"]`
|
||||
);
|
||||
const rect = el.getBoundingClientRect();
|
||||
const event = new MouseEvent("click", {
|
||||
view: window,
|
||||
clientX: rect.left + 10,
|
||||
clientY: rect.top,
|
||||
});
|
||||
this.$root.$emit(
|
||||
"confirm-dialog",
|
||||
{
|
||||
title: "Remove network",
|
||||
text: `Are you sure you want to quit and remove ${channel.name}? This cannot be undone.`,
|
||||
button: "Remove network",
|
||||
},
|
||||
(result) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$root.$emit("contextmenu:removenetwork", {
|
||||
event: event,
|
||||
lobby: channel,
|
||||
});
|
||||
channel.closed = true;
|
||||
socket.emit("input", {
|
||||
target: Number(channel.id),
|
||||
text: "/quit",
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -501,6 +501,32 @@ Client.prototype.more = function(data) {
|
||||
};
|
||||
};
|
||||
|
||||
Client.prototype.clearHistory = function(data) {
|
||||
const client = this;
|
||||
const target = client.find(data.target);
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.chan.messages = [];
|
||||
target.chan.unread = 0;
|
||||
target.chan.highlight = 0;
|
||||
target.chan.firstUnread = 0;
|
||||
|
||||
client.emit("history:clear", {
|
||||
target: target.chan.id,
|
||||
});
|
||||
|
||||
if (!target.chan.isLoggable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const messageStorage of this.messageStorage) {
|
||||
messageStorage.deleteChannel(target.network, target.chan);
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype.open = function(socketId, target) {
|
||||
// Due to how socket.io works internally, normal events may arrive later than
|
||||
// the disconnect event, and because we can't control this timing precisely,
|
||||
|
@ -144,6 +144,20 @@ class MessageStorage {
|
||||
);
|
||||
}
|
||||
|
||||
deleteChannel(network, channel) {
|
||||
if (!this.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.database.serialize(() =>
|
||||
this.database.run(
|
||||
"DELETE FROM messages WHERE network = ? AND channel = ?",
|
||||
network.uuid,
|
||||
channel.name.toLowerCase()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load messages for given channel on a given network and resolve a promise with loaded messages.
|
||||
*
|
||||
|
@ -100,11 +100,35 @@ class TextFileMessageStorage {
|
||||
|
||||
line += "\n";
|
||||
|
||||
fs.appendFile(path.join(logPath, `${cleanFilename(channel.name)}.log`), line, (e) => {
|
||||
if (e) {
|
||||
log.error("Failed to write user log", e);
|
||||
fs.appendFile(
|
||||
path.join(logPath, TextFileMessageStorage.getChannelFileName(channel)),
|
||||
line,
|
||||
(e) => {
|
||||
if (e) {
|
||||
log.error("Failed to write user log", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
deleteChannel() {
|
||||
/* TODO: Truncating text logs is disabled, until we figure out some UI for it
|
||||
if (!this.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logPath = path.join(
|
||||
Helper.getUserLogsPath(),
|
||||
this.client.name,
|
||||
TextFileMessageStorage.getNetworkFolderName(network),
|
||||
TextFileMessageStorage.getChannelFileName(channel)
|
||||
);
|
||||
|
||||
fs.truncate(logPath, 0, (e) => {
|
||||
if (e) {
|
||||
log.error("Failed to truncate user log", e);
|
||||
}
|
||||
});*/
|
||||
}
|
||||
|
||||
getMessages() {
|
||||
@ -125,6 +149,10 @@ class TextFileMessageStorage {
|
||||
|
||||
return `${networkName}-${network.uuid.substring(networkName.length + 1)}`;
|
||||
}
|
||||
|
||||
static getChannelFileName(channel) {
|
||||
return `${cleanFilename(channel.name)}.log`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TextFileMessageStorage;
|
||||
|
@ -420,6 +420,12 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||
network.edit(client, data);
|
||||
});
|
||||
|
||||
socket.on("history:clear", (data) => {
|
||||
if (typeof data === "object") {
|
||||
client.clearHistory(data);
|
||||
}
|
||||
});
|
||||
|
||||
if (!Helper.config.public && !Helper.config.ldap.enable) {
|
||||
socket.on("change-password", (data) => {
|
||||
if (typeof data === "object") {
|
||||
|
Loading…
Reference in New Issue
Block a user