- // ==UserScript==
- // @name Discord custom nicknames
- // @namespace https://github.com/aspiers/Discord-custom-nicks-userscript
- // @version 0.3.3
- // @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
- // @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 <userscripts@adamspiers.org>
- //
- // 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 <https://www.gnu.org/licenses/>.
-
- // 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 <title>. 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 replying, name of user we're replying to
- "div[class^=replyBar] span[class^=name]",
-
- /////////////////////////////////////////////////////////
- // 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]"
-
- // 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();
- })();