[Reddit] Modmail++

Additional tools and information to Reddit's Modmail

目前為 2022-08-30 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name [Reddit] Modmail++
  3. // @namespace HKR
  4. // @match https://mod.reddit.com/mail/*
  5. // @grant none
  6. // @version 3.7
  7. // @author HKR
  8. // @description Additional tools and information to Reddit's Modmail
  9. // @icon https://www.redditstatic.com/modmail/favicon/favicon-32x32.png
  10. // @supportURL https://github.com/Hakorr/Userscripts/issues
  11. // @require https://cdn.jsdelivr.net/npm/party-js@2.1.2/bundle/party.js#sha256-J9/UDCn536lyy03NDKIUT6WX3DU9FqZZ9ydg++UVUC0=
  12. // ==/UserScript==
  13.  
  14. (() => {
  15. console.log("[Modmail++] %cScript started!", "color: green");
  16.  
  17. class __settings__ {
  18. subTag = $(".ThreadTitle__community")?.href?.slice(23) || "r/subreddit"; //Format r/subreddit
  19. userTag = "u/" + $(".ModIdCard__UserNameLink")?.innerText || "u/username"; //Format u/username
  20. modmail = `[modmail](https://www.reddit.com/message/compose?to=/${keepPrefix(this.subTag, true)})`;
  21. rules = `https://www.reddit.com/${keepPrefix(this.subTag, true)}/about/rules`;
  22. /* Responses - Edit to your own liking, remove whatever you don't like!
  23. - name | The name of the response that will show on the listbox. (Example value: "Hello!")
  24. - replace | Replace all messagebox text if true, otherwise just add. (Example value: true)
  25. - subreddit | Visible only while on this subreddit's modmail. (Example value: "r/subreddit")
  26. - content | This text will be added to the messagebox once selected (Example value: "Hello world!")*/
  27. responses = [
  28. {
  29. "name":"Default Approved",
  30. "replace":true,
  31. "subreddit":"",
  32. "content":`Hey, approved the post!`
  33. },
  34. {
  35. "name":"Default Rule Broken",
  36. "replace":true,
  37. "subreddit":"",
  38. "content":`Your post broke our [rules](${this.rules}).\n\nThe action will not be reverted.`
  39. },
  40. {
  41. "name":"Add Rule Description",
  42. "replace":false,
  43. "subreddit":"",
  44. "content":`<open-rulelist-dialog>`
  45. },
  46. {
  47. "name":"Add Greetings",
  48. "replace":false,
  49. "subreddit":"",
  50. "content":`${randItem(["Greetings","Hello","Hi"])} ${this.userTag},\n\n`
  51. },
  52. {
  53. "name":"Add Subreddit Mention",
  54. "replace":false,
  55. "subreddit":"",
  56. "content":`${this.subTag}`
  57. },
  58. {
  59. "name":"Add User Mention",
  60. "replace":false,
  61. "subreddit":"",
  62. "content":`${this.userTag}`
  63. },
  64. {
  65. "name":"Add Modmail Link",
  66. "replace":false,
  67. "subreddit":"",
  68. "content":`${this.modmail}`
  69. },
  70. {
  71. "name":"Add Karma Link",
  72. "replace":false,
  73. "subreddit":"",
  74. "content":`[karma](https://reddit.zendesk.com/hc/en-us/articles/204511829-What-is-karma-)`
  75. },
  76. {
  77. "name":"Add Shadowban Link",
  78. "replace":false,
  79. "subreddit":"",
  80. "content":`[shadowbanned](https://www.reddit.com/r/ShadowBan/comments/8a2gpk/an_unofficial_guide_on_how_to_avoid_being/)`
  81. },
  82. {
  83. "name":"Add Content Policy",
  84. "replace":false,
  85. "subreddit":"",
  86. "content":`[Content Policy](https://www.redditinc.com/policies/content-policy)`
  87. },
  88. {
  89. "name":"Add User Agreement",
  90. "replace":false,
  91. "subreddit":"",
  92. "content":`[User Agreement](https://www.redditinc.com/policies/user-agreement)`
  93. },
  94. {
  95. "name":"Add Reddiquette",
  96. "replace":false,
  97. "subreddit":"",
  98. "content":`[Reddiquette](https://reddit.zendesk.com/hc/en-us/articles/205926439-Reddiquette)`
  99. },
  100. {
  101. "name":"Add Admin Modmail",
  102. "replace":false,
  103. "subreddit":"",
  104. "content":`[Admins](https://www.reddit.com/message/compose?to=%2Fr%2Freddit.com)`
  105. },
  106. {
  107. "name":"Add Rickroll",
  108. "replace":false,
  109. "subreddit":"",
  110. "content":`[link](https://www.youtube.com/watch?v=dQw4w9WgXcQ)`
  111. },
  112. {
  113. "name":"Invitation (New Message)",
  114. "replace":true,
  115. "subreddit":"",
  116. "subject": `Invitation to become a moderator of ${this.subTag}`,
  117. "content":`${randItem(["Greetings","Hello","Hi"])} ${this.userTag},\n\nWould you like to moderate ${this.subTag} with us? Please let us know as soon as possible!`
  118. }
  119. ];
  120.  
  121. themeMode = $$(".theme-dark").length ? true : false;
  122. textColor = this.themeMode ? "#757575" : "#6e6e6e"; // dark hex : light hex
  123. titleColor = this.themeMode ? "#a7a7a7" : "#2c2c2c"; // dark hex : light hex
  124. listBoxColor = this.themeMode ? "#242424" : "#f1f3f5"; // dark hex : light hex
  125.  
  126. dataColor = "#0079d3"; // data (numbers etc.) color
  127.  
  128. enableCustomResponses = true; // if to append the custom response box
  129.  
  130. chatProfileIcons = true; // if to append chat profile icons
  131.  
  132. placeholderMessage = randItem([
  133. "Message...",
  134. "Look, a bird! Message...",
  135. "What have you been up to today? Message...",
  136. "Beautiful day, isn't it? Message...",
  137. "Was the weather nice? Message...",
  138. "You look good today! Message...",
  139. "What dreams did you see last night? Message...",
  140. "What did you do today? Message...",
  141. "What did you eat today? Message...",
  142. "Have you drank enough water? Message...",
  143. "Remember to stretch! Message...",
  144. "≖‿≖ I live inside of your walls. Message...",
  145. "(✿◠‿◠) Message...",
  146. ]);
  147. }
  148. /*/////////////////////////////////////////////////
  149. \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
  150. DO NOT PROCEED IF YOU DO NOT KNOW WHAT YOU'RE DOING
  151. ///////////////////////////////////////////////////
  152. \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*/
  153. const $ = document.querySelector.bind(document);
  154. const $$ = document.querySelectorAll.bind(document);
  155.  
  156. // returns a random item from array
  157. const randItem = itemArr => itemArr[Math.floor(Math.random() * itemArr.length)];
  158. // removes the Reddit prefix
  159. const removePrefix = username => ["r/","u/"].some(tag => username.includes(tag)) ? username.slice(2) : username;
  160. // adds the Reddit prefix if nonexistant
  161. const keepPrefix = (username, subreddit) => ["r/","u/"].some(tag => username.includes(tag)) ? username : subreddit ? `r/${username}` : `u/${username}`;
  162.  
  163. const recipientUsername = () => {
  164. const defaultUsernameElem = $(".ModIdCard__UserNameLink");
  165. if(defaultUsernameElem) {
  166. return removePrefix(defaultUsernameElem?.innerText);
  167. }
  168. else {
  169. return undefined;
  170. }
  171. };
  172. async function Get(url) {
  173. const response = await fetch(url);
  174.  
  175. if (!response.ok) {
  176. throw new Error(`HTTP error! status: ${response.status}`);
  177. }
  178.  
  179. const text = await response.text();
  180. return text;
  181. };
  182.  
  183. async function getUserInfo() {
  184. try
  185. {
  186. const about = await Get(`https://www.reddit.com/user/${recipientUsername()}/about.json`);
  187. return JSON.parse(about);
  188. }
  189. catch
  190. {
  191. console.log("[Modmail++] %cFailed to load user information.", "color: red");
  192. return 0;
  193. }
  194. };
  195. async function getRules(Settings) {
  196. try
  197. {
  198. const rules = await Get(Settings.rules + ".json");
  199. return JSON.parse(rules);
  200. }
  201. catch
  202. {
  203. console.log("[Modmail++] %cFailed to load subreddit rules, possibly a private subreddit?", "color: red");
  204. return 0;
  205. }
  206. };
  207.  
  208. // adds a zero suffix if x < 10
  209. const fixNumber = number => number < 10 ? "0" + number : number;
  210.  
  211. // returns a date string from UNIX timestamp
  212. function unixToDate(UNIX_timestamp) {
  213. const d = new Date(UNIX_timestamp * 1000);
  214. const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  215.  
  216. const year = d.getFullYear(),
  217. monthNum = d.getMonth() + 1,
  218. month = months[d.getMonth()],
  219. date = d.getDate(),
  220. hour = fixNumber(d.getHours()),
  221. min = fixNumber(d.getMinutes()),
  222. sec = fixNumber(d.getSeconds());
  223.  
  224. return `${date}.${monthNum}.${year} ${hour}:${min}:${sec}`; // (DD/MM/YY HH/MM/SS)
  225. };
  226.  
  227. // returns a string without evil HTML elements
  228. function sanitize(evilstring) {
  229. const decoder = document.createElement('div');
  230. decoder.innerHTML = evilstring;
  231. return decoder.textContent;
  232. };
  233.  
  234. // add a link to the Modmail text and change its name to Modmail++
  235. const sidebarTitle = $(".Sidebar__titleMessage");
  236. sidebarTitle.setAttribute("onclick","window.open('https://github.com/Hakorr/Userscripts/tree/main/Reddit.com/ModmailExtraInfo')");
  237. sidebarTitle.setAttribute("style","cursor: pointer");
  238. sidebarTitle.innerText = "Modmail++";
  239.  
  240. // apply the custom css
  241. function applyCSS(Settings) {
  242. //Took advice for the listbox CSS from moderncss.dev/custom-select-styles-with-pure-css, thanks!
  243. const css = `
  244. .profileIcon {
  245. margin-bottom: 10px;
  246. border-radius: 50%;
  247. height: 100px !important;
  248. }
  249. .InfoBar__recentsNone {
  250. color: #6e6e6e;
  251. }
  252. .InfoBar__metadata, .InfoBar__recents {
  253. margin: 6px 0;
  254. margin-left: 10px;
  255. }
  256. .value {
  257. color: ${Settings.dataColor};
  258. }
  259. .InfoBar__banText {
  260. padding-bottom: 15px;
  261. }
  262. .InfoBar__username, .InfoBar__username:visited {
  263. padding-left: 10px;
  264. }
  265. .dataText {
  266. color: ${Settings.textColor};
  267. font-size: 13px;
  268. padding-left: 10px;
  269. }
  270. .dataTitle {
  271. color: ${Settings.titleColor};
  272. font-size: 15px;
  273. margin-bottom: 3px;
  274. margin-top: 5px;
  275. }
  276. .responseListbox {
  277. width: 50%;
  278. cursor: pointer;
  279. }
  280. :root {
  281. --select-border: #0079d3;
  282. --select-focus: blue;
  283. --select-arrow: var(--select-border);
  284. }
  285. *,
  286. *::before,
  287. *::after {
  288. box-sizing: border-box;
  289. }
  290. select {
  291. appearance: none;
  292. background-color: ${Settings.listBoxColor};
  293. color: ${Settings.textColor};
  294. border: none;
  295. padding: 0 1em 0 0;
  296. margin: 0;
  297. width: 100%;
  298. cursor: pointer;
  299. font-family: inherit;
  300. font-size: inherit;
  301. line-height: inherit;
  302. outline: none;
  303. position: relative;
  304. }
  305. .select {
  306. width: 100%;
  307. min-width: 15ch;
  308. max-width: 30ch;
  309. border: 1px solid var(--select-border);
  310. border-radius: 0.25em;
  311. padding: 0.3em 0.4em;
  312. font-size: 0.9rem;
  313. line-height: 1.1;
  314. background-color: ${Settings.listBoxColor};
  315. margin-bottom: 15px;
  316. }
  317. select::-ms-expand {
  318. display: none;
  319. }
  320. option {
  321. white-space: normal;
  322. outline-color: var(--select-focus);
  323. }
  324. select:focus + .focus {
  325. position: absolute;
  326. top: -1px;
  327. left: -1px;
  328. right: -1px;
  329. bottom: -1px;
  330. border: 2px solid var(--select-focus);
  331. border-radius: inherit;
  332. }
  333. .Author__text {
  334. padding: 6px 0;
  335. }
  336. .chatProfileIcon {
  337. margin-right: 7px;
  338. transition: transform .1s;
  339. border-radius: 50%;
  340. }
  341. .App__page {
  342. background: var(--color-tone-8);
  343. }
  344. ::-webkit-scrollbar {
  345. width: 10px;
  346. }
  347. ::-webkit-scrollbar-track {
  348. background: ${Settings.listBoxColor};
  349. }
  350. ::-webkit-scrollbar-thumb {
  351. background: #888;
  352. }
  353. ::-webkit-scrollbar-thumb:hover {
  354. background: #555;
  355. }
  356. .subredditRuleList {
  357. --newRedditTheme-bodyText: ${Settings.titleColor};
  358. --newRedditTheme-metaText: ${Settings.textColor};
  359. --newRedditTheme-navIconFaded10: rgba(215,218,220,0.1);
  360. --newRedditTheme-actionIconTinted80: #9a9b9c;
  361. --newRedditTheme-activeShaded90: #006cbd;
  362. --newRedditTheme-actionIconAlpha20: rgba(129,131,132,0.2);
  363. --newCommunityTheme-actionIcon: #818384;
  364. --newRedditTheme-bodyTextAlpha03: ${Settings.listBoxColor};
  365. --newRedditTheme-navIcon: #D7DADC;
  366. --newCommunityTheme-line: #343536;
  367. --newCommunityTheme-body: #1A1A1B;
  368. }
  369. .ruleList {
  370. padding: 0 24px 0 20px;
  371. background: var(--newRedditTheme-bodyTextAlpha03);
  372. max-height: 100%;
  373. }
  374. html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, button, cite, code, del, dfn, em, img, input, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
  375. margin: 0;
  376. padding: 0;
  377. border: 0;
  378. font-size: 100%;
  379. font: inherit;
  380. vertical-align: baseline;
  381. }
  382. .dialogWindow {
  383. pointer-events: auto;
  384. }
  385. .ruleDiv {
  386. -ms-flex-align: center;
  387. align-items: center;
  388. box-sizing: border-box;
  389. display: -ms-flexbox;
  390. display: flex;
  391. height: 100%;
  392. padding: 75px 30px 20px;
  393. pointer-events: none;
  394. position: fixed;
  395. top: 0;
  396. width: 100%;
  397. z-index: 55;
  398. }
  399. .dialogWindow {
  400. background-color: var(--newCommunityTheme-body);
  401. border: 1px solid var(--newCommunityTheme-line);
  402. border-radius: 4px;
  403. box-shadow: 0 2px 20px 0 rgb(0 0 0 / 30%);
  404. margin: auto;
  405. pointer-events: auto;
  406. z-index: 55;
  407. }
  408. .listWindow {
  409. width: 550px;
  410. position: relative;
  411. }
  412. .ruleHeader {
  413. height: 50px;
  414. border-bottom: 1px solid var(--newRedditTheme-bodyTextAlpha03);
  415. position: relative;
  416. display: -ms-flexbox;
  417. display: flex;
  418. -ms-flex-align: center;
  419. align-items: center;
  420. padding: 0 24px 0 20px;
  421. margin: 0 -24px 0 -20px;
  422. font-weight: 700;
  423. font-size: 14px;
  424. color: var(--newRedditTheme-metaText);
  425. }
  426. .infoIcon {
  427. background: #86848412;
  428. border-radius: 8px;
  429. padding: 10px 16px 10px 12px;
  430. display: -ms-flexbox;
  431. display: flex;
  432. box-sizing: border-box;
  433. margin-top: 16px;
  434. }
  435. .bottomFooter {
  436. box-shadow: 0 -1px 0 var(--newRedditTheme-bodyTextAlpha03);
  437. padding: 20px 0 16px;
  438. min-height: 80px;
  439. display: -ms-flexbox;
  440. display: flex;
  441. box-sizing: border-box;
  442. bottom: 0;
  443. left: 0;
  444. }
  445. .closeIconSVG {
  446. margin-left: auto;
  447. margin-right: -4px;
  448. cursor: pointer;
  449. height: 20px;
  450. padding: 4px;
  451. width: 20px;
  452. fill: var(--newCommunityTheme-actionIcon);
  453. }
  454. .infoIconSVG {
  455. -ms-flex: 0 0 20px;
  456. flex: 0 0 20px;
  457. width: 20px;
  458. margin-right: 12px;
  459. fill: #878a8c;
  460. }
  461. .selectButton:disabled {
  462. opacity: .5;
  463. }
  464. .selectButton {
  465. margin-top: 8px;
  466. -ms-flex: 0 0 150px;
  467. flex: 0 0 150px;
  468. background: var(--newRedditTheme-activeShaded90);
  469. height: 31px;
  470. border-radius: 100px;
  471. color: #fff;
  472. -ms-flex-item-align: end;
  473. align-self: flex-end;
  474. margin-left: auto;
  475. font-size: 12px;
  476. font-weight: 700;
  477. text-transform: uppercase;
  478. letter-spacing: .05em;
  479. outline: none!important;
  480. }
  481. .infoBox {
  482. -ms-flex: 0 1 auto;
  483. flex: 0 1 auto;
  484. font-size: 14px;
  485. line-height: 1.45;
  486. letter-spacing: -.01em;
  487. color: var(--newRedditTheme-metaText);
  488. }
  489. .infoBox a {
  490. color: #24a0ed;
  491. }
  492. button {
  493. background: transparent;
  494. border: none;
  495. color: inherit;
  496. cursor: pointer;
  497. padding: initial;
  498. }
  499. input {
  500. -webkit-writing-mode: horizontal-tb !important;
  501. text-rendering: auto;
  502. color: -internal-light-dark(black, white);
  503. letter-spacing: normal;
  504. word-spacing: normal;
  505. text-transform: none;
  506. text-indent: 0px;
  507. text-shadow: none;
  508. display: inline-block;
  509. text-align: start;
  510. appearance: auto;
  511. background-color: -internal-light-dark(rgb(255, 255, 255), rgb(59, 59, 59));
  512. -webkit-rtl-ordering: logical;
  513. cursor: text;
  514. margin: 0em;
  515. font: 400 13.3333px Arial;
  516. padding: 1px 2px;
  517. border-width: 2px;
  518. border-style: inset;
  519. border-color: -internal-light-dark(rgb(118, 118, 118), rgb(133, 133, 133));
  520. border-image: initial;
  521. }
  522. body {
  523. min-height: calc(100vh - 48px);
  524. line-height: 1;
  525. font-family: IBMPlexSans, Arial, sans-serif;
  526. -webkit-font-smoothing: antialiased;
  527. }
  528. ._3kEv5z1lDKGV8PQ5ijp4Uh {
  529. background-size: 24px 24px;
  530. background-position: 11px 6px;
  531. padding-left: 42px!important;
  532. background-repeat: no-repeat;
  533. }
  534. .title {
  535. margin-top: 16px;
  536. font-size: 16px;
  537. line-height: 1.2;
  538. font-weight: 700;
  539. color: var(--newRedditTheme-bodyText);
  540. }
  541. .fieldSet {
  542. display: -ms-flexbox;
  543. display: flex;
  544. -ms-flex-direction: column;
  545. flex-direction: column;
  546. box-sizing: border-box;
  547. }
  548. .listValue label {
  549. padding: 0 72px 0 20px;
  550. display: -ms-flexbox;
  551. display: flex;
  552. height: 100%;
  553. -ms-flex-align: center;
  554. align-items: center;
  555. cursor: pointer;
  556. font-size: 14px;
  557. font-weight: 700;
  558. color: var(--newRedditTheme-metaText);
  559. position: relative;
  560. }
  561. .listValue {
  562. box-sizing: border-box;
  563. height: 64px;
  564. border-top: 1px solid var(--newRedditTheme-navIconFaded10);
  565. }
  566. .listBox {
  567. margin: 16px -24px 0 -20px;
  568. max-height: 60vh;
  569. min-height: 100px;
  570. overflow: auto;
  571. }
  572. .listValue input {
  573. visibility: hidden;
  574. display: none;
  575. }
  576. .StyledHtml tr {
  577. color: ${Settings.titleColor};
  578. }
  579. .StyledHtml td {
  580. color: ${Settings.textColor};
  581. }
  582. @media (min-width: 768px)
  583. .ThreadViewer__infobarContainer {
  584. display: table;
  585. }
  586. #currentlySelected {
  587. background-color: rgba(121, 121, 121, 0.35);
  588. }
  589. .ruleDiv {
  590. background-color: rgba(26, 26, 27, 0.6);
  591. visibility: hidden;
  592. }
  593. .ModmailPlusPlus__UserDescription {
  594. font-size: 11px;
  595. font-weight: 100;
  596. font-style: italic;
  597. line-height: 16px;
  598. letter-spacing: 0em;
  599. color: var(--color-tone-3);
  600. display: -ms-flexbox;
  601. display: flex;
  602. -ms-flex-pack: center;
  603. justify-content: center;
  604. -ms-flex-align: center;
  605. align-items: center;
  606. margin: 3;
  607. margin-left: 20px;
  608. margin-right: 20px;
  609. }
  610. .ModmailPlusPlus__Title {
  611. font-size: 15px;
  612. font-weight: 700;
  613. color: var(--color-tone-3);
  614. }
  615. `;
  616.  
  617. const styleSheet = document.createElement("style");
  618. styleSheet.type = "text/css";
  619. styleSheet.id = "modmailPlusSheet";
  620. styleSheet.innerText = css;
  621. if($$("#modmailPlusSheet").length == 0)
  622. document.head.appendChild(styleSheet);
  623. };
  624.  
  625. function initializeCore(Settings) {
  626. /* About the core
  627. * - it contains functions required for handling different in-page-actions, such as hiding a div
  628. * - references to variables outside the core and the document will not be recognized
  629. * */
  630. class Core {
  631. ruleListActivator = "<open-rulelist-dialog>";
  632.  
  633. listBoxChanged(responseIndex) {
  634. const message = document.ModmailPlus.responses[responseIndex].content;
  635.  
  636. if(message == this.ruleListActivator)
  637. {
  638. const ruleDiv = document.getElementsByClassName("ruleDiv")[0];
  639. ruleDiv.style.visibility = "visible";
  640. }
  641. else
  642. {
  643. const userVisitingCreatePostPage = document.querySelectorAll(".NewThread").length;
  644.  
  645. const messageBox = userVisitingCreatePostPage
  646. ? document.querySelector(".Textarea, NewThread__message")
  647. : document.getElementById("realTextarea");
  648.  
  649. const response = document.ModmailPlus.responses.find(x => x.content == message);
  650.  
  651. response.replace ? messageBox.value = message : messageBox.value += message;
  652.  
  653. if(response.subject) {
  654. document.querySelector(".NewThread__subject").value = response.subject;
  655. }
  656.  
  657. console.log("[Modmail++] Updated the message: %c" + messageBox.value,"color: orange");
  658. }
  659. }
  660.  
  661. // implement listbox select highlight
  662. selected(element) {
  663. const selectedElem = document.getElementById("currentlySelected");
  664.  
  665. // if an element already selected, reset the id and set its background color to nothing
  666. if(selectedElem)
  667. selectedElem.removeAttribute("id");
  668.  
  669. element.parentElement.id = "currentlySelected";
  670. document.getElementsByClassName("selectButton")[0].disabled = false;
  671. }
  672.  
  673. removeBreaks = text => text.replace(/(\r\n|\n|\r)/gm, "");
  674.  
  675. selectButtonClicked() {
  676. const selectedElem = document.getElementById("currentlySelected");
  677. const userVisitingCreatePostPage = document.querySelectorAll(".NewThread").length;
  678.  
  679. const messageBox = userVisitingCreatePostPage
  680. ? document.querySelector(".Textarea, NewThread__message")
  681. : document.getElementById("realTextarea");
  682.  
  683. if(selectedElem)
  684. {
  685. const selectedRule = document.ModmailPlus.rules[selectedElem.getAttribute('value')];
  686. const ruleName = selectedRule.short_name;
  687. const ruleDescription = selectedRule.description;
  688. const fixedDescription = ruleDescription.replaceAll("\n","\n> ") + '\n\n';
  689. const message = `> [**${ruleName}**]\n>\n> ${fixedDescription}`;
  690.  
  691. const response = document.ModmailPlus.responses.find(x => x.content == this.ruleListActivator);
  692. response.replace // if to replace or add text to the messagebox
  693. ? messageBox.value = message
  694. : messageBox.value += message;
  695.  
  696. console.log("[Modmail++] New messageBox value: %c" + messageBox.value,"color: orange");
  697.  
  698. this.closeIconClicked();
  699. }
  700. }
  701.  
  702. closeIconClicked() {
  703. const ruleDiv = document.querySelector(".ruleDiv");
  704. ruleDiv.style.visibility = "hidden";
  705. }
  706. divertQuoteText() {
  707. console.log("[Modmail++] %cDiverting quote text from original textbox to Modmail++'s", "color: orange");
  708. setTimeout(() => {
  709. const originalForm = document.querySelector(".Textarea, .ThreadViewerReplyForm__replyText");
  710. const originalValue = originalForm.value;
  711. let text = "";
  712.  
  713. if(originalValue.includes("\n\n"))
  714. text = originalValue.split("\n\n").filter(x => x.length > 0).pop();
  715. else
  716. text = originalValue;
  717.  
  718. if(text.indexOf("\n") == 0) text = text.slice(1);
  719.  
  720. if(text && text.includes("said:"))
  721. document.querySelector("#realTextarea").value += text + "\n\n";
  722. }, 50);
  723. }
  724. clearReplyForm() {
  725. setTimeout(() => {
  726. document.getElementById("realTextarea").value = "";
  727. console.log("[Modmail++] Cleared the textarea!");
  728. // set onclick variable again because the button refreshes itself
  729. document.querySelector(".ThreadViewerReplyForm__replyButton")
  730. .setAttribute("onclick", "document.ModmailPlus.Core.clearReplyForm()");
  731. }, 500);
  732. }
  733. };
  734. document.ModmailPlus.Core = new Core;
  735. document.ModmailPlus.responses = Settings.responses;
  736. };
  737.  
  738. async function appendChatProfileIcons() {
  739. const user = await getUserInfo();
  740.  
  741. // icon element
  742. const chatProfileIcon = document.createElement('div');
  743. chatProfileIcon.innerHTML = `<img class="chatProfileIcon" src="${user.data.icon_img}" width="25">`;
  744.  
  745. const authors = $$(".ThreadPreview__author");
  746. if(authors) {
  747. authors.forEach((author, index) => {
  748. // get username (u/xxxxxx)
  749. const name = $$(".Author__text")[index].innerText;
  750.  
  751. // check if there is an icon appended already
  752. const exists = author.childNodes.length == 1 ? false : true;
  753.  
  754. if(removePrefix(name) == recipientUsername() && !exists) // if the username is the user (non-mod)
  755. {
  756. // append the icon next to the username -> [icon] u/username
  757. author.insertBefore(chatProfileIcon.cloneNode(true), author.firstChild);
  758. }
  759. });
  760. }
  761. };
  762.  
  763. async function appendUserInfo(Settings) {
  764. const user = await getUserInfo();
  765.  
  766. if(user)
  767. {
  768. const userDetails = document.createElement('div');
  769. userDetails.id = "CustomMetadata";
  770. userDetails.classList.add("KarmaAndTrophies__userInfoGrid");
  771. userDetails.innerHTML = `
  772. <div class="KarmaAndTrophies__col"><span class="KarmaAndTrophies__count">${user.data.id}</span><span class="KarmaAndTrophies__label">User ID</span></div>
  773. <div class="KarmaAndTrophies__col"><span class="KarmaAndTrophies__count">${user.data.verified ? "🟢" : "🔴"}</span><span class="KarmaAndTrophies__label">Verified</span></div>
  774. <div class="KarmaAndTrophies__col"><span class="KarmaAndTrophies__count">${user.data.is_employee ? "🟢" : "🔴"}</span><span class="KarmaAndTrophies__label">Reddit Employee</span></div>
  775. <div class="KarmaAndTrophies__col"><span class="KarmaAndTrophies__count">${user.data.subreddit.over_18 ? "🟢" : "🔴"}</span><span class="KarmaAndTrophies__label">NSFW Profile</span></div>
  776. <div class="KarmaAndTrophies__col"><span class="KarmaAndTrophies__count">${user.data.subreddit.accept_followers ? "🟢" : "🔴"}</span><span class="KarmaAndTrophies__label">Accept Followers</span></div>
  777. <div class="KarmaAndTrophies__col"><span class="KarmaAndTrophies__count">${user.data.subreddit.hide_from_robots ? "🟢" : "🔴"}</span><span class="KarmaAndTrophies__label">Hide from bots</span></div>`;
  778. const links = document.createElement('div');
  779. links.classList.add("KarmaAndTrophies__userInfoGrid");
  780. links.innerHTML = `
  781. <div style="margin-bottom: 10px;">
  782. <a class="InfoBar__recent" href="https://redditmetis.com/user/${user.data.name}" target="_blank">RedditMetis ⧉</a>
  783. <a class="InfoBar__recent" href="https://www.reddit.com/search?q=${user.data.name}" target="_blank">Reddit Search ⧉</a>
  784. <a class="InfoBar__recent" href="https://www.google.com/search?q=%22${user.data.name}%22" target="_blank">Google Search ⧉</a>
  785. <div>`;
  786. const getTitleElement = titleText => {
  787. const title = document.createElement('h1');
  788. title.classList.add("KarmaAndTrophies__userInfoGrid");
  789. title.classList.add("ModmailPlusPlus__Title");
  790. title.innerText = titleText;
  791. return title;
  792. };
  793. if(!$("#CustomMetadata")) {
  794. const userCard = $(".ModIdCard");
  795. if(userCard && user.data.subreddit.public_description && !$(".ModmailPlusPlus__UserDescription")) {
  796. const description = document.createElement('h1');
  797. description.classList.add("ModmailPlusPlus__UserDescription");
  798. description.innerText = '“' + sanitize(user.data.subreddit.public_description) + '”';
  799. userCard.insertBefore(description, $(".ModIdCard__UserProfileLink"));
  800. }
  801. const banStatus = $(".KarmaAndTrophies__BanStatus");
  802. if(banStatus) {
  803. $(".KarmaAndTrophies").insertBefore(getTitleElement("Additional Information"), banStatus);
  804. $(".KarmaAndTrophies").insertBefore(userDetails, banStatus);
  805. $(".KarmaAndTrophies").insertBefore(getTitleElement("Additional Links"), banStatus);
  806. $(".KarmaAndTrophies").insertBefore(links, banStatus);
  807. } else {
  808. $(".KarmaAndTrophies").appendChild(getTitleElement("Additional Information"));
  809. $(".KarmaAndTrophies").appendChild(userDetails);
  810. $(".KarmaAndTrophies").appendChild(getTitleElement("Additional Links"));
  811. $(".KarmaAndTrophies").appendChild(links);
  812. }
  813. }
  814. const observer = new MutationObserver(() => {
  815. const overview = document.querySelector(".NewInfoBar__overviewContainer");
  816. if(overview) {
  817. observer.disconnect();
  818. appendUserInfo(Settings);
  819. }
  820. });
  821. observer.observe(document.querySelector(".NewInfoBar"), {
  822. attributes: false, characterData: false, childList: true
  823. });
  824. }
  825. };
  826.  
  827. function replaceReplyForm(Settings) {
  828. // hide the original replyform textarea
  829. $(".ThreadViewerReplyForm__replyText").style.cssText += 'display: none';
  830.  
  831. // create and append a new replyform textarea
  832. const newReplyForm = document.createElement("textarea");
  833. newReplyForm.setAttribute('class', 'Textarea ThreadViewerReplyForm__replyText ');
  834. newReplyForm.setAttribute('id', 'realTextarea');
  835. newReplyForm.setAttribute('name', 'body');
  836. newReplyForm.setAttribute('placeholder', `${Settings.placeholderMessage}`);
  837. $(".ThreadViewerReplyForm").insertBefore(newReplyForm, $(".ThreadViewerReplyForm__replyFooter"));
  838.  
  839. // make the reply button clear the new replyform
  840. $(".ThreadViewerReplyForm__replyButton").setAttribute("onclick", "document.ModmailPlus.Core.clearReplyForm()");
  841. };
  842.  
  843. async function appendResponseTemplateBox(Settings) {
  844. const responseTemplateElement =
  845. `<h2 class="dataTitle">Response Templates</h2>
  846. <select id="responseListbox" onchange="document.ModmailPlus.Core.listBoxChanged(this.value);" onfocus="this.selectedIndex = -1;"/>
  847. <option selected disabled hidden>Select a template</option>
  848. <span class="focus"></span>`;
  849.  
  850. const responseTemplateParent = document.createElement('div');
  851. responseTemplateParent.classList.add("select", "customResponseBox");
  852. responseTemplateParent.innerHTML = responseTemplateElement;
  853.  
  854. const userVisitingCreatePostPage = document.querySelectorAll(".NewThread").length;
  855.  
  856. if(typeof Settings.responses == "object" && Settings.responses.length) // if the responses variable exists and has responses
  857. {
  858. if(userVisitingCreatePostPage) // user visited mod.reddit.com/mail/create
  859. {
  860. // append the template box to the site
  861. $(".NewThread__fields").prepend(responseTemplateParent);
  862. $(".NewThread__fields").insertBefore($(".customResponseBox"), $(".Textarea, .NewThread__message"));
  863. }
  864. else // user visited modmail chat
  865. {
  866. // append the template box to the site
  867. $(".ThreadViewer__replyContainer").prepend(responseTemplateParent);
  868. $(".ThreadViewer__replyContainer").insertBefore($(".ThreadViewer__typingIndicator"), $(".select")); // append typing indicator before listbox
  869. }
  870. }
  871. // populates the response template listbox
  872. function populateListbox(listBoxId) {
  873. const listBox = $(listBoxId);
  874.  
  875. if(typeof Settings.responses == "object" && Settings.responses.length) // if the responses variable exists and has responses
  876. {
  877. Settings.responses.forEach((response, i) => {
  878. const responseSubreddit = keepPrefix(response.subreddit.toLowerCase(), true);
  879. const currentSubreddit = keepPrefix(Settings.subTag.toLowerCase(), true);
  880. const sameSubreddit = currentSubreddit == responseSubreddit;
  881.  
  882. if(sameSubreddit || response.subreddit.length == 0)
  883. {
  884. if(userVisitingCreatePostPage)
  885. {
  886. listBox.options[listBox.options.length] = new Option(response.name, i);
  887. }
  888. else
  889. {
  890. if(!response.subject)
  891. {
  892. listBox.options[listBox.options.length] = new Option(response.name, i);
  893. }
  894. }
  895. }
  896. });
  897. }
  898. };
  899.  
  900. populateListbox("#responseListbox"); // add all the responses to the response template listbox
  901.  
  902. // creates and returns a list element
  903. function makeListValue(index, rule) {
  904. document.ModmailPlus.rules.push(rule);
  905. return `<div value='${index}' class="listValue">
  906. <input onclick="document.ModmailPlus.Core.selected(this)" name="subredditRule" id='${"input_" + index}' type="radio">
  907. <label for='${"input_" + index}'>${rule.short_name}</label>
  908. </div>`;
  909. };
  910.  
  911. const ruleObj = await getRules(Settings);
  912.  
  913. if(ruleObj)
  914. {
  915. $$(".subredditRuleList").forEach(elem => elem.remove()); // remove all subredditRuleList elements
  916.  
  917. let listContent = "";
  918.  
  919. ruleObj.rules.forEach((rule, index) => {
  920. listContent += makeListValue(index, rule)
  921. });
  922.  
  923. // (Append) Div ruleList element to body
  924. const ruleList = document.createElement('div');
  925. ruleList.classList.add("subredditRuleList");
  926. ruleList.innerHTML = `<div class="ruleDiv">
  927. <div aria-modal="true" class="dialogWindow" role="dialog" tabindex="-1">
  928. <div class="listWindow">
  929. <div class="ruleList">
  930. <div class="ruleHeader">Select a rule<svg onclick="document.ModmailPlus.Core.closeIconClicked()" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" class="closeIconSVG"><polygon fill="inherit" points="11.649 9.882 18.262 3.267 16.495 1.5 9.881 8.114 3.267 1.5 1.5 3.267 8.114 9.883 1.5 16.497 3.267 18.264 9.881 11.65 16.495 18.264 18.262 16.497"></polygon></svg></div>
  931. <fieldset class="fieldSet">
  932. <div class="title"><span>Which community rule did the user violate?</span></div>
  933. <div class="listBox">
  934. ${listContent}
  935. </div>
  936. <div class="infoIcon"><svg class="infoIconSVG" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g><path d="M10,8.5 C10.553,8.5 11,8.948 11,9.5 L11,13.5 C11,14.052 10.553,14.5 10,14.5 C9.447,14.5 9,14.052 9,13.5 L9,9.5 C9,8.948 9.447,8.5 10,8.5 Z M10.7002,5.79 C10.8012,5.89 10.8702,6 10.9212,6.12 C10.9712,6.24 11.0002,6.37 11.0002,6.5 C11.0002,6.57 10.9902,6.63 10.9802,6.7 C10.9712,6.76 10.9502,6.82 10.9212,6.88 C10.9002,6.94 10.8702,7 10.8302,7.05 C10.7902,7.11 10.7502,7.16 10.7002,7.21 C10.6602,7.25 10.6102,7.29 10.5512,7.33 C10.5002,7.37 10.4402,7.4 10.3812,7.42 C10.3202,7.45 10.2612,7.47 10.1902,7.48 C10.1312,7.49 10.0602,7.5 10.0002,7.5 C9.7402,7.5 9.4802,7.39 9.2902,7.21 C9.1102,7.02 9.0002,6.77 9.0002,6.5 C9.0002,6.37 9.0302,6.24 9.0802,6.12 C9.1312,5.99 9.2002,5.89 9.2902,5.79 C9.5202,5.56 9.8702,5.46 10.1902,5.52 C10.2612,5.53 10.3202,5.55 10.3812,5.58 C10.4402,5.6 10.5002,5.63 10.5512,5.67 C10.6102,5.71 10.6602,5.75 10.7002,5.79 Z M10,16 C6.691,16 4,13.309 4,10 C4,6.691 6.691,4 10,4 C13.309,4 16,6.691 16,10 C16,13.309 13.309,16 10,16 M10,2 C5.589,2 2,5.589 2,10 C2,14.411 5.589,18 10,18 C14.411,18 18,14.411 18,10 C18,5.589 14.411,2 10,2"></path></g></svg>
  937. <div class="infoBox">
  938. <p><span>Not sure? </span><a href="https://www.reddit.com/${keepPrefix(Settings.subTag)}/about/rules" target="_blank" rel="noopener noreferrer">Read ${Settings.subTag}'s rules</a></p>
  939. </div>
  940. </div>
  941. <footer class="bottomFooter"><button type="button" disabled="" onclick="document.ModmailPlus.Core.selectButtonClicked()" class="selectButton">Select</button></footer>
  942. </fieldset>
  943. </div>
  944. </div>
  945. </div>
  946. </div>`;
  947. $("body").appendChild(ruleList);
  948. }
  949. };
  950.  
  951. function fixQuoteButtons() {
  952. /* On click, do this
  953. 1) take the text from the original form
  954. 2) split it by two new lines
  955. 3) take the last result (last quoted message)
  956. 4) paste the last result to the new form
  957. */
  958.  
  959. $$(".Message__quote").forEach(elem => {
  960. if(!elem.getAttribute("onclick"))
  961. elem.setAttribute("onclick", "document.ModmailPlus.Core.divertQuoteText()");
  962. });
  963. };
  964. function handleCreateMessagePage() {
  965. // e.g. wait for an input change, then update the template strings to have that subreddit or username
  966. function updateResponseTemplate(isPostFrom, lastValue, newValue) {
  967. const defaultValue = isPostFrom
  968. ? "r/subreddit"
  969. : "u/undefined";
  970. document.ModmailPlus.responses.forEach(response => {
  971. // update subject string
  972. if(response.subject) {
  973. response.subject = response.subject.replaceAll(
  974. lastValue || defaultValue,
  975. newValue
  976. );
  977. }
  978.  
  979. // update content string
  980. response.content = response.content.replaceAll(
  981. lastValue || defaultValue,
  982. newValue
  983. );
  984. });
  985. }
  986. (() => {
  987. let lastUsername = null;
  988.  
  989. // Handle change of "To User"
  990. const toUser = document.querySelector(".Radio__input[value=user]");
  991.  
  992. if(toUser) {
  993. toUser.onclick = () => {
  994. const waitForTextbox = setInterval(() => {
  995. const newThreadTextbox = document.querySelector(".NewThread__username");
  996.  
  997. if(newThreadTextbox) {
  998. clearInterval(waitForTextbox);
  999.  
  1000. newThreadTextbox.onchange = e => {
  1001. const username = "u/" + e.target.value;
  1002.  
  1003. updateResponseTemplate(false, lastUsername, username);
  1004.  
  1005. lastUsername = username;
  1006. }
  1007. }
  1008. }, 100);
  1009. }
  1010. }
  1011.  
  1012. // Handle change of "To Subreddit"
  1013. const toSubreddit = document.querySelector(".Radio__input[value=subreddit]");
  1014.  
  1015. if(toSubreddit) {
  1016. toSubreddit.onclick = () => {
  1017. const waitForTextbox = setInterval(() => {
  1018. const newThreadTextbox = document.querySelector(".NewThread__subreddit");
  1019.  
  1020. if(newThreadTextbox) {
  1021. clearInterval(waitForTextbox);
  1022.  
  1023. newThreadTextbox.onchange = e => {
  1024. const subreddit = "r/" + e.target.value;
  1025.  
  1026. updateResponseTemplate(false, lastUsername, subreddit);
  1027.  
  1028. lastUsername = subreddit;
  1029. }
  1030. }
  1031. }, 100);
  1032. }
  1033. }
  1034. })();
  1035. (() => {
  1036. // Handle change of "Post from"
  1037. const srName = document.querySelector("[name=srName]");
  1038.  
  1039. if(srName) {
  1040. let lastSubreddit = null;
  1041.  
  1042. const postToChanges = setInterval(() => {
  1043. const subreddit = "r/" + srName.value;
  1044.  
  1045. if(subreddit != lastSubreddit && subreddit != 'r/') {
  1046. updateResponseTemplate(true, lastSubreddit, subreddit);
  1047.  
  1048. lastSubreddit = subreddit;
  1049. }
  1050. }, 500);
  1051. }
  1052. })();
  1053. };
  1054.  
  1055. const __main__ = async () => {
  1056. console.log("[Modmail++] %cMain function ran!", "color: grey");
  1057. const Settings = new __settings__();
  1058. document.ModmailPlus = {};
  1059. document.ModmailPlus.rules = [];
  1060.  
  1061. // These will be executed in any page //
  1062. if(!document.ModmailPlus.length)
  1063. initializeCore(Settings);
  1064. if(Settings.enableCustomResponses && !$("#responseListbox"))
  1065. appendResponseTemplateBox(Settings);
  1066. applyCSS(Settings);
  1067. ////////////////////////////////////////
  1068. if(!$(".NewThread")) // execute in chat page only
  1069. {
  1070. appendUserInfo(Settings); // if the element already exists will be checked before appending
  1071. if(Settings.chatProfileIcons && !$(".chatProfileIcon"))
  1072. appendChatProfileIcons();
  1073. if(!$("#realTextarea"))
  1074. replaceReplyForm(Settings);
  1075. fixQuoteButtons();
  1076. }
  1077. else // execute in create message page only
  1078. {
  1079. handleCreateMessagePage();
  1080. }
  1081.  
  1082. console.log("[Modmail++] %cLoaded!", "color: lime");
  1083. console.log("[Modmail++]", document.ModmailPlus);
  1084. };
  1085.  
  1086. let URLChangeDetectorActive = false;
  1087.  
  1088. // this is a hot mess, please help me
  1089. setInterval (() => {
  1090. if(this.lastPathStr != location.pathname)
  1091. {
  1092. this.lastPathStr = location.pathname;
  1093.  
  1094. console.log("[Modmail++] %cNew page detected!", "color: gold");
  1095.  
  1096. URLChangeDetectorActive = true;
  1097.  
  1098. const waitForElements = setInterval (() => {
  1099. if($(".NoThreadMessage__generic") && URLChangeDetectorActive) // add confetti explosion if no mail
  1100. {
  1101. clearInterval(waitForElements);
  1102. URLChangeDetectorActive = false;
  1103.  
  1104. console.log("[Modmail++] %cNo modmail!", "color: lime");
  1105.  
  1106. party.confetti($(".NoThreadMessage__generic"), {
  1107. count: 15,
  1108. spread: 50
  1109. });
  1110. }
  1111.  
  1112. if($(".NewInfoBar") && URLChangeDetectorActive) // user is on modmail "chat" page
  1113. {
  1114. clearInterval(waitForElements);
  1115. URLChangeDetectorActive = false;
  1116.  
  1117. if($("body") && !$("#CustomMetadata"))
  1118. __main__();
  1119. }
  1120. if($(".NewThread") && URLChangeDetectorActive) // user is on the create new message page
  1121. {
  1122. clearInterval(waitForElements);
  1123. URLChangeDetectorActive = false;
  1124. __main__();
  1125. }
  1126. }, 5);
  1127. }
  1128. }, 100);
  1129. })();