HFReply

Reply to messages on HF Convo with formatted text

  1. // ==UserScript==
  2. // @name HFReply
  3. // @version 2025-02-15
  4. // @description Reply to messages on HF Convo with formatted text
  5. // @author NovoDev
  6. // @match https://hackforums.net/*
  7. // @grant none
  8. // @license MIT
  9. // @namespace https://greasyfork.org/users/1435467
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14.  
  15. function addReplyButtons() {
  16. const messageContainerXPath = "/html/body/div[3]/div[3]/div/div[3]/div[3]/div/div[2]/div[3]";
  17. const commentBox = document.evaluate("//*[@id='comment']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  18.  
  19. if (!commentBox) {
  20. console.error("Comment box not found");
  21. return;
  22. }
  23.  
  24. const container = document.evaluate(messageContainerXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  25. if (!container) {
  26. console.error("Message container not found");
  27. return;
  28. }
  29.  
  30. const observer = new MutationObserver(() => {
  31. const messages = container.querySelectorAll(".message-convo-left, .message-convo-right, .message-convo-follow");
  32.  
  33. messages.forEach((msg) => {
  34. if (msg.dataset.replyAdded) return;
  35.  
  36. const isOwnMessage = msg.classList.contains("message-convo-right");
  37. if (isOwnMessage) return;
  38.  
  39. const messageText = msg.textContent.trim().toLowerCase();
  40.  
  41. if (messageText.includes("/flip") || messageText.includes("/jackpot")) {
  42. return;
  43. }
  44.  
  45. msg.dataset.replyAdded = "true";
  46.  
  47. let nameElement = msg.querySelector("[data-profile-username] a strong");
  48. if (!nameElement) {
  49. let prevMsg = msg.previousElementSibling;
  50. while (prevMsg) {
  51. nameElement = prevMsg.querySelector("[data-profile-username] a strong");
  52. if (nameElement) break;
  53. prevMsg = prevMsg.previousElementSibling;
  54. }
  55. }
  56.  
  57. const textContainers = msg.querySelectorAll(".message-bubble-message");
  58.  
  59. if (nameElement && textContainers.length > 0) {
  60. let replyText = `@${nameElement.textContent.trim()}@ : ***"`;
  61.  
  62. textContainers.forEach(container => {
  63. const textSpans = container.querySelectorAll("span");
  64. textSpans.forEach(span => {
  65. replyText += `${span.textContent.trim()} `;
  66. });
  67. });
  68.  
  69. replyText = replyText.trim() + `"***`;
  70.  
  71. const replyButton = document.createElement("span");
  72. replyButton.innerHTML = `
  73. <span style="
  74. cursor: pointer;
  75. color: #fff;
  76. background: #2563eb;
  77. padding: 4px 8px;
  78. margin-left: 10px;
  79. font-size: 12px;
  80. border-radius: 6px;
  81. display: inline-flex;
  82. align-items: center;
  83. gap: 4px;
  84. transition: filter 0.2s;
  85. ">
  86. <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
  87. <path d="M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z"/>
  88. </svg>
  89. Reply
  90. </span>
  91. `;
  92.  
  93. replyButton.onmouseenter = () => {
  94. replyButton.firstElementChild.style.filter = "brightness(0.9)";
  95. };
  96. replyButton.onmouseleave = () => {
  97. replyButton.firstElementChild.style.filter = "none";
  98. };
  99.  
  100. replyButton.onclick = () => {
  101. commentBox.value = `${replyText}\n${commentBox.value}`;
  102. commentBox.focus();
  103. commentBox.scrollIntoView();
  104. };
  105.  
  106. textContainers[textContainers.length - 1].appendChild(replyButton);
  107. }
  108. });
  109. });
  110.  
  111. observer.observe(container, { childList: true, subtree: true });
  112. }
  113.  
  114. window.addEventListener("load", addReplyButtons);
  115. })();