AOPS classroom notification sound

Beep when a new message div is created or a key is pressed

  1. // ==UserScript==
  2. // @name AOPS classroom notification sound
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.6
  5. // @description Beep when a new message div is created or a key is pressed
  6. // @author Shaun Wang
  7. // @match https://artofproblemsolving.com/classroom/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=artofproblemsolving.com
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // notification sound
  16. function notificationSound() {
  17. var context = new (window.AudioContext || window.webkitAudioContext)();
  18. // Oscillators and gain nodes for A and E notes
  19. var oscillatorA = context.createOscillator();
  20. var oscillatorE = context.createOscillator();
  21. var gainNodeA = context.createGain(); // Gain node for A note
  22. var gainNodeE = context.createGain(); // Gain node for E note
  23.  
  24. oscillatorA.type = 'sine';
  25. oscillatorE.type = 'sine';
  26.  
  27. // Connect oscillators to their respective gain nodes
  28. oscillatorA.connect(gainNodeA);
  29. oscillatorE.connect(gainNodeE);
  30.  
  31. // Connect gain nodes to context destination
  32. gainNodeA.connect(context.destination);
  33. gainNodeE.connect(context.destination);
  34.  
  35. // A note settings
  36. oscillatorA.frequency.setValueAtTime(440, context.currentTime);
  37. gainNodeA.gain.setValueAtTime(1, context.currentTime);
  38. gainNodeA.gain.exponentialRampToValueAtTime(0.00001, context.currentTime + 1);
  39. oscillatorA.start(context.currentTime);
  40. oscillatorA.stop(context.currentTime + 1);
  41.  
  42. // E note settings (starts shortly after A)
  43. oscillatorE.frequency.setValueAtTime(659.25, context.currentTime);
  44. gainNodeE.gain.setValueAtTime(1, context.currentTime);
  45. gainNodeE.gain.exponentialRampToValueAtTime(0.00001, context.currentTime + 1);
  46. oscillatorE.start(context.currentTime + 0.05);
  47. oscillatorE.stop(context.currentTime + 1);
  48. }
  49.  
  50. // check if the new node has the required classes
  51. function isTargetNode(node) {
  52. return node.nodeType === 1 && node.classList.contains('styles_thread__3HaEQ') && node.classList.contains('styles_topTracked__wDRH_');
  53. }
  54.  
  55. // mark existing messages to avoid triggering sound on them
  56. function markExistingMessages() {
  57. const existingMessages = document.querySelectorAll('.styles_thread__3HaEQ.styles_topTracked__wDRH_');
  58. existingMessages.forEach(message => {
  59. message.dataset.seen = 'true';
  60. });
  61. }
  62.  
  63. // monitors the page
  64. var observer = new MutationObserver(function(mutationsList) {
  65. for (var mutation of mutationsList) {
  66. if (mutation.type === 'childList') {
  67. for (var addedNode of mutation.addedNodes) {
  68. if (isTargetNode(addedNode) && !addedNode.dataset.seen) {
  69. addedNode.dataset.seen = 'true';
  70. notificationSound();
  71. }
  72. }
  73. }
  74. }
  75. });
  76.  
  77. // mark existing messages and start observing for new ones
  78. window.addEventListener('load', function() {
  79. markExistingMessages();
  80. observer.observe(document.body, { childList: true, subtree: true });
  81. notificationSound();
  82. });
  83.  
  84.  
  85. })();