[s4s] interface

Lets you view the greenposts.

  1. // ==UserScript==
  2. // @name [s4s] interface
  3. // @namespace s4s4s4s4s4s4s4s4s4s
  4. // @version 3.33
  5. // @author le fun css man AKA Doctor Worse Than Hitler, kekero
  6. // @email doctorworsethanhitler@gmail.com
  7. // @description Lets you view the greenposts.
  8. // @match https://boards.4chan.org/s4s/*
  9. // @match http://boards.4chan.org/s4s/*
  10. // @connect funposting.online
  11. // @run-at document-start
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM.xmlHttpRequest
  14. // @grant GM.setValue
  15. // @grant GM.getValue
  16. // @grant GM_setValue
  17. // @grant GM_getValue
  18. // @grant unsafeWindow
  19. // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTAgMEgxNlYxNkgwIiBmaWxsPSIjZGZkIi8+PHBhdGggZD0iTTMgNCA2IDFoNGwzIDN2OGwtMyAzSDZMMyAxMiIgZmlsbD0iZ3JlZW4iLz48cGF0aCBkPSJtNS41IDExLjVoLTJ2LTdoMnYtM2MtMyAwLTUgMi41LTUgNi41IDAgNCAyIDYuNSA1IDYuNXptNSAzYzMgMCA1LTIuNSA1LTYuNSAwLTQtMi02LjUtNS02LjV2M2gydjdoLTJ6bS00LTRoM0wxMCAyLjVINlptMCAzaDN2LTNoLTN6IiBmaWxsPSIjZmZmIiBzdHJva2U9ImdyZWVuIi8+PC9zdmc+
  20. // ==/UserScript==
  21. "use strict";
  22.  
  23.  
  24. if(query("#s4sinterface-css")){
  25. throw "Multiple instances of [s4s] interface detected"
  26. }
  27.  
  28. var interfaceLinkRegex = new RegExp('<a[^>]*>&gt;&gt;\\d*( \\(.*\\))?<\\/a>(<span>)?-\\d*');
  29. var weekdays=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]
  30. var postForm={}
  31. var lastCommentForm
  32. var updateLinks=new Set()
  33. var cacheCatalogPosts={}
  34. var mode=""
  35. var threadId
  36. var numThreads
  37. var pathName=location.pathname
  38. var threadMatch=pathName.match(/\/thread\/(\d+)/)
  39. if(threadMatch){
  40. // /board/thread/1
  41. mode="thread"
  42. threadId=threadMatch[1]
  43. }else if(/\/catalog$/.test(pathName)){
  44. // /board/catalog
  45. mode="catalog"
  46. }else if(/^\/[^\/]+\/\d*$/.test(pathName)){
  47. // /board/
  48. mode="index"
  49. }
  50.  
  51. if(typeof GM=="undefined"){
  52. window.GM={
  53. xmlHttpRequest:window.GM_xmlhttpRequest,
  54. getValue:window.GM_getValue,
  55. setValue:window.GM_setValue
  56. }
  57. }
  58.  
  59. // Request green posts
  60. var serverurl="https://funposting.online/interface/"
  61. if(mode=="thread"){
  62. getGreenPosts(threadId)
  63. }else if(mode=="catalog"){
  64. onPageLoad(_=>{
  65. getGreenPostsCatalog()
  66. })
  67. }else if(mode=="index") {
  68. onPageLoad(_=>{
  69. numThreads = document.getElementsByClassName("thread").length
  70. // use a mutation observer to update the green posts on the index on infinite scrollio
  71. var observer = new MutationObserver(function (mutations) {
  72. mutations.forEach(function (mutation) {
  73. checkForIndexUpdate() // checks for and updates the index on infinite scroll
  74. });
  75. });
  76. observer.observe(document, {childList:true, subtree:true})
  77.  
  78. addGreenPostsToIndex()
  79. })
  80. }
  81.  
  82. // checks for and updates the index on infinite scroll
  83. function checkForIndexUpdate() {
  84. if (numThreads != document.getElementsByClassName("thread").length) {
  85. numThreads = document.getElementsByClassName("thread").length
  86. addGreenPostsToIndex()
  87. }
  88. }
  89.  
  90. function addGreenPostsToIndex() {
  91. var threads = document.getElementsByClassName("thread")
  92. for (var i = 0; i < threads.length; i++) {
  93. var responses = threads[i].getElementsByClassName("replyContainer")
  94. var since = 0
  95. if(responses != null && responses.length > 0) {
  96. var since = threads[i].getElementsByClassName("replyContainer")[0].id.substr(2)
  97. }
  98. getGreenPosts(threads[i].id.substr(1), since)
  99. }
  100. }
  101.  
  102. onPageLoad(_=>{
  103. // Classic post form
  104. if(mode=="thread"){
  105. var nameField=query("#postForm input[name=name]")
  106. if(nameField){
  107. var commentField=query("#postForm textarea")
  108. addCommentForm(commentField,1)
  109. var greenToggle=element(
  110. ["button#toggle",{
  111. class:"greenToggle",
  112. title:"[s4s] Interface",
  113. onclick:event=>{
  114. event.preventDefault()
  115. event.stopPropagation()
  116. showPostFormClassic()
  117. }
  118. },"!"]
  119. ).toggle
  120. var nameParent=nameField.parentNode
  121. nameParent.classList.add("nameFieldParent")
  122. insertBefore(greenToggle,nameField)
  123. }else{
  124. // Thread is archived
  125. showPostFormClassic()
  126. }
  127. getUpdateLinks()
  128. }
  129. })
  130.  
  131.  
  132. // watch lists
  133. onPageLoad(_=>{
  134. // native extension watch list
  135. if(document.getElementById("watchList") !== null) {
  136. var observer = new MutationObserver(function (mutations) {
  137. mutations.forEach(function (mutation) {
  138. updateNativeWatchList();
  139. });
  140. });
  141. observer.observe(document.getElementById("watchList"), {childList:true, subtree:true})
  142. }
  143. updateNativeWatchList() // call this once in addition to the observer to make sure it gets ran on page load even if the thread watcher doesn't refresh
  144. ///todo: 4chan x watch list
  145. })
  146.  
  147. // updates the native watch list
  148. async function updateNativeWatchList() {
  149. var watchList = document.getElementById("watchList");
  150. var watchedThreads = watchList.getElementsByTagName('li');
  151. if(watchedThreads !== null && watchedThreads.length > 0) {
  152. for(var i = 0; i < watchedThreads.length; i++) {
  153. var thread = watchedThreads[i].id.split('-')[1]; // format is 'watch-12345-s4s'
  154. var board = watchedThreads[i].id.split('-')[2];
  155. // it's da [s4s] inderfase not da otherboard indaface
  156. if(board != 's4s') continue;
  157.  
  158. // GM_getValue will store the ['thread' => 'number of last seen posts'] pairs
  159. var lastSeen = await GM.getValue(thread, false);
  160.  
  161. // if we have watched this thread, we check for updates
  162. if(lastSeen !== false) {
  163. getCountSince(thread, lastSeen);
  164. }
  165. else {
  166. getNewestPost(thread);
  167. }
  168. }
  169. }
  170.  
  171. }
  172.  
  173. // Native extension QR
  174. document.addEventListener("QRNativeDialogCreation",onQRCreated)
  175. if(unsafeWindow.Main){
  176. onNativeextInit()
  177. }else{
  178. document.addEventListener("4chanMainInit",onNativeextInit)
  179. }
  180.  
  181. function onPageLoad(func){
  182. if(document.readyState=="loading"){
  183. addEventListener("DOMContentLoaded",func)
  184. }else{
  185. func()
  186. }
  187. }
  188. // firefox will (sometimes) fail to load document.documentElement until the page is loaded
  189. onPageLoad(_=>{
  190. // 4chan-X QR integration
  191. if(document.documentElement.classList.contains("fourchan-x")){
  192. on4chanXInit()
  193. }else{
  194. document.addEventListener("4chanXInitFinished",on4chanXInit)
  195. }
  196. document.addEventListener("QRDialogCreation",onQRXCreated)
  197. })
  198.  
  199. // replaces links like >>1234567-123 in native 4chan posts with an appropriate link back to the interface post.
  200. function replaceInterfaceLinks(post) {
  201. while(interfaceLinkRegex.test(post.innerHTML)) {
  202. var link = interfaceLinkRegex.exec(post.innerHTML)[0] //something like <a href="#p6696342" class="quotelink ql-tracked">&gt;&gt;6696342 (You)</a>-6754<br>test pls ignorlol
  203. var link_afterno = /&gt;\d+/.exec(link)[0].substr(4)
  204. var link_interfaceno = /-\d+/.exec(link)[0].substr(1)
  205. var has_span = /<span>/.test(link) // sometimes the end of the link starts with a <span> so lets not forget it later
  206. var replace = '<a class="quotelink" href="#p'+link_afterno+'-'+link_interfaceno+'">&gt;&gt;'+link_afterno+'-'+link_interfaceno+'</a>'
  207. if(has_span) replace += '<span>'
  208. post.innerHTML = post.innerHTML.replace(interfaceLinkRegex, replace)
  209. }
  210. }
  211.  
  212. // gets the number of posts since the newest green post specified by the green post's interface id (the number after the -, e.g. 123456-123 is 123)
  213. function getCountSince(thread, newestGreenPost) {
  214. GM.xmlHttpRequest({
  215. method:"get",
  216. url:serverurl+"watch.php?thread="+thread+"&newestGreenPost="+newestGreenPost,
  217. onload:response=>{
  218. if(response.status==200){
  219. onPageLoad(_=>{
  220. var count=response.responseText
  221. if(count > 0) {
  222. updateWatchListItem(thread,count)
  223. }
  224. })
  225. }
  226. },
  227. onerror:response=>{
  228. return 0;
  229. }
  230. })
  231. }
  232.  
  233. // todo: 4chan X
  234. function updateWatchListItem(thread, count) {
  235. var item = query('#watch-'+thread+'-s4s > a:nth-child(2)');
  236.  
  237. if(item.classList.contains("newGreenPost")) return;
  238. item.classList.add("newGreenPost");
  239. }
  240.  
  241. // gets the green id of the newest green post in a thread
  242. function getNewestPost(thread) {
  243. GM.xmlHttpRequest({
  244. method:"get",
  245. url:serverurl+"watch.php?thread="+thread,
  246. onload:response=>{
  247. if(response.status==200){
  248. onPageLoad(_=>{
  249. GM.setValue(thread, response.responseText);
  250. })
  251. }
  252. },
  253. onerror:response=>{
  254. }
  255. })
  256. }
  257.  
  258. // Request green posts & add them
  259. function getGreenPosts(thread, since = 0){
  260. GM.xmlHttpRequest({
  261. method:"get",
  262. url:serverurl+"get.php?thread="+thread+((since != 0) ? "since="+since : ""),
  263. onload:response=>{
  264. if(response.status==200){
  265. onPageLoad(_=>{
  266. var postsObj=JSON.parse(response.responseText)
  267. var postsCount=Object.keys(postsObj).length
  268. if(postsCount){
  269. if(mode == "thread") {
  270. var oldPosts=queryAll(".greenPostContainer")
  271. for(var i=0;i<oldPosts.length;i++){
  272. removeChild(oldPosts[i])
  273. }
  274. var currentPost
  275. for(var i=postsCount;i--;){
  276. currentPost=addPost(postsObj[i],currentPost)
  277. }
  278.  
  279. // update the watchlist to say "weve seen the post lole"
  280. GM.setValue(thread,postsObj[0].id)
  281. }
  282. else if(mode == "index") {
  283. for(var i=0; i< postsCount; i++){
  284. // dont reinsert posts
  285. if(document.getElementById('p'+postsObj[i].after_no+"-"+postsObj[i].id) === null) {
  286. addPost(postsObj[i],document.getElementById(postsObj[i].after_no))
  287. }
  288. }
  289. }
  290. }
  291. })
  292. }
  293. },
  294. onerror:response=>{
  295. }
  296. })
  297. }
  298.  
  299. // takes the JSON from the server and converts to an HTML element
  300. function postJsonToElement(aPost){
  301. var numberless=aPost.options=="numberless"
  302. var afterNo=numberless?"XXXXXX":aPost.after_no
  303. var postId=afterNo+"-"+aPost.id
  304. var date=new Date(aPost.timestamp*1000)
  305. var dateString=
  306. padding(date.getMonth()+1,2)+"/"+
  307. padding(date.getDate(),2)+"/"+
  308. (""+date.getFullYear()).slice(-2)+
  309. "("+weekdays[date.getDay()]+")"+
  310. padding(date.getHours(),2)+":"+
  311. padding(date.getMinutes(),2)+":"+
  312. padding(date.getSeconds(),2)
  313. var linkReply
  314. if(!numberless){
  315. linkReply=[0,
  316. " ",
  317. ["a",{
  318. href:"#p"+postId,
  319. title:"Link to this post"
  320. },"No."],
  321. ["a",{
  322. href:"javascript:quote('"+postId+"');",
  323. onclick:insertQuote,
  324. title:"Reply to this post"
  325. },postId]
  326. ]
  327. }
  328. var replyHideX=document.documentElement.classList.contains("reply-hide")
  329. var post=element(
  330. ["div#post",{
  331. class:"postContainer replyContainer greenPostContainer",
  332. id:"pc"+aPost.after_no
  333. },
  334. (replyHideX?
  335. ["div",{
  336. id:"sa"+postId
  337. },
  338. ["a",{
  339. class:"hide-reply-button"
  340. },
  341. ["span",{
  342. class:"fa fa-minus-square-o"
  343. }]
  344. ]
  345. ]
  346. :
  347. ["div",{
  348. class:"sideArrows",
  349. id:"sa"+postId
  350. },">>"]
  351. ),
  352. ["div",{
  353. class:"post reply",
  354. id:"p"+postId
  355. },
  356. ["div",{
  357. class:"postInfoM mobile",
  358. id:"pim"+postId
  359. },
  360. ["span",{
  361. class:"nameBlock"
  362. },
  363. ["span",{
  364. class:"name"
  365. },aPost.username],
  366. ["br"]
  367. ],
  368. ["span",{
  369. class:"dateTime postNum",
  370. "data-utc":aPost.timestamp
  371. },
  372. dateString,
  373. linkReply
  374. ]
  375. ],
  376. ["div",{
  377. class:"postInfo desktop",
  378. id:"pi"+postId
  379. },
  380. ["input",{
  381. type:"checkbox",
  382. name:"ignore",
  383. value:"delete"
  384. }],
  385. ["span",{
  386. class:"nameBlock"
  387. },
  388. ["span",{
  389. class:"name"
  390. },aPost.username]
  391. ],
  392. " ",
  393. ["span",{
  394. class:"dateTime",
  395. "data-utc":aPost.timestamp
  396. },dateString],
  397. (!numberless&&
  398. ["span",{
  399. class:"postNum desktop",
  400. onclick:insertQuote,
  401. title:"Reply to this post"
  402. },linkReply]
  403. )
  404. ],
  405. ["blockquote",{
  406. class:"postMessage",
  407. id:"m"+postId,
  408. innerHTML:aPost.text.replace(/\r/g,"")
  409. }]
  410. ]
  411. ]
  412. ).post
  413. return post
  414. }
  415.  
  416. // Add a post to the proper position in the thread
  417. function addPost(aPost,currentPost){
  418. if(!currentPost){
  419. currentPost=query(".thread>.postContainer")
  420. }
  421.  
  422. var post=postJsonToElement(aPost)
  423.  
  424. if(mode == "thread") {
  425. // Add the post
  426. while(currentPost){
  427. var lastPost=currentPost
  428. if(!/^pc\d+$/.test(currentPost.id)||currentPost.id.slice(2)<=aPost.after_no){
  429. currentPost=currentPost.nextSibling
  430. }else{
  431. return insertBefore(post,currentPost)
  432. }
  433. }
  434. return insertAfter(post,lastPost)
  435. }
  436. else if(mode == "index") {
  437. return insertAfter(post,document.getElementById("pc"+aPost.after_no))
  438. }
  439. }
  440.  
  441. // Get green post count on catalog
  442. function getGreenPostsCatalog(){
  443. var threadContainer=query(".is_catalog #threads,.catalog-mode .board")
  444. if(!threadContainer||!threadContainer.children.length){
  445. if(mode=="catalog"){
  446. return setTimeout(getGreenPostsCatalog,500)
  447. }else{
  448. var insertListener=event=>{
  449. document.removeEventListener("PostsInserted",insertListener)
  450. getGreenPostsCatalog()
  451. }
  452. return document.addEventListener("PostsInserted",insertListener)
  453. }
  454. }
  455. var threads=[]
  456. var catalogThreads=threadContainer.children
  457. for(var i=0;i<catalogThreads.length;i++){
  458. var idMatch=catalogThreads[i].id.match(/\d+/)
  459. if(idMatch){
  460. threads.push(idMatch[0])
  461. }
  462. }
  463. GM.xmlHttpRequest({
  464. method:"post",
  465. headers:{
  466. "Content-type":"application/x-www-form-urlencoded"
  467. },
  468. url:serverurl+"get.php?mode=catalog",
  469. data:"thread="+threads.join(","),
  470. onload:response=>{
  471. if(response.status==200){
  472. cacheCatalogPosts=JSON.parse(response.responseText)
  473. showGreenPostsCatalog()
  474. if(mode=="catalog"){
  475. new MutationObserver(mutations=>{
  476. showGreenPostsCatalog()
  477. }).observe(threadContainer,{childList:1})
  478. }else{
  479. document.addEventListener("PostsInserted",showGreenPostsCatalog)
  480. }
  481. }
  482. },
  483. onerror:response=>{
  484. }
  485. })
  486. }
  487.  
  488. function showGreenPostsCatalog(){
  489. var countObj=cacheCatalogPosts
  490. var oldPosts=queryAll(".greenPostCount")
  491. for(var i=0;i<oldPosts.length;i++){
  492. removeChild(oldPosts[i].previousSibling)
  493. removeChild(oldPosts[i])
  494. }
  495. var threadMeta
  496. for(var thread in countObj){
  497. if(mode=="catalog"){
  498. threadMeta=document.getElementById("meta-"+thread)
  499. }else{
  500. threadMeta=query("#p"+thread+">.catalog-stats>span")
  501. }
  502. if(threadMeta){
  503. addCatalogPosts(countObj[thread],threadMeta)
  504. }
  505. }
  506. }
  507.  
  508. function addCatalogPosts(count,threadMeta){
  509. if(count){
  510. var nativeCatalog=0
  511. if(mode=="catalog"){
  512. nativeCatalog=1
  513. }
  514. var text=document.createTextNode(" / ")
  515. var postCount=element(
  516. ["span#span",{
  517. class:"greenPostCount"
  518. },
  519. (nativeCatalog&&
  520. "G: "
  521. ),
  522. ["b",count]
  523. ]
  524. ).span
  525. var afterNode=threadMeta.childNodes[nativeCatalog]
  526. insertAfter(text,afterNode)
  527. insertAfter(postCount,text)
  528. }
  529. }
  530.  
  531. // Classic post form
  532. function showPostFormClassic(hide){
  533. var formSelector="body>form:not(.greenPostForm)"
  534. var nameField=query(formSelector+" input[name=name]")
  535. var optionsField=query(formSelector+" input[name=email]")
  536. var commentField=query(formSelector+" textarea")
  537. if(hide){
  538. if(postForm.classic){
  539. if(nameField){
  540. nameField.value=postForm.classic.name.value
  541. optionsField.value=postForm.classic.options.value
  542. commentField.value=postForm.classic.comment.value
  543. lastCommentForm=commentField
  544. }
  545. removeChild(postForm.classic.form)
  546. postForm.classic=0
  547. }
  548. return
  549. }
  550. if(postForm.classic){
  551. return
  552. }
  553. var username=""
  554. if(nameField){
  555. username=nameField.value
  556. }else{
  557. var nameMatch=document.cookie.match(/4chan_name=(.*?)(?:;|$)/)
  558. if(nameMatch){
  559. username=nameMatch[1]
  560. }
  561. }
  562. postForm.classic=element(
  563. ["form#form",{
  564. name:"post",
  565. action:serverurl+"post.php",
  566. method:"post",
  567. enctype:"multipart/form-data",
  568. class:"greenPostForm",
  569. onsubmit:submitGreenPost
  570. },
  571. ["input",{
  572. name:"thread",
  573. value:threadId,
  574. type:"hidden"
  575. }],
  576. ["table",{
  577. class:"postForm"
  578. },
  579. ["tbody",
  580. ["tr",
  581. ["td","Name"],
  582. ["td",{
  583. class:"nameFieldParent"
  584. },
  585. (nameField&&
  586. ["button#toggle",{
  587. class:"greenToggle pressed",
  588. title:"[s4s] Interface",
  589. onclick:event=>{
  590. event.preventDefault()
  591. event.stopPropagation()
  592. showPostFormClassic(1)
  593. }
  594. },"!"]
  595. ),
  596. ["input#name",{
  597. type:"text",
  598. name:"username",
  599. tabIndex:1,
  600. placeholder:"Anonymous",
  601. value:username
  602. }]
  603. ]
  604. ],
  605. ["tr",
  606. ["td","Options"],
  607. ["td",
  608. ["input#options",{
  609. type:"text",
  610. name:"options",
  611. tabIndex:2,
  612. value:optionsField?optionsField.value:""
  613. }],
  614. ["input",{
  615. type:"submit",
  616. tabIndex:6,
  617. value:"Post"
  618. }]
  619. ]
  620. ],
  621. ["tr",
  622. ["td","Comment"],
  623. ["td",
  624. ["textarea#comment",{
  625. name:"text",
  626. tabindex:4,
  627. cols:48,
  628. rows:4,
  629. wrap:"soft",
  630. value:commentField?commentField.value:""
  631. }]
  632. ]
  633. ]
  634. ]
  635. ]
  636. ]
  637. )
  638. addCommentForm(postForm.classic.comment)
  639. var originalForm=query("#postForm")
  640. if(originalForm){
  641. originalForm=originalForm.parentNode
  642. }else{
  643. originalForm=query("body>.closed+*")
  644. if(!originalForm){
  645. originalForm=query("#op")
  646. }
  647. }
  648. insertBefore(postForm.classic.form,originalForm)
  649. }
  650.  
  651. // Native extension initialised
  652. function onNativeextInit(){
  653. if(mode=="thread"||mode=="index"){
  654. getUpdateLinks()
  655. // Native extension quick reply
  656. unsafeWindow.QR.showInterface=unsafeWindow.QR.show
  657. var newQRshow=thread=>{
  658. var event=new CustomEvent("QRNativeDialogCreation",{
  659. bubbles:true,
  660. detail:{thread:thread}
  661. })
  662. document.dispatchEvent(event)
  663. }
  664. if(typeof exportFunction=="function"){
  665. newQRshow=exportFunction(newQRshow,document.defaultView)
  666. }
  667. unsafeWindow.QR.show=newQRshow
  668. }
  669. }
  670.  
  671. function onQRCreated(event){
  672. threadId=event.detail.thread
  673. try{
  674. unsafeWindow.QR.showInterface(threadId)
  675. }catch(e){}
  676. // Clean up post form if it was initialised before
  677. var oldToggle=query("#quickReply form:not(.greenPostForm) .greenToggle")
  678. if(oldToggle){
  679. removeChild(oldToggle)
  680. }
  681. showPostFormQR(1)
  682. var formSelector="#qrForm"
  683. var nameField=query(formSelector+" input[name=name]")
  684. nameField.value=query("#postForm input[name=name]").value
  685. nameField.tabIndex=0
  686. var commentField=query(formSelector+" textarea")
  687. addCommentForm(commentField)
  688. var toggle=element(
  689. ["button#toggle",{
  690. type:"button",
  691. class:"greenToggle",
  692. title:"[s4s] Interface",
  693. onclick:event=>{
  694. event.preventDefault()
  695. event.stopPropagation()
  696. showPostFormQR()
  697. }
  698. },"!"]
  699. ).toggle
  700. var nameParent=nameField.parentNode
  701. nameParent.classList.add("nameFieldParent")
  702. insertBefore(toggle,nameField)
  703. }
  704.  
  705. function showPostFormQR(hide){
  706. var formSelector="#qrForm"
  707. var nameField=query(formSelector+" input[name=name]")
  708. var optionsField=query(formSelector+" input[name=email]")
  709. var commentField=query(formSelector+" textarea")
  710. if(hide){
  711. if(postForm.QR){
  712. nameField.value=postForm.QR.name.value
  713. optionsField.value=postForm.QR.options.value
  714. commentField.value=postForm.QR.comment.value
  715. lastCommentForm=commentField
  716. removeChild(postForm.QR.form)
  717. postForm.QR=0
  718. }
  719. return
  720. }
  721. var qr=query("#quickReply form:not(.greenPostForm)")
  722. if(postForm.QR||!qr){
  723. return
  724. }
  725. postForm.QR=element(
  726. ["form#form",{
  727. name:"post",
  728. action:serverurl+"post.php",
  729. method:"post",
  730. enctype:"multipart/form-data",
  731. class:"greenPostForm",
  732. onsubmit:submitGreenPost
  733. },
  734. ["input",{
  735. name:"thread",
  736. value:threadId,
  737. type:"hidden"
  738. }],
  739. ["div",{
  740. class:"nameFieldParent"
  741. },
  742. ["button",{
  743. type:"button",
  744. class:"greenToggle pressed",
  745. title:"[s4s] Interface",
  746. onclick:event=>{
  747. showPostFormQR(1)
  748. }
  749. },"!"],
  750. ["input#name",{
  751. type:"text",
  752. name:"username",
  753. class:"field",
  754. placeholder:"Anonymous",
  755. value:nameField.value
  756. }]
  757. ],
  758. ["div",
  759. ["input#options",{
  760. type:"text",
  761. name:"options",
  762. class:"field",
  763. placeholder:"Options",
  764. value:optionsField.value
  765. }]
  766. ],
  767. ["div",
  768. ["textarea#comment",{
  769. name:"text",
  770. class:"field",
  771. cols:48,
  772. rows:4,
  773. wrap:"soft",
  774. placeholder:"Comment",
  775. value:commentField.value
  776. }],
  777. ],
  778. ["div",
  779. ["span",{
  780. class:"greenSubmit",
  781. onclick:event=>{
  782. submitGreenPost(event,postForm.QR.form)
  783. }
  784. },"Post"]
  785. ]
  786. ]
  787. )
  788. addCommentForm(postForm.QR.comment)
  789. insertBefore(postForm.QR.form,qr)
  790. }
  791.  
  792. // 4chan-X initialised
  793. function on4chanXInit(){
  794. if(mode=="index"&&document.documentElement.classList.contains("catalog-mode")){
  795. getGreenPostsCatalog()
  796. }
  797. }
  798.  
  799. // 4chan-X QR
  800. function onQRXCreated(){
  801. getUpdateLinks()
  802. var formSelector="#qr form:not(.greenPostForm)"
  803. var commentField=query(formSelector+" textarea")
  804. addCommentForm(commentField)
  805. var toggle=element(
  806. ["button#toggle",{
  807. type:"button",
  808. class:"greenToggle",
  809. title:"[s4s] Interface",
  810. onclick:event=>{
  811. event.preventDefault()
  812. event.stopPropagation()
  813. showPostFormQRX()
  814. }
  815. },"!"]
  816. ).toggle
  817. var qrPersona=query("#qr .persona")
  818. insertBefore(toggle,qrPersona.firstChild)
  819. }
  820.  
  821. function showPostFormQRX(hide){
  822. var formSelector="#qr form:not(.greenPostForm)"
  823. var nameField=query(formSelector+" input[name=name]")
  824. var optionsField=query(formSelector+" input[name=email]")
  825. var commentField=query(formSelector+" textarea")
  826. if(hide){
  827. if(postForm.QRX){
  828. nameField.value=postForm.QRX.name.value
  829. optionsField.value=postForm.QRX.options.value
  830. commentField.value=postForm.QRX.comment.value
  831. lastCommentForm=commentField
  832. removeChild(postForm.QRX.form)
  833. postForm.QRX=0
  834. }
  835. return
  836. }
  837. var qrx=query(formSelector)
  838. if(postForm.QRX||!qrx){
  839. return
  840. }
  841. threadId=query("#qr select[data-name=thread]").value
  842. postForm.QRX=element(
  843. ["form#form",{
  844. name:"post",
  845. action:serverurl+"post.php",
  846. method:"post",
  847. enctype:"multipart/form-data",
  848. class:"greenPostForm",
  849. onsubmit:submitGreenPost
  850. },
  851. ["input",{
  852. name:"thread",
  853. value:threadId,
  854. type:"hidden"
  855. }],
  856. ["div",{
  857. class:"persona"
  858. },
  859. ["button",{
  860. type:"button",
  861. class:"greenToggle pressed",
  862. title:"[s4s] Interface",
  863. onclick:event=>{
  864. showPostFormQRX(1)
  865. }
  866. },"!"],
  867. ["input#name",{
  868. name:"username",
  869. class:"field",
  870. placeholder:"Name",
  871. size:1,
  872. value:nameField.value
  873. }],
  874. ["input#options",{
  875. name:"options",
  876. class:"field",
  877. placeholder:"Options",
  878. size:1,
  879. value:optionsField.value
  880. }]
  881. ],
  882. ["textarea#comment",{
  883. name:"text",
  884. class:"field",
  885. placeholder:"Comment",
  886. value:commentField.value
  887. }],
  888. ["div",{
  889. class:"file-n-submit"
  890. },
  891. ["input",{
  892. type:"submit",
  893. value:"Submit"
  894. }]
  895. ]
  896. ]
  897. )
  898. addCommentForm(postForm.QRX.comment)
  899. insertBefore(postForm.QRX.form,qrx)
  900. }
  901.  
  902.  
  903. // Track last used comment field for inserting quotes
  904. function addCommentForm(commentField,notLast){
  905. if(!notLast){
  906. lastCommentForm=commentField
  907. }
  908. commentField.addEventListener("focus",event=>{
  909. lastCommentForm=event.currentTarget
  910. })
  911. }
  912.  
  913. function insertQuote(event){
  914. var commentField=lastCommentForm
  915. if(commentField&&document.contains(commentField)){
  916. event.preventDefault()
  917. event.stopPropagation()
  918. var isQRX=commentField.closest("#qr")
  919. if(isQRX){
  920. isQRX.hidden=0
  921. }
  922. var text=">>"+event.currentTarget.firstChild.data+"\n"
  923. var caretPos=commentField.selectionStart
  924. commentField.value=
  925. commentField.value.slice(0,caretPos)
  926. +text
  927. +commentField.value.slice(commentField.selectionEnd)
  928. var range=caretPos+text.length
  929. commentField.setSelectionRange(range,range)
  930. commentField.focus()
  931. }
  932. }
  933.  
  934. // Manually update thread with green posts
  935. function getUpdateLinks(){
  936. var update=queryAll("[data-cmd=update],.updatelink>a")
  937. for(var i=0;i<update.length;i++){
  938. if(!updateLinks.has(update[i])){
  939. update[i].addEventListener("click",event=>{
  940. getGreenPosts(threadId)
  941. })
  942. updateLinks.add(update[i])
  943. }
  944. }
  945. }
  946.  
  947. // Submit a green post
  948. function submitGreenPost(event,form){
  949. event.preventDefault()
  950. event.stopPropagation()
  951. if(!form){
  952. form=event.currentTarget
  953. }
  954. var submit={}
  955. submit.button=form.querySelector(":scope input[type=submit],:scope .greenSubmit")
  956. submit.fakeButton=submit.button.classList.contains("greenSubmit")
  957. if(submit.fakeButton){
  958. submit.text=submit.button.firstChild.data
  959. submit.button.firstChild.data="..."
  960. submit.button.classList.add("greenSubmitDisabled")
  961. }else{
  962. submit.text=submit.button.value
  963. submit.button.value="..."
  964. submit.button.disabled=1
  965. }
  966. var data=[]
  967. var formData=new FormData(form)
  968. for(var nameValue of formData){
  969. data.push(
  970. nameValue[0]+"="
  971. +encodeURIComponent(nameValue[1].replace(/\r?\n/g,"\r"))
  972. )
  973. }
  974. data=data.join("&")
  975. GM.xmlHttpRequest({
  976. method:"post",
  977. headers:{
  978. "Content-type":"application/x-www-form-urlencoded"
  979. },
  980. url:serverurl+"post.php",
  981. data:data,
  982. onload:response=>{
  983. if(response.status==200){
  984. if(/Post Successful/.test(response.responseText)){
  985. form.getElementsByTagName("textarea")[0].value=""
  986. if(mode=="thread"){
  987. getGreenPosts(threadId)
  988. }else{
  989. alert("Post successful")
  990. }
  991. }else{
  992. return postSubmitted(submit,response.status,response.responseText)
  993. }
  994. }
  995. postSubmitted(submit,response.status)
  996. },
  997. onerror:response=>{
  998. postSubmitted(submit)
  999. }
  1000. })
  1001. }
  1002.  
  1003. function postSubmitted(submit,errorCode,responseText){
  1004. if(submit.fakeButton){
  1005. submit.button.firstChild.data=submit.text
  1006. submit.button.classList.remove("greenSubmitDisabled")
  1007. }else{
  1008. submit.button.value=submit.text
  1009. submit.button.disabled=0
  1010. }
  1011. if(errorCode==200){
  1012. if(responseText){
  1013. alert("Could not submit post ("+responseText+")")
  1014. }
  1015. }else{
  1016. var alertText="Could not connect to the [s4s] interface"
  1017. if(errorCode){
  1018. alertText+=" ("+errorCode+")"
  1019. }
  1020. alert(alertText)
  1021. }
  1022. }
  1023.  
  1024. //updates native 4chan posts with whatever, atm it's only fixing links
  1025. function updatePosts() {
  1026. var posts=document.querySelectorAll('.postMessage:not(.interfaced)');
  1027. for(var i=0;i<posts.length;i++){
  1028. var post = posts[i];
  1029. post.classList.add('interfaced');
  1030. replaceInterfaceLinks(post);
  1031. }
  1032. }
  1033.  
  1034. // add listenering for when posts are inserted.
  1035. document.addEventListener('4chanParsingDone',updatePosts)
  1036. document.addEventListener('PostsInserted',updatePosts)
  1037.  
  1038. // Stylesheet
  1039. onPageLoad(_=>{
  1040.  
  1041. var stylesheet=`
  1042. .greenPostForm+form .postForm>tbody>tr:not(.rules),
  1043. #quickReply .greenPostForm+form,
  1044. #qr .greenPostForm+form,
  1045. #qr:not(.reply-to-thread) .greenToggle:not(.pressed){
  1046. display:none!important;
  1047. }
  1048. .greenPostForm .file-n-submit{
  1049. display:flex;
  1050. align-items:stretch;
  1051. justify-content:flex-end;
  1052. height:25px;
  1053. margin-top:1px;
  1054. }
  1055. .greenPostForm .file-n-submit input{
  1056. width:25%;
  1057. background:linear-gradient(to bottom,#f8f8f8,#dcdcdc) no-repeat;
  1058. border:1px solid #bbb;
  1059. border-radius:2px;
  1060. height:100%;
  1061. }
  1062. .greenPostContainer .post.reply{
  1063. background-color:#dfd!important;
  1064. border:2px solid #008000!important;
  1065. }
  1066. .greenPostContainer .postMessage{
  1067. color:#000!important;
  1068. }
  1069. .greenToggle{
  1070. font-family:monospace;
  1071. font-size:16px;
  1072. line-height:17px;
  1073. background:#ceb!important;
  1074. width:24px;
  1075. padding:0;
  1076. border:1px solid #bbb;
  1077. }
  1078. .greenPostForm input:not([type=submit]),
  1079. .greenPostForm textarea{
  1080. background-color:#dfd;
  1081. color:#000;
  1082. }
  1083. .greenToggle.pressed{
  1084. background:#6d6!important;
  1085. font-weight:bold;
  1086. color:#fff;
  1087. }
  1088. .postForm .greenToggle+input{
  1089. width:220px!important;
  1090. }
  1091. .postForm .nameFieldParent,
  1092. #quickReply .nameFieldParent{
  1093. display:flex;
  1094. flex-direction:row;
  1095. }
  1096. .postForm textarea{
  1097. width:292px;
  1098. }
  1099. #quickReply .greenToggle{
  1100. width:23px;
  1101. height:23px;
  1102. }
  1103. #quickReply .greenToggle+input{
  1104. width:273px!important;
  1105. }
  1106. .greenSubmit{
  1107. display:inline-block;
  1108. width:75px;
  1109. float:right;
  1110. padding:1px 6px;
  1111. text-align:center;
  1112. border:1px solid #adadad;
  1113. background-color:#e1e1e1;
  1114. box-sizing:border-box;
  1115. user-select:none;
  1116. font:400 13.3333px Arial,sans-serif;
  1117. font:-moz-button;
  1118. color:#000;
  1119. cursor:default;
  1120. }
  1121. .greenSubmit:hover{
  1122. border-color:#0078d7;
  1123. background-color:#e5f1fb;
  1124. }
  1125. .greenSubmit:active{
  1126. border-color:#005499;
  1127. background-color:#cce4f7;
  1128. }
  1129. .greenSubmitDisabled{
  1130. color:#808080;
  1131. pointer-events:none;
  1132. }
  1133. .greenPostCount{
  1134. color:#060;
  1135. }
  1136. .greenPostContainer .hide-reply-button{
  1137. opacity:0!important;
  1138. pointer-events:none;
  1139. }
  1140. a.newGreenPost:not(:hover) {
  1141. color: green !important;
  1142. }
  1143. .greenPostForm {
  1144. display: table;
  1145. margin: auto;
  1146. }
  1147. @media only screen and (max-width:480px){
  1148. .postForm .greenToggle+input{
  1149. width:196px!important;
  1150. }
  1151. .postForm input[type="submit"]{
  1152. width:60px;
  1153. padding:2px 4px 3px;
  1154. margin:0;
  1155. }
  1156. .postForm:not(.hideMobile){
  1157. margin-top:20px;
  1158. }
  1159. }
  1160. `.replace(/\n\s*/g,"")
  1161. element(
  1162. document.head||document.documentElement,
  1163. ["style",{
  1164. id:"s4sinterface-css"
  1165. },stylesheet]
  1166. )
  1167. })
  1168.  
  1169. function padding(string,num){
  1170. return (""+string).padStart(num,0)
  1171. }
  1172.  
  1173. function query(selector){
  1174. return document.querySelector(selector)
  1175. }
  1176.  
  1177. function queryAll(selector){
  1178. return document.querySelectorAll(selector)
  1179. }
  1180.  
  1181. function insertBefore(newElement,targetElement){
  1182. return targetElement.parentNode.insertBefore(newElement,targetElement)
  1183. }
  1184.  
  1185. function insertAfter(newElement,targetElement){
  1186. var nextSibling=targetElement.nextSibling
  1187. if(nextSibling){
  1188. return insertBefore(newElement,nextSibling)
  1189. }else{
  1190. return targetElement.parentNode.appendChild(newElement)
  1191. }
  1192. }
  1193.  
  1194. function removeChild(targetElement){
  1195. return targetElement.parentNode.removeChild(targetElement)
  1196. }
  1197.  
  1198. function element(){
  1199. var parent
  1200. var lasttag
  1201. var createdtag
  1202. var toreturn={}
  1203. for(var i=0;i<arguments.length;i++){
  1204. var current=arguments[i]
  1205. if(current){
  1206. if(current.nodeType){
  1207. parent=lasttag=current
  1208. }else if(Array.isArray(current)){
  1209. for(var j=0;j<current.length;j++){
  1210. if(current[j]){
  1211. if(!j&&typeof current[j]=="string"){
  1212. var tagname=current[0].split("#")
  1213. lasttag=createdtag=document.createElement(tagname[0])
  1214. if(tagname[1]){
  1215. toreturn[tagname[1]]=createdtag
  1216. }
  1217. }else if(current[j].constructor==Object){
  1218. if(lasttag){
  1219. for(var value in current[j]){
  1220. if(value!="style"&&value in lasttag){
  1221. lasttag[value]=current[j][value]
  1222. }else{
  1223. lasttag.setAttribute(value,current[j][value])
  1224. }
  1225. }
  1226. }
  1227. }else{
  1228. var returned=element(lasttag,current[j])
  1229. for(var k in returned){
  1230. toreturn[k]=returned[k]
  1231. }
  1232. }
  1233. }
  1234. }
  1235. }else if(current){
  1236. createdtag=document.createTextNode(current)
  1237. }
  1238. if(parent&&createdtag){
  1239. parent.appendChild(createdtag)
  1240. }
  1241. createdtag=0
  1242. }
  1243. }
  1244. return toreturn
  1245. }