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>
|
<router-view ref="window"></router-view>
|
||||||
<ImageViewer ref="imageViewer" />
|
<ImageViewer ref="imageViewer" />
|
||||||
<ContextMenu ref="contextMenu" />
|
<ContextMenu ref="contextMenu" />
|
||||||
|
<ConfirmDialog ref="confirmDialog" />
|
||||||
<div id="upload-overlay"></div>
|
<div id="upload-overlay"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -18,6 +19,7 @@ import storage from "../js/localStorage";
|
|||||||
import Sidebar from "./Sidebar.vue";
|
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";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
@ -25,6 +27,7 @@ export default {
|
|||||||
Sidebar,
|
Sidebar,
|
||||||
ImageViewer,
|
ImageViewer,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
|
ConfirmDialog,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
viewportClasses() {
|
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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {generateUserContextMenu, generateChannelContextMenu} from "../js/helpers/contextMenu.js";
|
||||||
generateUserContextMenu,
|
|
||||||
generateChannelContextMenu,
|
|
||||||
generateRemoveNetwork,
|
|
||||||
} from "../js/helpers/contextMenu.js";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ContextMenu",
|
name: "ContextMenu",
|
||||||
@ -65,21 +61,15 @@ export default {
|
|||||||
this.$root.$on("escapekey", this.close);
|
this.$root.$on("escapekey", this.close);
|
||||||
this.$root.$on("contextmenu:user", this.openUserContextMenu);
|
this.$root.$on("contextmenu:user", this.openUserContextMenu);
|
||||||
this.$root.$on("contextmenu:channel", this.openChannelContextMenu);
|
this.$root.$on("contextmenu:channel", this.openChannelContextMenu);
|
||||||
this.$root.$on("contextmenu:removenetwork", this.openRemoveNetworkContextMenu);
|
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
this.$root.$off("escapekey", this.close);
|
this.$root.$off("escapekey", this.close);
|
||||||
this.$root.$off("contextmenu:user", this.openUserContextMenu);
|
this.$root.$off("contextmenu:user", this.openUserContextMenu);
|
||||||
this.$root.$off("contextmenu:channel", this.openChannelContextMenu);
|
this.$root.$off("contextmenu:channel", this.openChannelContextMenu);
|
||||||
this.$root.$off("contextmenu:removenetwork", this.openRemoveNetworkContextMenu);
|
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openRemoveNetworkContextMenu(data) {
|
|
||||||
const items = generateRemoveNetwork(this.$root, data.lobby);
|
|
||||||
this.open(data.event, items);
|
|
||||||
},
|
|
||||||
openChannelContextMenu(data) {
|
openChannelContextMenu(data) {
|
||||||
const items = generateChannelContextMenu(this.$root, data.channel, data.network);
|
const items = generateChannelContextMenu(this.$root, data.channel, data.network);
|
||||||
this.open(data.event, items);
|
this.open(data.event, items);
|
||||||
|
@ -349,6 +349,7 @@ p {
|
|||||||
.context-menu-action-voice::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ }
|
.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-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-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 {
|
.channel-list-item .not-secure-icon::before {
|
||||||
content: "\f071"; /* https://fontawesome.com/icons/exclamation-triangle?style=solid */
|
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 */
|
/* Image viewer and drag-and-drop overlay */
|
||||||
|
|
||||||
|
#confirm-dialog-overlay,
|
||||||
#upload-overlay,
|
#upload-overlay,
|
||||||
#image-viewer,
|
#image-viewer,
|
||||||
#image-viewer .open-btn,
|
#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;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#confirm-dialog-overlay,
|
||||||
#upload-overlay,
|
#upload-overlay,
|
||||||
#image-viewer {
|
#image-viewer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -2735,12 +2738,14 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#confirm-dialog-overlay.opened,
|
||||||
#upload-overlay.is-dragover,
|
#upload-overlay.is-dragover,
|
||||||
#image-viewer.opened {
|
#image-viewer.opened {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#confirm-dialog-overlay,
|
||||||
#image-viewer {
|
#image-viewer {
|
||||||
background: rgba(0, 0, 0, 0.9);
|
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
|
// Add close menu item
|
||||||
items.push({
|
items.push({
|
||||||
label: closeMap[channel.type],
|
label: closeMap[channel.type],
|
||||||
@ -261,31 +288,3 @@ export function generateUserContextMenu($root, channel, network, user) {
|
|||||||
|
|
||||||
return items;
|
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 "./configuration";
|
||||||
import "./changelog";
|
import "./changelog";
|
||||||
import "./setting";
|
import "./setting";
|
||||||
|
import "./history_clear";
|
||||||
|
@ -30,20 +30,25 @@ const vueApp = new Vue({
|
|||||||
},
|
},
|
||||||
closeChannel(channel) {
|
closeChannel(channel) {
|
||||||
if (channel.type === "lobby") {
|
if (channel.type === "lobby") {
|
||||||
const el = document.querySelector(
|
this.$root.$emit(
|
||||||
`#sidebar .channel-list-item[aria-controls="#chan-${channel.id}"]`
|
"confirm-dialog",
|
||||||
);
|
{
|
||||||
const rect = el.getBoundingClientRect();
|
title: "Remove network",
|
||||||
const event = new MouseEvent("click", {
|
text: `Are you sure you want to quit and remove ${channel.name}? This cannot be undone.`,
|
||||||
view: window,
|
button: "Remove network",
|
||||||
clientX: rect.left + 10,
|
},
|
||||||
clientY: rect.top,
|
(result) => {
|
||||||
});
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.$root.$emit("contextmenu:removenetwork", {
|
channel.closed = true;
|
||||||
event: event,
|
socket.emit("input", {
|
||||||
lobby: channel,
|
target: Number(channel.id),
|
||||||
});
|
text: "/quit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return;
|
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) {
|
Client.prototype.open = function(socketId, target) {
|
||||||
// Due to how socket.io works internally, normal events may arrive later than
|
// 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,
|
// 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.
|
* Load messages for given channel on a given network and resolve a promise with loaded messages.
|
||||||
*
|
*
|
||||||
|
@ -100,11 +100,35 @@ class TextFileMessageStorage {
|
|||||||
|
|
||||||
line += "\n";
|
line += "\n";
|
||||||
|
|
||||||
fs.appendFile(path.join(logPath, `${cleanFilename(channel.name)}.log`), line, (e) => {
|
fs.appendFile(
|
||||||
if (e) {
|
path.join(logPath, TextFileMessageStorage.getChannelFileName(channel)),
|
||||||
log.error("Failed to write user log", e);
|
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() {
|
getMessages() {
|
||||||
@ -125,6 +149,10 @@ class TextFileMessageStorage {
|
|||||||
|
|
||||||
return `${networkName}-${network.uuid.substring(networkName.length + 1)}`;
|
return `${networkName}-${network.uuid.substring(networkName.length + 1)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getChannelFileName(channel) {
|
||||||
|
return `${cleanFilename(channel.name)}.log`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TextFileMessageStorage;
|
module.exports = TextFileMessageStorage;
|
||||||
|
@ -420,6 +420,12 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
network.edit(client, data);
|
network.edit(client, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("history:clear", (data) => {
|
||||||
|
if (typeof data === "object") {
|
||||||
|
client.clearHistory(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!Helper.config.public && !Helper.config.ldap.enable) {
|
if (!Helper.config.public && !Helper.config.ldap.enable) {
|
||||||
socket.on("change-password", (data) => {
|
socket.on("change-password", (data) => {
|
||||||
if (typeof data === "object") {
|
if (typeof data === "object") {
|
||||||
|
Loading…
Reference in New Issue
Block a user