Discord Static Avatars

Animate avatars in the chat area

  1. // ==UserScript==
  2. // @name Discord Static Avatars
  3. // @namespace https://files.noodlebox.moe/
  4. // @version 1.0.1
  5. // @description Animate avatars in the chat area
  6. // @author Andromeda
  7. // @require https://code.jquery.com/jquery-3.1.1.min.js
  8. // @require https://cdn.jsdelivr.net/lodash/4.17.2/lodash.min.js
  9. // @match *://*.discord.com/channels/*
  10. // @match *://*.discord.com/invite/*
  11. // @match *://*.discord.com/login
  12. // @run-at document-idle
  13. // @grant none
  14. // ==/UserScript==
  15.  
  16. (function ($, _) {
  17. "use strict";
  18.  
  19. // This is super hackish, and will likely break as Discord's internal API changes
  20. // Anything using this or what it returns should be prepared to catch some exceptions
  21. const getInternalInstance = e => e[Object.keys(e).find(k => k.startsWith("__reactInternalInstance"))];
  22.  
  23. function getOwnerInstance(e, {include, exclude=["Popout", "Tooltip", "Scroller", "BackgroundFlash"]} = {}) {
  24. if (e === undefined) {
  25. return undefined;
  26. }
  27.  
  28. // Set up filter; if no include filter is given, match all except those in exclude
  29. const excluding = include === undefined;
  30. const filter = excluding ? exclude : include;
  31.  
  32. // Get displayName of the React class associated with this element
  33. // Based on getName(), but only check for an explicit displayName
  34. function getDisplayName(owner) {
  35. const type = owner._currentElement.type;
  36. const constructor = owner._instance && owner._instance.constructor;
  37. return type.displayName || constructor && constructor.displayName || null;
  38. }
  39. // Check class name against filters
  40. function classFilter(owner) {
  41. const name = getDisplayName(owner);
  42. return (name !== null && !!(filter.includes(name) ^ excluding));
  43. }
  44.  
  45. // Walk up the hierarchy until a proper React object is found
  46. for (let prev, curr=getInternalInstance(e); !_.isNil(curr); prev=curr, curr=curr._hostParent) {
  47. // Before checking its parent, try to find a React object for prev among renderedChildren
  48. // This finds React objects which don't have a direct counterpart in the DOM hierarchy
  49. // e.g. Message, ChannelMember, ...
  50. if (prev !== undefined && !_.isNil(curr._renderedChildren)) {
  51. /* jshint loopfunc: true */
  52. let owner = Object.values(curr._renderedChildren)
  53. .find(v => !_.isNil(v._instance) && v.getHostNode() === prev.getHostNode());
  54. if (!_.isNil(owner) && classFilter(owner)) {
  55. return owner._instance;
  56. }
  57. }
  58.  
  59. if (_.isNil(curr._currentElement)) {
  60. continue;
  61. }
  62.  
  63. // Get a React object if one corresponds to this DOM element
  64. // e.g. .user-popout -> UserPopout, ...
  65. let owner = curr._currentElement._owner;
  66. if (!_.isNil(owner) && classFilter(owner)) {
  67. return owner._instance;
  68. }
  69. }
  70.  
  71. return null;
  72. }
  73.  
  74. $(".theme-dark, .theme-light").on("mouseenter.staticavatars", ".message-group", function () {
  75. try {
  76. getOwnerInstance(this, {include: ["MessageGroup"]}).setState({animate: false, animatedAvatar: false});
  77. } catch (err) {
  78. //console.error("DiscordStaticAvatars", err);
  79. }
  80. });
  81. })(jQuery.noConflict(true), _.noConflict());