Merge pull request #3778 from thelounge/xpaw/clear-history

Clear channel history (and a new confirmation dialog)
This commit is contained in:
Pavel Djundik 2020-03-09 15:59:37 +02:00 committed by GitHub
commit a4ef328d8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 231 additions and 56 deletions

View File

@ -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() {

View 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>

View File

@ -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);

View File

@ -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);
} }

View File

@ -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",
},
];
}

View 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;
});

View File

@ -23,3 +23,4 @@ import "./sessions_list";
import "./configuration"; import "./configuration";
import "./changelog"; import "./changelog";
import "./setting"; import "./setting";
import "./history_clear";

View File

@ -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;
} }

View File

@ -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,

View File

@ -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.
* *

View File

@ -100,11 +100,35 @@ class TextFileMessageStorage {
line += "\n"; line += "\n";
fs.appendFile(path.join(logPath, `${cleanFilename(channel.name)}.log`), line, (e) => { fs.appendFile(
path.join(logPath, TextFileMessageStorage.getChannelFileName(channel)),
line,
(e) => {
if (e) { if (e) {
log.error("Failed to write user log", 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;

View File

@ -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") {