├── 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 | 271 | 272 | 273 | 274 |
`); 275 | 276 | var savedeletedTemplate = (``); 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.

    14 | 15 | Discussion:
    16 | Discord: https://discord.gg/BJTk6get7H
    17 | Telegram: https://t.me/toolz_maker 18 | 19 | --------------------------------------------------------------------------------