├── DiscordSaveDeleted.user.js
└── README.md
/DiscordSaveDeleted.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Discord Watch Deleted Messages
3 | // @version 1.0.6
4 | // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
5 | // @author toolzmaker
6 | // @description Records all deleted messages in every opened channel and stores them so you can read it later ;)
7 | // @homepageURL https://discord.gg/BJTk6get7H
8 | // @match *://discordapp.com/*
9 | // @match *://discord.com/*
10 | // @downloadURL https://github.com/toolzmaker/DiscordSaveDeleted/raw/main/DiscordSaveDeleted.user.js
11 | // ==/UserScript==
12 |
13 |
14 | (function () {
15 |
16 | /////////////////////CLASS FOR DRAG AND RESIZE WITHOUT JQUERY//////////////////////////////////
17 | class Drag {
18 | /**
19 | * Make an element draggable/resizable
20 | * @param {Element} targetElm The element that will be dragged/resized
21 | * @param {Element} handleElm The element that will listen to events (handdle/grabber)
22 | * @param {object} [options] Options
23 | * @param {string} [options.mode="move"] Define the type of operation (move/resize)
24 | * @param {number} [options.minWidth=200] Minimum width allowed to resize
25 | * @param {number} [options.maxWidth=Infinity] Maximum width allowed to resize
26 | * @param {number} [options.minHeight=100] Maximum height allowed to resize
27 | * @param {number} [options.maxHeight=Infinity] Maximum height allowed to resize
28 | * @param {string} [options.draggingClass="drag"] Class added to targetElm while being dragged
29 | * @param {boolean} [options.useMouseEvents=true] Use mouse events
30 | * @param {boolean} [options.useTouchEvents=true] Use touch events
31 | *
32 | * @author Victor N. wwww.vitim.us
33 | */
34 | constructor(targetElm, handleElm, options) {
35 | this.options = Object.assign({
36 | mode: 'move',
37 |
38 | minWidth: 200,
39 | maxWidth: Infinity,
40 | minHeight: 100,
41 | maxHeight: Infinity,
42 | xAxis: true,
43 | yAxis: true,
44 |
45 | draggingClass: 'drag',
46 |
47 | useMouseEvents: true,
48 | useTouchEvents: true,
49 | }, options);
50 |
51 | // Public properties
52 | this.minWidth = this.options.minWidth;
53 | this.maxWidth = this.options.maxWidth;
54 | this.minHeight = this.options.minHeight;
55 | this.maxHeight = this.options.maxHeight;
56 | this.xAxis = this.options.xAxis;
57 | this.yAxis = this.options.yAxis;
58 | this.draggingClass = this.options.draggingClass;
59 |
60 | /** @private */
61 | this._targetElm = targetElm;
62 | /** @private */
63 | this._handleElm = handleElm;
64 |
65 | const moveOp = (x, y) => {
66 | let l = x - offLeft;
67 | if (x - offLeft < 0) l = 0; //offscreen <-
68 | else if (x - offRight > vw) l = vw - this._targetElm.clientWidth; //offscreen ->
69 | let t = y - offTop;
70 | if (y - offTop < 0) t = 0; //offscreen /\
71 | else if (y - offBottom > vh) t = vh - this._targetElm.clientHeight; //offscreen \/
72 |
73 | if(this.xAxis) this._targetElm.style.left = `${l}px`;
74 | if(this.yAxis) this._targetElm.style.top = `${t}px`;
75 | // NOTE: profilling on chrome translate wasn't faster than top/left as expected. And it also permanently creates a new layer, increasing vram usage.
76 | // this._targetElm.style.transform = `translate(${l}px, ${t}px)`;
77 | };
78 |
79 | const resizeOp = (x, y) => {
80 | let w = x - this._targetElm.offsetLeft - offRight;
81 | if (x - offRight > vw) w = Math.min(vw - this._targetElm.offsetLeft, this.maxWidth); //offscreen ->
82 | else if (x - offRight - this._targetElm.offsetLeft > this.maxWidth) w = this.maxWidth; //max width
83 | else if (x - offRight - this._targetElm.offsetLeft < this.minWidth) w = this.minWidth; //min width
84 | let h = y - this._targetElm.offsetTop - offBottom;
85 | if (y - offBottom > vh) h = Math.min(vh - this._targetElm.offsetTop, this.maxHeight); //offscreen \/
86 | else if (y - offBottom - this._targetElm.offsetTop > this.maxHeight) h = this.maxHeight; //max height
87 | else if (y - offBottom - this._targetElm.offsetTop < this.minHeight) h = this.minHeight; //min height
88 |
89 | if(this.xAxis) this._targetElm.style.width = `${w}px`;
90 | if(this.yAxis) this._targetElm.style.height = `${h}px`;
91 | };
92 |
93 | // define which operation is performed on drag
94 | const operation = this.options.mode === 'move' ? moveOp : resizeOp;
95 |
96 | // offset from the initial click to the target boundaries
97 | let offTop, offLeft, offBottom, offRight;
98 |
99 | let vw = window.innerWidth;
100 | let vh = window.innerHeight;
101 |
102 |
103 | function dragStartHandler(e) {
104 | const touch = e.type === 'touchstart';
105 |
106 | if ((e.buttons === 1 || e.which === 1) || touch) {
107 | e.preventDefault();
108 |
109 | const x = touch ? e.touches[0].clientX : e.clientX;
110 | const y = touch ? e.touches[0].clientY : e.clientY;
111 |
112 | const targetOffset = this._targetElm.getBoundingClientRect();
113 |
114 | //offset from the click to the top-left corner of the target (drag)
115 | offTop = y - targetOffset.y;
116 | offLeft = x - targetOffset.x;
117 | //offset from the click to the bottom-right corner of the target (resize)
118 | offBottom = y - (targetOffset.y + targetOffset.height);
119 | offRight = x - (targetOffset.x + targetOffset.width);
120 |
121 | vw = window.innerWidth;
122 | vh = window.innerHeight;
123 |
124 | if (this.options.useMouseEvents) {
125 | document.addEventListener('mousemove', this._dragMoveHandler);
126 | document.addEventListener('mouseup', this._dragEndHandler);
127 | }
128 | if (this.options.useTouchEvents) {
129 | document.addEventListener('touchmove', this._dragMoveHandler, {
130 | passive: false,
131 | });
132 | document.addEventListener('touchend', this._dragEndHandler);
133 | }
134 |
135 | this._targetElm.classList.add(this.draggingClass);
136 | }
137 | }
138 |
139 | function dragMoveHandler(e) {
140 | e.preventDefault();
141 | let x, y;
142 |
143 | const touch = e.type === 'touchmove';
144 | if (touch) {
145 | const t = e.touches[0];
146 | x = t.clientX;
147 | y = t.clientY;
148 | } else { //mouse
149 |
150 | // If the button is not down, dispatch a "fake" mouse up event, to stop listening to mousemove
151 | // This happens when the mouseup is not captured (outside the browser)
152 | if ((e.buttons || e.which) !== 1) {
153 | this._dragEndHandler();
154 | return;
155 | }
156 |
157 | x = e.clientX;
158 | y = e.clientY;
159 | }
160 |
161 | operation(x, y);
162 | }
163 |
164 | function dragEndHandler(e) {
165 | if (this.options.useMouseEvents) {
166 | document.removeEventListener('mousemove', this._dragMoveHandler);
167 | document.removeEventListener('mouseup', this._dragEndHandler);
168 | }
169 | if (this.options.useTouchEvents) {
170 | document.removeEventListener('touchmove', this._dragMoveHandler);
171 | document.removeEventListener('touchend', this._dragEndHandler);
172 | }
173 | this._targetElm.classList.remove(this.draggingClass);
174 | }
175 |
176 | // We need to bind the handlers to this instance and expose them to methods enable and destroy
177 | /** @private */
178 | this._dragStartHandler = dragStartHandler.bind(this);
179 | /** @private */
180 | this._dragMoveHandler = dragMoveHandler.bind(this);
181 | /** @private */
182 | this._dragEndHandler = dragEndHandler.bind(this);
183 |
184 | this.enable();
185 | }
186 |
187 | /**
188 | * Turn on the drag and drop of the instancea
189 | * @memberOf Drag
190 | */
191 | enable() {
192 | // this.destroy(); // prevent events from getting binded twice
193 | if (this.options.useMouseEvents) this._handleElm.addEventListener('mousedown', this._dragStartHandler);
194 | if (this.options.useTouchEvents) this._handleElm.addEventListener('touchstart', this._dragStartHandler, { passive: false });
195 | }
196 | /**
197 | * Teardown all events bound to the document and elements
198 | * You can resurrect this instance by calling enable()
199 | * @memberOf Drag
200 | */
201 | destroy() {
202 | this._targetElm.classList.remove(this.draggingClass);
203 |
204 | if (this.options.useMouseEvents) {
205 | this._handleElm.removeEventListener('mousedown', this._dragStartHandler);
206 | document.removeEventListener('mousemove', this._dragMoveHandler);
207 | document.removeEventListener('mouseup', this._dragEndHandler);
208 | }
209 | if (this.options.useTouchEvents) {
210 | this._handleElm.removeEventListener('touchstart', this._dragStartHandler);
211 | document.removeEventListener('touchmove', this._dragMoveHandler);
212 | document.removeEventListener('touchend', this._dragEndHandler);
213 | }
214 | }
215 | }
216 | ///////////////////CLASS FOR DRAG AND RESIZE WITHOUT JQUERY////////////////////////////////////
217 |
218 |
219 |
220 | //////////RESTORING LOCALSTORAGE IN DISCORD//////////////
221 | const myiframe = document.createElement("iframe");
222 | myiframe.onload = () => {
223 | const ifrLocalStorage = myiframe.contentWindow.localStorage;
224 | window.localStorage = ifrLocalStorage;
225 | };
226 | myiframe.src = "about:blank";
227 | document.body.appendChild(myiframe);
228 | //////////RESTORING LOCALSTORAGE IN DISCORD//////////////
229 |
230 | //////////ADD NEW STYLES///////////////////////////
231 | var newStyles = (`
232 | .new-hr-line {
233 | border: none;
234 | border-top:
235 | 2px solid silver;
236 | }
237 | .delmsgborder {
238 | border-top-style: solid;
239 | border-top-color: silver;
240 | border-top-width: 1px;
241 | }
242 | .right-onhover-btn {
243 | right:0px;
244 | cursor: pointer;
245 | }
246 | .right-onhover-btn:hover {
247 | font-weight: bolder;
248 | }
249 | `);
250 |
251 | function insertCss(css) {
252 | const style = document.createElement('style');
253 | style.appendChild(document.createTextNode(css));
254 | document.head.appendChild(style);
255 | return style;
256 | }
257 | insertCss(newStyles);
258 | //////////ADD NEW STYLES///////////////////////////
259 |
260 |
261 | var last_channel = ''; // Remembers last switched channel
262 | var msgs_underline = '
'; // Lines after messages in deleted messaged list
263 | var delmsgs_count = 0;
264 | var delmsgs_saved_str = ' messages';
265 |
266 | var observer, observing = false;
267 |
268 | var buttonHtml = (`
269 |
270 |
273 |
274 |
`);
275 |
276 | var savedeletedTemplate = (`
277 |
278 |
Deleted messages count.
279 |
280 |
QQAdmin
sdfsdf
281 |
282 |
DELETED MESSAGES
283 |
X
284 |
//
285 |
`);
286 |
287 | var prev_ele = false;
288 | var savedeletedBtn;
289 | var savedeletedWindow;
290 |
291 | function createElm(html) {
292 | const temp = document.createElement('div');
293 | temp.innerHTML = html;
294 | return temp.removeChild(temp.firstElementChild);
295 | }
296 |
297 | function toggleWindow() {
298 | if (savedeletedWindow.style.display !== 'none') {
299 | savedeletedWindow.style.display = 'none';
300 | savedeletedBtn.style.color = 'var(--interactive-normal)';
301 | } else {
302 | savedeletedWindow.style.display = '';
303 | savedeletedBtn.style.color = 'var(--interactive-active)';
304 | }
305 | }
306 |
307 | var path_find_str = 'channels/';
308 | var local_storage_name = "SAVED_DELMSGS";
309 |
310 | function delmsg_close() {
311 | delmsgs_count -= 1;
312 | CheckMessagesCount(delmsgs_count);
313 |
314 | let delnode_id = this.id;
315 | let var_parsed = JSON.parse(window.localStorage.getItem(local_storage_name));
316 | if (var_parsed[last_channel]) {
317 |
318 | for (let cur_key in var_parsed[last_channel]) {
319 | if (var_parsed[last_channel][cur_key].indexOf(delnode_id) != -1) { //if current id is found in strnig, delete it from вшсе
320 | var_parsed[last_channel].splice(cur_key, 1);
321 | }
322 | }
323 | }
324 | window.localStorage.setItem(local_storage_name, JSON.stringify(var_parsed));
325 |
326 | this.parentNode.remove();
327 | }
328 |
329 | function CheckMessagesCount(DelCount) {
330 | savedeletedWindow.querySelector('#DELMSGS_HEADER').innerHTML = 'Found ' + DelCount.toString() + ' messages.';
331 | }
332 |
333 | function addLocalStorageItem(strng) {
334 |
335 | let channel_path = '0'; // channel string in discord (everything after /channels/)
336 | if (location.pathname.indexOf(path_find_str) != -1) { // if path has /channels/ substring
337 | channel_path = location.pathname.substr(location.pathname.indexOf(path_find_str) + path_find_str.length);
338 | }
339 | let find_str = 'channels/';
340 | location.pathname.substr(location.pathname.indexOf(find_str) + find_str.length);
341 | if (window.localStorage.getItem(local_storage_name) === null) {
342 | let newItem = {
343 | [channel_path]: [strng]
344 | };
345 | window.localStorage.setItem(local_storage_name, JSON.stringify(newItem));
346 | } else {
347 | let var_parsed = JSON.parse(window.localStorage.getItem(local_storage_name));
348 | if(!(channel_path in var_parsed)) {
349 | var_parsed[channel_path] = [strng];
350 | }
351 | else {
352 | var_parsed[channel_path].push(strng);
353 | }
354 | window.localStorage.setItem(local_storage_name, JSON.stringify(var_parsed));
355 | }
356 | }
357 |
358 | function extra_change_message(msg_str) {
359 | const spoiler_regex = /(spoilerText-\w+ )(hidden-[-\w]+)/ig;
360 | return msg_str.replace(spoiler_regex, '$1'); /// UNHIDE SPOILER MESSAGES
361 | }
362 |
363 | function check_channel_change() {
364 | let cur_chan = location.pathname.substr(location.pathname.indexOf(path_find_str) + path_find_str.length);
365 | if (last_channel != cur_chan) { // if switching channels
366 | delmsgs_count = 0; // reset deleted messages count
367 | last_channel = cur_chan;
368 | let delmsglist = savedeletedWindow.querySelector('[id*="DELMSGS_OLMSGLIST"]'); /// get node (deleted message list)
369 | if (delmsglist) { // clear current list when channel changes
370 | delmsglist.innerHTML = '';
371 | }
372 | ///SWITCHING CHANNELS, GETS DATA FROM LOCALSTORAGE
373 | let var_parsed = JSON.parse(window.localStorage.getItem(local_storage_name));
374 | let channel_name = document.body.querySelector('h1[class*="heading-"]').textContent /// Считывает название канала Channel Name
375 | let border_name = savedeletedWindow.querySelector('[id*="DELMSGS_BORDER"]');
376 | if (border_name) {
377 | border_name.innerHTML = 'Deleted in ' + channel_name + '';
378 | }
379 | if (var_parsed[cur_chan]) {
380 | (var_parsed[cur_chan]).forEach(del_record => { // ADD MESSAGE FROM STORED MEMORY
381 | /// UNHIDE SPOILER MESSAGES
382 | const spoiler_regex = /(spoilerText-\w+ )(hidden-[-\w]+)/ig;
383 | del_record = del_record.replace(spoiler_regex, '$1'); ///
384 | delmsglist.innerHTML = delmsglist.innerHTML + extra_change_message(del_record);
385 | delmsgs_count += 1;
386 | });
387 | const closeButtons = delmsglist.querySelectorAll('[id*="delmsg"]');
388 | closeButtons.forEach((cur_elem) => {
389 | cur_elem.onclick = delmsg_close;
390 | });
391 | }
392 | CheckMessagesCount(delmsgs_count);
393 | }
394 | }
395 |
396 | function check(mutations) { // checks DOM mutations, fires when mouse over msg and new msg, even if scroll in somewhere up
397 | check_channel_change();
398 | let delmsgs_scroll = savedeletedWindow.querySelector('[id*="DELMSGS_CLASSDIV"]');
399 | let delmsglist = savedeletedWindow.querySelector('[id*="DELMSGS_OLMSGLIST"]');
400 |
401 | let scroll_elem = document.body.querySelector('[class*="scroller-kQBbkU"]');
402 |
403 | mutations.forEach(function (mutation) { // iterate all mutations
404 | mutation.removedNodes.forEach(function (removed_node) {
405 |
406 | let check_old_msgs = document.body.querySelector('[class*="jumpToPresentBar-"]');
407 | if (check_old_msgs) {
408 | return; // Skips adding new deleted msgs when scrolling old messages
409 | }
410 |
411 | let scroll_elem = document.body.querySelector('[class*="scroller-kQBbkU"]');
412 | if (scroll_elem && scroll_elem.scrollHeight > 7000 && scroll_elem.scrollTop) {
413 | let diff_scroll = 1;
414 | diff_scroll = scroll_elem.scrollHeight / scroll_elem.scrollTop;
415 | if (diff_scroll > 1.7) {
416 | return; // Skip adding deleted mssgs because of scroll
417 | }
418 | }
419 |
420 | if ((removed_node.tagName === 'LI' || removed_node.tagName === 'DIV') && !removed_node.querySelector('[class*="isSending-"]') && (removed_node.querySelector('[class^="markup-"]'))) {
421 |
422 | // because we allow divs, we need to filter out divs that are not just chat deletes
423 | if (!isChatDelete(removed_node)) {
424 | return;
425 | }
426 | let prevCount = 0;
427 | let prevNode = mutation.previousSibling;
428 | let olCount = 0; // OL child elements count
429 |
430 | if (prevNode) {
431 | if (prevNode.parentNode.tagName === 'OL') {
432 | olCount = prevNode.parentNode.childElementCount;
433 |
434 | }
435 | }
436 | while (prevNode /* && prevNode.tagName != 'OL'*/) {
437 | prevCount++;
438 | prevNode = prevNode.previousSibling;
439 | }
440 |
441 | let prevLimit = 10;
442 | if (olCount > prevLimit * 3 && prevCount < prevLimit) {
443 | return; // Skip adding deleted msgs to list if the there are less than 10 elements before the beginning of OL tag. Prevents adding deleted messages when channel dels them from cache.
444 | }
445 |
446 | /// USERNAME IN DELETED NODE
447 | let delmsg_usrname = ''; // Nickname of deleted msg
448 | let delmsg_time = ''; // time of deleted msg
449 |
450 | if (!(getUsername(removed_node))) {
451 | let findNode = mutation.previousSibling;
452 | let usrnameNode = false;
453 | while (findNode) {
454 | usrnameNode = getUsername(findNode, true);
455 | if (usrnameNode) {
456 | break;
457 | }
458 | findNode = findNode.previousSibling;
459 | }
460 | if (usrnameNode) {
461 | delmsg_usrname = usrnameNode.textContent; // Nickname of deleted msg
462 | }
463 | } else { // if deleted message has nickname in it
464 | delmsg_usrname = getUsername(removed_node)
465 | }
466 |
467 | if (delmsglist) {
468 | let id_curtimestamp = 'delmsg' + Date.now();
469 | const contentElements = removed_node.querySelectorAll('[id*="message-content-"]');
470 | let new_delnode = [...contentElements].find(el => !el.className.includes("repliedTextContent"));
471 | let delnode_imgs = removed_node.querySelector('[id*="message-accessories-"]'); //if message has images and other accessories
472 | let msg_time_node = removed_node.querySelector('[id*="message-timestamp-"]');
473 | let msg_time_text = msg_time_node.getAttribute('datetime') ?? "N/A";
474 | //delmsg_time = msg_time_node.textContent;
475 | const mregex = /^20(\d{2})-(\d{2})-(\d{2})T(.+):.+Z/i;
476 | delmsg_time = msg_time_text.replace(mregex, '$4 $3/$2/$1');
477 | /// ADD NEW ITEM TO DELMSGS LIST /////////////
478 | let new_html = '
X
' + delmsg_usrname + ' (' + delmsg_time + ') ' + new_delnode.innerHTML + delnode_imgs.outerHTML;// + msgs_underline;
479 | new_delnode.innerHTML = extra_change_message(new_html);
480 | new_delnode.classList.add("delmsgborder");
481 | delmsglist.appendChild(new_delnode);
482 | //delmsglist.innerHTML = delmsglist.innerHTML + msgs_underline;
483 | let cur_elem = delmsglist.querySelector('[id*="' + id_curtimestamp + '"]');
484 | // Set all mouse events for delete [X] button
485 | cur_elem.onclick = delmsg_close;
486 | delmsgs_count += 1;
487 | CheckMessagesCount(delmsgs_count);
488 | delmsgs_scroll.scrollTop = delmsgs_scroll.scrollHeight; // Scroll to the bottom of the list
489 | addLocalStorageItem(new_delnode.outerHTML); // Add new deleted message to localStorage array
490 | }
491 |
492 | let nextSibl = mutation.nextSibling;
493 | let new_node = removed_node;
494 | let parent_elem = mutation.parentNode;
495 | }
496 | });
497 | });
498 |
499 | function getUsername(node, asNode = false) {
500 | const usernameNode = [...node.querySelectorAll('[class*="username-"]')].find(el => el.closest(`[id^='message-reply-context']`) === null) ?? null;
501 | if (!usernameNode) {
502 | return null;
503 | }
504 | return asNode ? usernameNode : usernameNode.textContent;
505 | }
506 |
507 | /**
508 | * Return true only if this delete refers to a removed message initiated by a user
509 | * @param node
510 | * @returns {boolean}
511 | */
512 | function isChatDelete(node) {
513 | // ensure we-wrapped message element (li -> div) when clicked on a reply link is removed
514 | const messageId = node.id.split("-").pop();
515 | const messageContent = document.querySelector(`#message-content-${messageId}`);
516 | if (messageContent) {
517 | // ignore modals
518 | const popup = messageContent?.closest(`[class^="focusLock"]`) ?? null;
519 | if (!popup) {
520 | return false;
521 | }
522 | }
523 | // ignore div tags removed from uploads finished
524 | if (node.querySelector(`[class^="progressContainer"]`) !== null) {
525 | return false;
526 | }
527 | return true;
528 | }
529 |
530 | }
531 |
532 | function init(observerInit) {
533 | savedeletedWindow = createElm(savedeletedTemplate);
534 | document.body.appendChild(savedeletedWindow);
535 | new Drag(savedeletedWindow, savedeletedWindow.querySelector('.header'), { mode: 'move' });
536 | new Drag(savedeletedWindow, savedeletedWindow.querySelector('.resizer'), { mode: 'resize' });
537 | savedeletedWindow.querySelector("#savedeletedCloseBtn").onclick = toggleWindow;
538 |
539 |
540 | observerInit = {
541 | childList: true,
542 | subtree: true
543 | };
544 | setInterval(function (ele) {
545 | // savedeletedBtn.onclick = toggleWindow;
546 | // Waits for mutation changes and adds button if needed
547 | const discordElm = document.querySelector('#app-mount');
548 | const toolbar = document.querySelector('#app-mount [class^=toolbar]');
549 | if (toolbar && (!discordElm.contains(savedeletedBtn))) { ///+ TOOLBAR BUTTON
550 | savedeletedBtn = createElm(buttonHtml);
551 | savedeletedBtn.onclick = toggleWindow;
552 | toolbar.appendChild(savedeletedBtn);
553 | }
554 | if (location.pathname.substr(0, 10) === "/channels/") {
555 | if (prev_ele) {
556 | if (!(document.body.contains(prev_ele))) {
557 | observing = false;
558 | prev_ele = false;
559 | }
560 | }
561 | if (!observing && (ele = document.querySelector('[class^="messagesWrapper-"]'))) {
562 | observing = true;
563 | prev_ele = ele;
564 | //console.log('Observing is true.');
565 | if (!observer) {
566 | observer = new MutationObserver(check);
567 | }
568 | observer.observe(ele, observerInit);
569 | }
570 | } else if (observing) {
571 | console.log('Observer disconnect!');
572 | observer.disconnect();
573 | observing = false;
574 | }
575 |
576 | if (toolbar) {
577 | check_channel_change(); // Checks channel change every 500ms
578 | }
579 | }, 500);
580 | }
581 |
582 | init(); // Start the whole script
583 |
584 |
585 | })();
586 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DiscordSaveDeleted
2 | [](https://greasyfork.org/en/scripts/453176-discord-watch-deleted-messages) [](https://openuserjs.org/scripts/toolzmaker/Discord_Watch_Deleted_Messages)
3 | You need browser addon for userscripts to work: Tampermonkey (Chrome) or Greasemonkey (Firefox) or Violentmonkey.
4 |
5 | Watches every deleted message in every discord-channel you have opened, and saves them to your browser localStorage (it lasts even after reboot).
6 |
7 | Trashcan icon with an arrow in the right upper corner of Discord window opens Deleted messages list. It contains a list of deleted messages of active channel
8 | To resize that window move your cursor to the right bottom corner of it.
9 |
10 | To record several channels, just open a new browser tab with needed channel and make sure this script is working.
11 | You can switch between channels and every channel will show its deleted messages storage.
12 | You can delete every message from memory by clicking [X] button.
13 | Make sure you enabled auto-update in your userscript settings, so you can get later versions.