[Reddit] Modmail++

Additional tools and information to Reddit's Modmail

目前為 2022-05-02 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name [Reddit] Modmail++
  3. // @namespace HKR
  4. // @match https://mod.reddit.com/mail/*
  5. // @grant none
  6. // @version 3.1
  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. // @require https://greasyfork.org/scripts/21927-arrive-js/code/arrivejs.js#sha256-rm9QEHHY91BwX6A/HHIkqMjpw/tHEuI8X2VBe4K8RIw=
  13. // ==/UserScript==
  14.  
  15. (() => {
  16. console.log("[Modmail++] %cScript started!", "color: green");
  17.  
  18. const $ = document.querySelector.bind(document);
  19. const $$ = document.querySelectorAll.bind(document);
  20.  
  21. // returns a random item from array
  22. const randItem = itemArr => itemArr[Math.floor(Math.random() * itemArr.length)];
  23.  
  24. // removes the Reddit prefix
  25. const removePrefix = username => ["r/","u/"].some(tag => username.includes(tag)) ? username.slice(2) : username;
  26.  
  27. // adds the Reddit prefix if nonexistant
  28. const keepPrefix = (username, subreddit) => ["r/","u/"].some(tag => username.includes(tag)) ? username : subreddit ? `r/${username}` : `u/${username}`;
  29.  
  30. function __settings__() {
  31. "use strict";
  32.  
  33. this.subTag = $(".ThreadTitle__community")?.href?.slice(23) || "r/subreddit"; //Format r/subreddit
  34. this.userTag = "u/" + $(".InfoBar__username")?.innerText || "u/username"; //Format u/username
  35. this.modmail = `[modmail](https://www.reddit.com/message/compose?to=/${keepPrefix(this.subTag, true)})`;
  36. this.rules = `https://www.reddit.com/${keepPrefix(this.subTag,true)}/about/rules`;
  37.  
  38. /* Responses - Edit to your own liking, remove whatever you don't like!
  39. - name | The name of the response that will show on the listbox. (Example value: "Hello!")
  40. - replace | Replace all messagebox text if true, otherwise just add. (Example value: true)
  41. - subreddit | Visible only while on this subreddit's modmail. (Example value: "r/subreddit")
  42. - content | This text will be added to the messagebox once selected (Example value: "Hello world!")*/
  43. this.responses = [
  44. {
  45. "name":"Default Approved",
  46. "replace":true,
  47. "subreddit":"",
  48. "content":`Hey, approved the post!`
  49. },
  50. {
  51. "name":"Default Rule Broken",
  52. "replace":true,
  53. "subreddit":"",
  54. "content":`Your post broke our [rules](${this.rules}).\n\nThe action will not be reverted.`
  55. },
  56. {
  57. "name":"Add Rule Description",
  58. "replace":false,
  59. "subreddit":"",
  60. "content":`<open-rulelist-dialog>`
  61. },
  62. {
  63. "name":"Add Greetings",
  64. "replace":false,
  65. "subreddit":"",
  66. "content":`${randItem(["Greetings","Hello","Hi"])} ${this.userTag},\n\n`
  67. },
  68. {
  69. "name":"Add Subreddit Mention",
  70. "replace":false,
  71. "subreddit":"",
  72. "content":`${this.subTag}`
  73. },
  74. {
  75. "name":"Add User Mention",
  76. "replace":false,
  77. "subreddit":"",
  78. "content":`${this.userTag}`
  79. },
  80. {
  81. "name":"Add Modmail Link",
  82. "replace":false,
  83. "subreddit":"",
  84. "content":`${this.modmail}`
  85. },
  86. {
  87. "name":"Add Karma Link",
  88. "replace":false,
  89. "subreddit":"",
  90. "content":`[karma](https://reddit.zendesk.com/hc/en-us/articles/204511829-What-is-karma-)`
  91. },
  92. {
  93. "name":"Add Shadowban Link",
  94. "replace":false,
  95. "subreddit":"",
  96. "content":`[shadowbanned](https://www.reddit.com/r/ShadowBan/comments/8a2gpk/an_unofficial_guide_on_how_to_avoid_being/)`
  97. },
  98. {
  99. "name":"Add Content Policy",
  100. "replace":false,
  101. "subreddit":"",
  102. "content":`[Content Policy](https://www.redditinc.com/policies/content-policy)`
  103. },
  104. {
  105. "name":"Add User Agreement",
  106. "replace":false,
  107. "subreddit":"",
  108. "content":`[User Agreement](https://www.redditinc.com/policies/user-agreement)`
  109. },
  110. {
  111. "name":"Add Reddiquette",
  112. "replace":false,
  113. "subreddit":"",
  114. "content":`[Reddiquette](https://reddit.zendesk.com/hc/en-us/articles/205926439-Reddiquette)`
  115. },
  116. {
  117. "name":"Add Admin Modmail",
  118. "replace":false,
  119. "subreddit":"",
  120. "content":`[Admins](https://old.reddit.com/message/compose?to=%2Fr%2Freddit.com)`
  121. },
  122. {
  123. "name":"Add Rickroll",
  124. "replace":false,
  125. "subreddit":"",
  126. "content":`[link](https://www.youtube.com/watch?v=dQw4w9WgXcQ)`
  127. }
  128. ];
  129.  
  130. const themeMode = $$(".theme-dark").length ? true : false;
  131. this.textColor = themeMode ? "#757575" : "#6e6e6e"; // dark hex : light hex
  132. this.titleColor = themeMode ? "#a7a7a7" : "#2c2c2c"; // dark hex : light hex
  133. this.listBoxColor = themeMode ? "#242424" : "#f1f3f5"; // dark hex : light hex
  134.  
  135. this.dataColor = "#0079d3"; // data (numbers etc.) color
  136.  
  137. this.enableCustomResponses = true; // if to append the custom response box
  138.  
  139. this.chatProfileIcons = true; // if to append chat profile icons
  140.  
  141. this.placeholderMessage = randItem([
  142. "Message...",
  143. "Look, a bird! Message...",
  144. "What have you been up to today? Message...",
  145. "Beautiful day, isn't it? Message...",
  146. "Was the weather nice? Message...",
  147. "You look good today! Message...",
  148. "What dreams did you see last night? Message...",
  149. "What did you do today? Message...",
  150. "What did you eat today? Message...",
  151. "Have you drank enough water? Message...",
  152. "Remember to stretch! Message...",
  153. "≖‿≖ I live inside of your walls. Message...",
  154. "(✿◠‿◠) Message...",
  155. ]);
  156. }
  157.  
  158. const Get = async (url) => {
  159. let response = await fetch(url);
  160.  
  161. if (!response.ok) {
  162. throw new Error(`HTTP error! status: ${response.status}`);
  163. }
  164.  
  165. let text = await response.text();
  166. return text;
  167. };
  168.  
  169. // adds a zero suffix if x < 10
  170. const fixnumber = number => number < 10 ? "0" + number : number;
  171.  
  172. // returns a date string from UNIX timestamp
  173. const unixToDate = UNIX_timestamp => {
  174. const d = new Date(UNIX_timestamp * 1000);
  175. const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  176.  
  177. let year = d.getFullYear(),
  178. monthNum = d.getMonth() + 1,
  179. month = months[d.getMonth()],
  180. date = d.getDate(),
  181. hour = fixnumber(d.getHours()),
  182. min = fixnumber(d.getMinutes()),
  183. sec = fixnumber(d.getSeconds());
  184.  
  185. return `${date}.${monthNum}.${year} ${hour}:${min}:${sec}`; // (DD/MM/YY HH/MM/SS)
  186. };
  187.  
  188. // returns a string without evil HTML elements
  189. const sanitize = evilstring => {
  190. const decoder = document.createElement('div');
  191. decoder.innerHTML = evilstring;
  192. return decoder.textContent;
  193. };
  194.  
  195. // add a link to the Modmail text and change its name to Modmail++
  196. const sidebarTitle = $(".Sidebar__titleMessage");
  197. sidebarTitle.setAttribute("onclick","window.open('https://github.com/Hakorr/Userscripts/tree/main/Reddit.com/ModmailExtraInfo')");
  198. sidebarTitle.setAttribute("style","cursor: pointer");
  199. sidebarTitle.innerText = "Modmail++";
  200.  
  201. // apply the custom css
  202. const applyCSS = (Settings) => {
  203. //Took advice for the listbox CSS from moderncss.dev/custom-select-styles-with-pure-css, thanks!
  204. const css = `.profileIcon:hover {
  205. -ms-transform: scale(6);
  206. -webkit-transform: scale(6);
  207. transform: scale(6);
  208. }
  209. .profileIcon {
  210. position: relative;
  211. bottom: 4px;
  212. margin-bottom: 10px;
  213. float: left; border-radius: 50%;
  214. transition: transform .1s;
  215. }
  216. .InfoBar__recentsNone {
  217. color: #6e6e6e;
  218. }
  219. .InfoBar__metadata, .InfoBar__recents {
  220. margin: 6px 0;
  221. margin-left: 10px;
  222. }
  223. .value {
  224. color: ${Settings.dataColor};
  225. }
  226. .InfoBar__banText {
  227. padding-bottom: 15px;
  228. }
  229. .InfoBar__username, .InfoBar__username:visited {
  230. padding-left: 10px;
  231. }
  232. .dataText {
  233. color: ${Settings.textColor};
  234. font-size: 13px;
  235. padding-left: 10px;
  236. }
  237. .dataTitle {
  238. color: ${Settings.titleColor};
  239. font-size: 15px;
  240. margin-bottom: 3px;
  241. margin-top: 5px;
  242. }
  243. .responseListbox {
  244. width: 50%;
  245. cursor: pointer;
  246. }
  247. :root {
  248. --select-border: #0079d3;
  249. --select-focus: blue;
  250. --select-arrow: var(--select-border);
  251. }
  252. *,
  253. *::before,
  254. *::after {
  255. box-sizing: border-box;
  256. }
  257. select {
  258. appearance: none;
  259. background-color: ${Settings.listBoxColor};
  260. color: ${Settings.textColor};
  261. border: none;
  262. padding: 0 1em 0 0;
  263. margin: 0;
  264. width: 100%;
  265. cursor: pointer;
  266. font-family: inherit;
  267. font-size: inherit;
  268. line-height: inherit;
  269. outline: none;
  270. position: relative;
  271. }
  272. .select {
  273. width: 100%;
  274. min-width: 15ch;
  275. max-width: 30ch;
  276. border: 1px solid var(--select-border);
  277. border-radius: 0.25em;
  278. padding: 0.3em 0.4em;
  279. font-size: 0.9rem;
  280. line-height: 1.1;
  281. background-color: ${Settings.listBoxColor};
  282. margin-bottom: 15px;
  283. }
  284. select::-ms-expand {
  285. display: none;
  286. }
  287. option {
  288. white-space: normal;
  289. outline-color: var(--select-focus);
  290. }
  291. select:focus + .focus {
  292. position: absolute;
  293. top: -1px;
  294. left: -1px;
  295. right: -1px;
  296. bottom: -1px;
  297. border: 2px solid var(--select-focus);
  298. border-radius: inherit;
  299. }
  300. .Author__text {
  301. padding: 6px 0;
  302. }
  303. .chatProfileIcon {
  304. margin-right: 7px;
  305. transition: transform .1s;
  306. border-radius: 50%;
  307. }
  308. .App__page {
  309. background: var(--color-tone-8);
  310. }
  311. ::-webkit-scrollbar {
  312. width: 10px;
  313. }
  314. ::-webkit-scrollbar-track {
  315. background: ${Settings.listBoxColor};
  316. }
  317. ::-webkit-scrollbar-thumb {
  318. background: #888;
  319. }
  320. ::-webkit-scrollbar-thumb:hover {
  321. background: #555;
  322. }
  323. .subredditRuleList {
  324. --newRedditTheme-bodyText: ${Settings.titleColor};
  325. --newRedditTheme-metaText: ${Settings.textColor};
  326. --newRedditTheme-navIconFaded10: rgba(215,218,220,0.1);
  327. --newRedditTheme-actionIconTinted80: #9a9b9c;
  328. --newRedditTheme-activeShaded90: #006cbd;
  329. --newRedditTheme-actionIconAlpha20: rgba(129,131,132,0.2);
  330. --newCommunityTheme-actionIcon: #818384;
  331. --newRedditTheme-bodyTextAlpha03: ${Settings.listBoxColor};
  332. --newRedditTheme-navIcon: #D7DADC;
  333. --newCommunityTheme-line: #343536;
  334. --newCommunityTheme-body: #1A1A1B;
  335. }
  336. .ruleList {
  337. padding: 0 24px 0 20px;
  338. background: var(--newRedditTheme-bodyTextAlpha03);
  339. max-height: 100%;
  340. }
  341. 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 {
  342. margin: 0;
  343. padding: 0;
  344. border: 0;
  345. font-size: 100%;
  346. font: inherit;
  347. vertical-align: baseline;
  348. }
  349. .dialogWindow {
  350. pointer-events: auto;
  351. }
  352. .ruleDiv {
  353. -ms-flex-align: center;
  354. align-items: center;
  355. box-sizing: border-box;
  356. display: -ms-flexbox;
  357. display: flex;
  358. height: 100%;
  359. padding: 75px 30px 20px;
  360. pointer-events: none;
  361. position: fixed;
  362. top: 0;
  363. width: 100%;
  364. z-index: 55;
  365. }
  366. .dialogWindow {
  367. background-color: var(--newCommunityTheme-body);
  368. border: 1px solid var(--newCommunityTheme-line);
  369. border-radius: 4px;
  370. box-shadow: 0 2px 20px 0 rgb(0 0 0 / 30%);
  371. margin: auto;
  372. pointer-events: auto;
  373. z-index: 55;
  374. }
  375. .listWindow {
  376. width: 550px;
  377. position: relative;
  378. }
  379. .ruleHeader {
  380. height: 50px;
  381. border-bottom: 1px solid var(--newRedditTheme-bodyTextAlpha03);
  382. position: relative;
  383. display: -ms-flexbox;
  384. display: flex;
  385. -ms-flex-align: center;
  386. align-items: center;
  387. padding: 0 24px 0 20px;
  388. margin: 0 -24px 0 -20px;
  389. font-weight: 700;
  390. font-size: 14px;
  391. color: var(--newRedditTheme-metaText);
  392. }
  393. .infoIcon {
  394. background: #86848412;
  395. border-radius: 8px;
  396. padding: 10px 16px 10px 12px;
  397. display: -ms-flexbox;
  398. display: flex;
  399. box-sizing: border-box;
  400. margin-top: 16px;
  401. }
  402. .bottomFooter {
  403. box-shadow: 0 -1px 0 var(--newRedditTheme-bodyTextAlpha03);
  404. padding: 20px 0 16px;
  405. min-height: 80px;
  406. display: -ms-flexbox;
  407. display: flex;
  408. box-sizing: border-box;
  409. bottom: 0;
  410. left: 0;
  411. }
  412. .closeIconSVG {
  413. margin-left: auto;
  414. margin-right: -4px;
  415. cursor: pointer;
  416. height: 20px;
  417. padding: 4px;
  418. width: 20px;
  419. fill: var(--newCommunityTheme-actionIcon);
  420. }
  421. .infoIconSVG {
  422. -ms-flex: 0 0 20px;
  423. flex: 0 0 20px;
  424. width: 20px;
  425. margin-right: 12px;
  426. fill: #878a8c;
  427. }
  428. .selectButton:disabled {
  429. opacity: .5;
  430. }
  431. .selectButton {
  432. margin-top: 8px;
  433. -ms-flex: 0 0 150px;
  434. flex: 0 0 150px;
  435. background: var(--newRedditTheme-activeShaded90);
  436. height: 31px;
  437. border-radius: 100px;
  438. color: #fff;
  439. -ms-flex-item-align: end;
  440. align-self: flex-end;
  441. margin-left: auto;
  442. font-size: 12px;
  443. font-weight: 700;
  444. text-transform: uppercase;
  445. letter-spacing: .05em;
  446. outline: none!important;
  447. }
  448. .infoBox {
  449. -ms-flex: 0 1 auto;
  450. flex: 0 1 auto;
  451. font-size: 14px;
  452. line-height: 1.45;
  453. letter-spacing: -.01em;
  454. color: var(--newRedditTheme-metaText);
  455. }
  456. .infoBox a {
  457. color: #24a0ed;
  458. }
  459. button {
  460. background: transparent;
  461. border: none;
  462. color: inherit;
  463. cursor: pointer;
  464. padding: initial;
  465. }
  466. input {
  467. -webkit-writing-mode: horizontal-tb !important;
  468. text-rendering: auto;
  469. color: -internal-light-dark(black, white);
  470. letter-spacing: normal;
  471. word-spacing: normal;
  472. text-transform: none;
  473. text-indent: 0px;
  474. text-shadow: none;
  475. display: inline-block;
  476. text-align: start;
  477. appearance: auto;
  478. background-color: -internal-light-dark(rgb(255, 255, 255), rgb(59, 59, 59));
  479. -webkit-rtl-ordering: logical;
  480. cursor: text;
  481. margin: 0em;
  482. font: 400 13.3333px Arial;
  483. padding: 1px 2px;
  484. border-width: 2px;
  485. border-style: inset;
  486. border-color: -internal-light-dark(rgb(118, 118, 118), rgb(133, 133, 133));
  487. border-image: initial;
  488. }
  489. body {
  490. min-height: calc(100vh - 48px);
  491. line-height: 1;
  492. font-family: IBMPlexSans, Arial, sans-serif;
  493. -webkit-font-smoothing: antialiased;
  494. }
  495. ._3kEv5z1lDKGV8PQ5ijp4Uh {
  496. background-size: 24px 24px;
  497. background-position: 11px 6px;
  498. padding-left: 42px!important;
  499. background-repeat: no-repeat;
  500. }
  501. .title {
  502. margin-top: 16px;
  503. font-size: 16px;
  504. line-height: 1.2;
  505. font-weight: 700;
  506. color: var(--newRedditTheme-bodyText);
  507. }
  508. .fieldSet {
  509. display: -ms-flexbox;
  510. display: flex;
  511. -ms-flex-direction: column;
  512. flex-direction: column;
  513. box-sizing: border-box;
  514. }
  515. .listValue label {
  516. padding: 0 72px 0 20px;
  517. display: -ms-flexbox;
  518. display: flex;
  519. height: 100%;
  520. -ms-flex-align: center;
  521. align-items: center;
  522. cursor: pointer;
  523. font-size: 14px;
  524. font-weight: 700;
  525. color: var(--newRedditTheme-metaText);
  526. position: relative;
  527. }
  528. .listValue {
  529. box-sizing: border-box;
  530. height: 64px;
  531. border-top: 1px solid var(--newRedditTheme-navIconFaded10);
  532. }
  533. .listBox {
  534. margin: 16px -24px 0 -20px;
  535. max-height: 60vh;
  536. min-height: 100px;
  537. overflow: auto;
  538. }
  539. .listValue input {
  540. visibility: hidden;
  541. display: none;
  542. }
  543. .StyledHtml tr {
  544. color: ${Settings.titleColor};
  545. }
  546. .StyledHtml td {
  547. color: ${Settings.textColor};
  548. }
  549. #CustomMetadata {
  550. font-size: 13px;
  551. line-height: 1.5;
  552. }
  553. .CustomInfoBar__username {
  554. padding-left: 10px;
  555. font-size: 18px;
  556. line-height: 1;
  557. padding-bottom: 4px;
  558. overflow: hidden;
  559. white-space: nowrap;
  560. text-overflow: ellipsis;
  561. text-decoration: none;
  562. color: var(--color-tone-1);
  563. }
  564. .CustomSeperator {
  565. position: relative;
  566. border-top: 1px solid var(--color-tone-6);
  567. margin-top: 16px;
  568. margin-bottom: 16px;
  569. }
  570. @media (min-width: 768px)
  571. .ThreadViewer__infobarContainer {
  572. display: table;
  573. }`;
  574.  
  575. let styleSheet = document.createElement("style");
  576. styleSheet.type = "text/css";
  577. styleSheet.id = "modmailPlusSheet";
  578. styleSheet.innerText = css;
  579. if($$("#modmailPlusSheet").length == 0)
  580. document.head.appendChild(styleSheet);
  581. };
  582.  
  583. const appendHeadScript = (Settings) => {
  584. document.responses = Settings.responses;
  585. if(!$$(".CustomHeadJS").length) {
  586. const ELEMENT_headJS = () => {
  587. // this function will be turned into a string and appended into the head
  588. // you can't use variables outside of this function, they won't load
  589. const f = () => {
  590. let ruleListActivator = "<open-rulelist-dialog>";
  591.  
  592. function listBoxChanged(responseIndex) {
  593. const message = document.responses[responseIndex].content;
  594. if(message == ruleListActivator)
  595. {
  596. let ruleDiv = document.getElementsByClassName("ruleDiv")[0];
  597. ruleDiv.style.visibility = "visible";
  598. }
  599. else
  600. {
  601. const userVisitingCreatePostPage = document.querySelectorAll(".NewThread").length;
  602.  
  603. let messageBox = userVisitingCreatePostPage
  604. ? document.querySelector(".Textarea, NewThread__message")
  605. : document.getElementById("realTextarea");
  606.  
  607. let response = document.responses.find(x => x.content == message);
  608.  
  609. response.replace ? messageBox.value = message : messageBox.value += message;
  610. console.log("[Modmail++] Updated the message: %c" + messageBox.value,"color: orange");
  611. }
  612. }
  613.  
  614. // implement listbox select highlight
  615. function selected(element) {
  616. let selectColor = "#79797959";
  617. let selectedElem = document.getElementById("currentlySelected");
  618.  
  619. // if an element already selected, reset the id and set its background color to nothing
  620. if(selectedElem)
  621. {
  622. selectedElem.style.backgroundColor = "";
  623. selectedElem.id = "";
  624. }
  625.  
  626. element.parentElement.style.backgroundColor = selectColor;
  627. element.parentElement.id = "currentlySelected";
  628. document.getElementsByClassName("selectButton")[0].disabled = false;
  629. }
  630.  
  631. const removeBreaks = text => text.replace(/(\r\n|\n|\r)/gm, "");
  632.  
  633. function selectButtonClicked() {
  634. let selectedElem = document.getElementById("currentlySelected");
  635.  
  636. const userVisitingCreatePostPage = document.querySelectorAll(".NewThread").length;
  637.  
  638. let messageBox = userVisitingCreatePostPage
  639. ? document.querySelector(".Textarea, NewThread__message")
  640. : document.getElementById("realTextarea");
  641.  
  642. if(selectedElem)
  643. {
  644. let fixedDescription = atob(selectedElem.getAttribute('value')).replaceAll("\n","\n> ") + '\n\n';
  645. let message = `> [**${selectedElem.children[1].textContent}**]\n>\n> ${fixedDescription}`;
  646.  
  647. let response = document.responses.find(x => x.content == ruleListActivator);
  648. response.replace ? messageBox.value = message : messageBox.value += message;
  649.  
  650. console.log("[Modmail++] New messageBox value: %c" + messageBox.value,"color: orange");
  651.  
  652. closeIconClicked();
  653. }
  654. }
  655.  
  656. function closeIconClicked() {
  657. let ruleDiv = document.getElementsByClassName("ruleDiv")[0];
  658. ruleDiv.style.visibility = "hidden";
  659. }
  660. };
  661.  
  662. return f.toString().slice(7).slice(0, -1);
  663. };
  664.  
  665. // script element
  666. let headJS = document.createElement('script');
  667. headJS.classList.add("CustomHeadJS");
  668. headJS.innerHTML = ELEMENT_headJS();
  669.  
  670. $("head").appendChild(headJS); // if it doesn't already exist, append to head
  671. }
  672. };
  673.  
  674. const appendChatProfileIcons = async () => {
  675. if(!$(".chatProfileIcon"))
  676. {
  677. const username = removePrefix($(".InfoBar__username")?.innerText || "u/username");
  678. const response = await Get(`https://www.reddit.com/user/${username}/about.json`);
  679. const user = JSON.parse(response);
  680.  
  681. // icon element
  682. const chatProfileIcon = document.createElement('div');
  683. chatProfileIcon.innerHTML = `<img class="chatProfileIcon" src="${user.data.icon_img}" width="25">`;
  684.  
  685. for(let i = 0; i < $$(".ThreadPreview__author").length; i++) // loop trough every username on chat
  686. {
  687. // get username (u/xxxxxx)
  688. let name = $$(".Author__text")[i].innerText;
  689.  
  690. // check if there is an icon appended already
  691. let exists = $$(".ThreadPreview__author")[i].childNodes.length == 1 ? false : true;
  692.  
  693. if(removePrefix(name) == username && !exists) // if the username is the user (non-mod)
  694. {
  695. // append the icon next to the username -> [icon] u/username
  696. $$(".ThreadPreview__author")[i].insertBefore(chatProfileIcon.cloneNode(true), $$(".ThreadPreview__author")[i].firstChild);
  697. }
  698. }
  699. }
  700. };
  701.  
  702. const appendUserInfo = async (Settings) => {
  703. const ELEMENT_userInformation = user => {
  704. return `<img class="profileIcon" src="${user.data.icon_img}" width="25"/>
  705. <a class="CustomInfoBar__username" href="https://www.reddit.com/user/${user.data.name}" target="_blank">${removePrefix(user.data.subreddit.display_name_prefixed)}</a>
  706. <h1 style="color: ${Settings.textColor} ; font-size: 11px; margin-top: 17px; margin-bottom: 10px;">${sanitize(user.data.subreddit.public_description)}</h1>
  707. <h1 class="dataTitle">Main</h1>
  708. <div class="dataText">
  709. <p>Created: <span class="value">${unixToDate(user.data.created)}</span></p>
  710. <p>UserID: <span class="value">${user.data.id}</span></p>
  711. <p>Verified: <span class="value">${user.data.verified}</span></p>
  712. <p>Employee: <span class="value">${user.data.is_employee}</span></p>
  713. <p>NSFW Profile: <span class="value">${user.data.subreddit.over_18}</span></p>
  714. </div>
  715. <h1 class="dataTitle">Karma</h1>
  716. <div class="dataText">
  717. <p>Post: <span class="value">${user.data.link_karma}</span></p>
  718. <p>Comment: <span class="value">${user.data.comment_karma}</span></p>
  719. <p>Total: <span class="value">${user.data.total_karma}</span></p>
  720. <p>Awardee: <span class="value">${user.data.awardee_karma}</span></p>
  721. <p>Awarder: <span class="value">${user.data.awarder_karma}</span></p>
  722. </div>
  723. <h1 class="dataTitle">Links</h1>
  724. <div style="padding-left: 10px;">
  725. <a class="InfoBar__recent" href="https://redditmetis.com/user/${user.data.name}" target="_blank">Redditmetis</a>
  726. <a class="InfoBar__recent" href="https://www.reddit.com/search?q=${user.data.name}" target="_blank">Reddit Search</a>
  727. <a class="InfoBar__recent" href="https://www.google.com/search?q=%22${user.data.name}%22" target="_blank">Google Search</a>
  728. </div>`;
  729. };
  730.  
  731. const username = removePrefix($(".InfoBar__username")?.innerText || "u/username");
  732.  
  733. const getUserInfo = async () => {
  734. try
  735. {
  736. return await await Get(`https://www.reddit.com/user/${username}/about.json`);
  737. }
  738. catch
  739. {
  740. console.log("[Modmail++] %cFailed to load user information.", "color: red");
  741. return 0;
  742. }
  743. };
  744.  
  745. const userInfo = await getUserInfo();
  746.  
  747. if(userInfo && !$(".NewThread"))
  748. {
  749. const userJSON = JSON.parse(userInfo);
  750.  
  751. // userinfo element
  752. const userDetails = document.createElement('div');
  753. userDetails.id = "CustomMetadata";
  754. userDetails.innerHTML = ELEMENT_userInformation(userJSON);
  755.  
  756. // seperator element
  757. const seperator = document.createElement('div');
  758. seperator.classList.add("CustomSeperator");
  759.  
  760. // append the elements
  761. if(!$("#CustomMetadata")) {
  762. $(".InfoBar").insertBefore(userDetails, $(".InfoBar__username")); // append user information on top of the sidebar
  763. $(".InfoBar").insertBefore($(".InfoBar__modActions"), $(".InfoBar__recents")); // move modActions on top of recent posts
  764.  
  765. if($(".InfoBar__banText"))
  766. $(".InfoBar").insertBefore($(".InfoBar__banText"), $("#CustomMetadata"));
  767.  
  768. $(".InfoBar__username").outerHTML = ""; // delete the original username element
  769. $(".InfoBar__metadata").outerHTML = ""; // delete the original metadata
  770. }
  771. }
  772. };
  773.  
  774. const replaceReplyForm = (Settings) => {
  775. if(!$("#realTextarea") && !$(".NewThread"))
  776. {
  777. // hide the original replyform textarea
  778. $(".ThreadViewerReplyForm__replyText").style.cssText += 'display: none';
  779.  
  780. // create and append a new replyform textarea
  781. const newReplyForm = document.createElement("textarea");
  782. newReplyForm.setAttribute('class', 'Textarea ThreadViewerReplyForm__replyText ');
  783. newReplyForm.setAttribute('id', 'realTextarea');
  784. newReplyForm.setAttribute('name', 'body');
  785. newReplyForm.setAttribute('placeholder', `${Settings.placeholderMessage}`);
  786. $(".ThreadViewerReplyForm").insertBefore(newReplyForm, $(".ThreadViewerReplyForm__replyFooter"));
  787.  
  788. // make the reply button clear the new replyform
  789. const clearBoxJS = `setTimeout(function(){document.getElementById("realTextarea").value = ""; console.log("[Modmail++] Cleared the textarea!");}, 500)`;
  790. $(".ThreadViewerReplyForm__replyButton").setAttribute("onclick", clearBoxJS);
  791. }
  792. };
  793.  
  794. const appendResponseTemplateBox = async (Settings) => {
  795. if(!$("#responseListbox"))
  796. {
  797. const responseTemplateElement =
  798. `<h2 class="dataTitle">Response Templates</h2>
  799. <select id="responseListbox" onchange="listBoxChanged(this.value);" onfocus="this.selectedIndex = -1;"/>
  800. <option selected disabled hidden>Select a template</option>
  801. <span class="focus"></span>`;
  802.  
  803. const responseTemplateParent = document.createElement('div');
  804. responseTemplateParent.classList.add("select", "customResponseBox");
  805. responseTemplateParent.innerHTML = responseTemplateElement;
  806.  
  807. const userVisitingCreatePostPage = document.querySelectorAll(".NewThread").length;
  808. if(userVisitingCreatePostPage) // user visited mod.reddit.com/mail/create
  809. {
  810. // append the template box to the site
  811. $(".NewThread__fields").prepend(responseTemplateParent);
  812. $(".NewThread__fields").insertBefore($(".customResponseBox"), $(".Textarea, .NewThread__message"));
  813. }
  814. else // user visited modmail chat
  815. {
  816. // append the template box to the site
  817. $(".ThreadViewer__replyContainer").prepend(responseTemplateParent);
  818. $(".ThreadViewer__replyContainer").insertBefore($(".ThreadViewer__typingIndicator"), $(".select")); // append typing indicator before listbox
  819. }
  820.  
  821. // populates the response template listbox
  822. const populateListbox = (_query, _settings) => {
  823. const select = $(_query);
  824.  
  825. for(let i = 0; i < _settings.responses.length; i++)
  826. {
  827. let sameSubreddit = keepPrefix(_settings.responses[i].subreddit.toLowerCase(), true) == keepPrefix(_settings.subTag.toLowerCase(), true);
  828. if(sameSubreddit || _settings.responses[i].subreddit == "")
  829. {
  830. select.options[select.options.length] = new Option(_settings.responses[i].name, i);
  831. }
  832. }
  833. };
  834.  
  835. populateListbox("#responseListbox", Settings); // add all the responses to the response template listbox
  836.  
  837. // add response template's rule description elements
  838.  
  839. const ELEMENT_ruleList = listContent => {
  840. return `<div class="ruleDiv" style="background-color: rgba(26, 26, 27, 0.6); visibility: hidden">
  841. <div aria-modal="true" class="dialogWindow" role="dialog" tabindex="-1">
  842. <div class="listWindow">
  843. <div class="ruleList">
  844. <div class="ruleHeader">Select a rule<svg onclick="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>
  845. <fieldset class="fieldSet">
  846. <div class="title"><span>Which community rule did the user violate?</span></div>
  847. <div class="listBox">
  848. ${listContent}
  849. </div>
  850. <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>
  851. <div class="infoBox">
  852. <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>
  853. </div>
  854. </div>
  855. <footer class="bottomFooter"><button type="button" disabled="" onclick="selectButtonClicked()" class="selectButton">Select</button></footer>
  856. </fieldset>
  857. </div>
  858. </div>
  859. </div>
  860. </div>`;
  861. };
  862.  
  863. // creates and returns a list element
  864. const makeListValue = (name, description) => {
  865. let encodedName = btoa(name);
  866. let encodedDesc = btoa(description);
  867. return `<div class="listValue" value='${encodedDesc}'><input onclick="selected(this)" name="subredditRule" type="radio" id='${encodedName}' value='${encodedName}'><label for='${encodedName}'>${name}</label></div>`;
  868. };
  869.  
  870. const getRules = async () => {
  871. try
  872. {
  873. return await Get(Settings.rules + ".json");
  874. }
  875. catch
  876. {
  877. console.log("[Modmail++] %cFailed to load subreddit rules, possibly a private subreddit?", "color: red");
  878. return 0;
  879. }
  880. };
  881.  
  882. let rules = await getRules();
  883.  
  884. if(rules)
  885. {
  886. const ruleJSON = JSON.parse(rules);
  887. $$(".subredditRuleList").forEach(elem => elem.remove()); // remove all subredditRuleList elements
  888.  
  889. let ruleListElements = "";
  890.  
  891. for(let i = 0; i < ruleJSON.rules.length; i++)
  892. {
  893. ruleListElements += makeListValue(ruleJSON.rules[i].short_name, ruleJSON.rules[i].description);
  894. }
  895.  
  896. // (Append) Div ruleList element to body
  897. const ruleList = document.createElement('div');
  898. ruleList.classList.add("subredditRuleList");
  899. ruleList.innerHTML = ELEMENT_ruleList(ruleListElements);
  900. $("body").appendChild(ruleList);
  901. }
  902. }
  903. };
  904.  
  905. const fixQuoteButtons = () => {
  906. /* On click, do this
  907. 1) take the text from the original form
  908. 2) split it by two new lines
  909. 3) take the last result (last quoted message)
  910. 4) paste the last result to the new form
  911. */
  912.  
  913. const onClickFunc = () => {
  914. setTimeout(() => {
  915. const originalForm = document.querySelector(".Textarea, .ThreadViewerReplyForm__replyText");
  916.  
  917. let originalValue = originalForm.value;
  918. let text = "";
  919.  
  920. if(originalValue.includes("\n\n"))
  921. text = originalValue.split("\n\n").filter(x => x.length > 0).pop();
  922. else
  923. text = originalValue;
  924.  
  925. if(text.indexOf("\n") == 0) text = text.slice(1);
  926.  
  927. if(text && text.includes("said:"))
  928. document.querySelector("#realTextarea").value += text + "\n\n";
  929. }, 50);
  930. };
  931.  
  932. $$(".Message__quote").forEach(elem => {
  933. if(!elem.getAttribute("onclick"))
  934. elem.setAttribute("onclick", onClickFunc.toString().slice(7).slice(0, -1));
  935. });
  936. };
  937. const handleCreateMessagePage = () => {
  938. /* Handle change of username
  939. * change the response usernames to the changed username
  940. * */
  941. let toUser = document.querySelector(".Radio__input[value=user]");
  942. if(toUser) {
  943. toUser.onclick = () => {
  944. let lastUsername = null;
  945.  
  946. const waitForTextbox = setInterval(() => {
  947. const newThreadTextbox = document.querySelector(".NewThread__username");
  948.  
  949. if(newThreadTextbox) {
  950. clearInterval(waitForTextbox);
  951.  
  952. newThreadTextbox.onchange = e => {
  953. const username = "u/" + e.target.value;
  954. const defaultLastUsername = "u/undefined";
  955.  
  956. document.responses.forEach(response => {
  957. response.content = response.content.replaceAll(lastUsername || defaultLastUsername, username);
  958. })
  959.  
  960. lastUsername = username;
  961. }
  962. }
  963. }, 100);
  964. }
  965. }
  966. /* Handle change of subreddit
  967. * change the response subreddits to the changed subreddit
  968. * */
  969. const srName = document.querySelector("[name=srName]");
  970. let lastSubreddit = null;
  971. let defaultLastSubreddit = "r/subreddit";
  972.  
  973. if(srName) {
  974. const postToChanges = setInterval(() => {
  975. const subreddit = "r/" + srName.value;
  976.  
  977. if(subreddit != lastSubreddit && subreddit != 'r/') {
  978. document.responses.forEach(response => {
  979. response.content = response.content.replaceAll(lastSubreddit || defaultLastSubreddit, subreddit);
  980. })
  981.  
  982. lastSubreddit = subreddit;
  983. }
  984. }, 500)
  985. }
  986. };
  987.  
  988. const __main__ = async () => {
  989. console.log("[Modmail++] %cMain function ran!", "color: grey");
  990.  
  991. const Settings = new __settings__();
  992.  
  993. appendHeadScript(Settings);
  994. appendUserInfo(Settings);
  995. replaceReplyForm(Settings);
  996. applyCSS(Settings);
  997. fixQuoteButtons();
  998. handleCreateMessagePage();
  999. if(Settings.chatProfileIcons)
  1000. appendChatProfileIcons();
  1001.  
  1002. if(Settings.enableCustomResponses && $("#responseListbox") == null)
  1003. appendResponseTemplateBox(Settings);
  1004.  
  1005. console.log("[Modmail++] %cLoaded!", "color: lime");
  1006. };
  1007.  
  1008. let run = false;
  1009.  
  1010. setInterval (function () {
  1011. if (this.lastPathStr !== location.pathname)
  1012. {
  1013. this.lastPathStr = location.pathname;
  1014.  
  1015. console.log("[Modmail++] %cNew page detected!", "color: gold");
  1016.  
  1017. run = true;
  1018.  
  1019. let waitForElements = setInterval (() => {
  1020. if($(".NoThreadMessage__generic") && run) // add confetti explosion if no mail
  1021. {
  1022. clearInterval(waitForElements);
  1023. run = false;
  1024.  
  1025. console.log("[Modmail++] %cNo modmail!", "color: lime");
  1026.  
  1027. party.confetti($(".NoThreadMessage__generic"), {
  1028. count: 15,
  1029. spread: 50
  1030. });
  1031. }
  1032.  
  1033. if($(".InfoBar__username") && run) // user is on modmail "chat" page
  1034. {
  1035. clearInterval(waitForElements);
  1036. run = false;
  1037.  
  1038. if($("body") && !$("#CustomMetadata"))
  1039. __main__();
  1040. }
  1041. if($(".NewThread") && run) {
  1042. clearInterval(waitForElements);
  1043. run = false;
  1044. __main__();
  1045. }
  1046. }, 5);
  1047. }
  1048. }, 100);
  1049. })();