// ==UserScript== // @name Discord custom nicknames // @namespace https://github.com/aspiers/Discord-custom-nicks-userscript // @version 0.3.5 // @description Assign custom nicknames to Discord usernames client-side // @author Adam Spiers // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt // @match https://discord.com/channels/* // @icon https://www.google.com/s2/favicons?domain=discord.com // @require https://code.jquery.com/jquery-3.6.0.min.js // @require https://code.jquery.com/ui/1.12.1/jquery-ui.min.js // @require https://greasyfork.org/scripts/5392-waitforkeyelements/code/WaitForKeyElements.js?version=115012 // @resource jQueryUI-css https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/vader/jquery-ui.min.css // @resource jQueryUI-icon1 https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/vader/images/ui-icons_666666_256x240.png // @resource jQueryUI-icon2 https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/vader/images/ui-icons_bbbbbb_256x240.png // @resource jqueryUI-icon3 https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/vader/images/ui-icons_c98000_256x240.png // @downloadURL https://raw.githubusercontent.com/aspiers/Discord-custom-nicks-userscript/main/Discord-custom-nicks.user.js // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_getResourceText // @grant GM_getResourceURL // @grant GM_info // @grant GM_addStyle // @run-at document-end // ==/UserScript== // // Browser userscript to assign custom names to Discord nicknames // Copyright (C) 2021 Adam Spiers // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // Stop JSHint in Tampermonkey's CodeMirror editor from complaining // about globals imported via @require: // https://jshint.com/docs/#inline-configuration /* globals jQuery waitForKeyElements */ (function() { 'use strict'; let $ = jQuery; unsafeWindow.jQuery = jQuery; // Don't replace more often than this number of milliseconds. const DEBOUNCE_MS = 2000; const ELEMENT_PREFIX = "Discord-custom-nicknames-"; const DIALOG_ID = ELEMENT_PREFIX + "dialog"; const TEXTAREA_ID = ELEMENT_PREFIX + "textarea"; const DIALOG_SELECTOR = "#" + DIALOG_ID; const TEXTAREA_SELECTOR = "#" + TEXTAREA_ID; const ORIG_ATTR = "data-Discord-orig-nickname"; const STORAGE = "Discord_custom_nicknames_mapping"; function get_nick_map_str() { let map_str = GM_getValue(STORAGE); return typeof(map_str) == "string" ? map_str : ""; } unsafeWindow.get_nick_map_str = get_nick_map_str; function set_nick_map_str(new_value) { GM_setValue(STORAGE, new_value); } unsafeWindow.set_nick_map_str = set_nick_map_str; function get_nick_map() { return parse_map(get_nick_map_str()); } unsafeWindow.get_nick_map = get_nick_map; // function serialise_map(map_obj) { // return Object.entries(map_obj).map(e => e[0] + "=" + e[1]).join("\n"); // } function parse_map(map_str) { let map_obj = {}; for (const pair of map_str.split("\n")) { if (pair.indexOf("=") != -1) { let [k, v] = pair.split("="); map_obj[k] = v; } } return map_obj; } window.parse_map = parse_map; const PREFIX = "[Discord custom nicknames]"; function debug(...args) { console.debug(PREFIX, ...args); } function log(...args) { console.log(PREFIX, ...args); } function replace_nick(nick_map, element) { // debug("replace", element); let orig_nick = element.getAttribute(ORIG_ATTR); let Discord_nick = orig_nick || element.innerText; let at = ""; if (Discord_nick.startsWith("@")) { at = "@"; Discord_nick = Discord_nick.slice(1); } let mapped_name = nick_map[Discord_nick]; if (mapped_name) { mapped_name = at + mapped_name; debug(`${at}${Discord_nick} -> ${mapped_name}`); if (!orig_nick && element.tagName !== "TITLE") { // Back up the original to an attribute so that we can remap later // without reloading the page. // // FIXME: Figure out a way to make this work // flawlessly for . Currently it's slightly // broken because <title> can change values when // switching between DM pages, so we can't back up // the original username to an attribute on it. element.setAttribute(ORIG_ATTR, element.innerText) } element.innerText = mapped_name; } else { // debug(`no mapping found for ${element.innerText}`); // This is required in case a nick mapping is removed: if (orig_nick) { element.innerText = orig_nick; } } } function replace_css_elements(nick_map, query) { let matches = jQuery(query); // debug(`replacing ${query}`, matches); if (matches && matches.each) { matches.each((i, elt) => replace_nick(nick_map, elt)); } } function replace_all() { debug("replace_all()"); let nick_map = get_nick_map(); debug("parsed:", nick_map); for (let selector of CSS_SELECTORS) { replace_css_elements(nick_map, selector); } } function dialog_html() { return ` <div id="${DIALOG_ID}" title="Discord custom nicknames"> <p> Enter your mappings here, one on each line. </p> <textarea rows="10" cols="50" id="${TEXTAREA_ID}" placeholder="nickname=Real Name"></textarea> <p> Each mapping should look something like </p> <pre><code>nickname=Firstname Lastname</code></pre> <p> where the left-hand side of the <code>=</code> sign is the normal Discord nickname (excluding the <code>#1234</code> suffix), and the right-hand side is what you want to see instead. </p> </div> `; } function handle_dialog_save(dialog) { let map_str = $(TEXTAREA_SELECTOR).val(); debug(`${TEXTAREA_SELECTOR} dialog save:`, map_str); GM_setValue(STORAGE, map_str || ""); replace_all(); $(dialog).dialog("close"); } function handle_dialog_open(dialog) { let orig = get_nick_map_str(); debug(`restoring ${TEXTAREA_SELECTOR} to`, orig); $(TEXTAREA_SELECTOR).val(orig); } unsafeWindow.GM_info = GM_info; function insert_CSS() { let CSS = GM_getResourceText("jQueryUI-css"); for (let resource of GM_info.script.resources) { let image = resource.url.match(/images\/.+\.png/); if (!image) { continue; } let URL = GM_getResourceURL(resource.name); let rel_path = image[0]; CSS = CSS.replaceAll( `url("${rel_path}")`, `url("${URL}")`, ); } GM_addStyle(CSS); } function insert_dialog() { $("body").append(dialog_html()); $(TEXTAREA_SELECTOR).val(get_nick_map_str()); $(DIALOG_SELECTOR).dialog({ minWidth: 300, width: 700, maxWidth: 300, buttons: [ { text: "Save", click: function() { handle_dialog_save(this); } }, { text: "Cancel", click: function() { $(this).dialog("close"); } } ], open: handle_dialog_open, }); } function display_dialog() { if ($(DIALOG_SELECTOR).length == 0) { insert_CSS(); insert_dialog(); } $(DIALOG_SELECTOR).dialog("open"); } GM_registerMenuCommand("Nickname mapping", display_dialog); const CSS_SELECTORS = [ "title", ///////////////////////////////////////////////////////// // Channel pages // User list on right-hand side "div[class^=membersWrap] span[class^=roleColor]", // Attributions in main chat pane "span[class^=headerText] span[class^=username]", // Mentions within messages "div[class*=messageContent] span.mention", // When composing a reply, name of user we're replying to "div[class^=replyBar] span[class^=name]", // Attributions of messages replied to "div[class^=repliedMessage] span[class^=username]", ///////////////////////////////////////////////////////// // DM pages // DM list in left bar "div#private-channels div[class^=nameAndDecorators]", // Main friends list when "Friends" is clicked on "div[class^=peopleList] div[class^=userInfo] span[class^=username]", // Top of individual DM page "div[class^=chat] section[class^=title] h3[class*=title]", // h3 under individual DM large avatar "div[id^=chat-messages] h3[class^=header]", // "Person is typing..." notification at bottom "main[class^=chatContent] div[class^=typing] span[class^=text] strong", // N.B. deliberately not replacing // // "This is the beginning of your direct message history with" // // because that's a useful place to show the mapping with // the original username. ]; function init() { let lastWaited = {}; let nick_map = get_nick_map(); for (let selector of CSS_SELECTORS) { waitForKeyElements( selector, () => { debug("waitForKeyElements triggered for", selector); let last = lastWaited[selector]; if (!last || (new Date() - last > DEBOUNCE_MS)) { replace_css_elements(nick_map, selector); lastWaited[selector] = new Date(); } } ); } setInterval(replace_all, 5000); } init(); })();