Sololearn Code Comments

Use comment section features on web version of Sololearn playground

As of 2022-12-27. See the latest version.

  1. // ==UserScript==
  2. // @name Sololearn Code Comments
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1
  5. // @description Use comment section features on web version of Sololearn playground
  6. // @author DonDejvo
  7. // @match https://www.sololearn.com/compiler-playground/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=sololearn.com
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (async () => {
  14. 'use strict';
  15.  
  16. class Store {
  17. static _instance;
  18.  
  19. _token;
  20. _profile;
  21.  
  22. static _get() {
  23. if (this._instance == null) {
  24. this._instance = new Store();
  25. }
  26. return this._instance;
  27. }
  28.  
  29. static async login(userId, token) {
  30. this._get()._token = token;
  31. const data = await this.postAction("https://api3.sololearn.com/Profile/GetProfile", {
  32. excludestats: true,
  33. id: userId
  34. });
  35. this._get()._profile = data.profile;
  36. }
  37.  
  38. static async postAction(url, body) {
  39. const res = await fetch(url, {
  40. headers: {
  41. "Content-Type": "application/json",
  42. "Authorization": "Bearer " + this._get()._token
  43. },
  44. referrer: "https://www.sololearn.com/",
  45. body: JSON.stringify(body),
  46. method: "POST",
  47. mode: "cors"
  48. });
  49. return await res.json();
  50. }
  51.  
  52. static get profile() {
  53. return this._get()._profile;
  54. }
  55. }
  56.  
  57. class Code {
  58. _data;
  59. _comments = [];
  60. _replies = [];
  61.  
  62. static async load(publicId) {
  63. const data = await Store.postAction("https://api3.sololearn.com/Playground/GetCode", {
  64. publicId: publicId
  65. });
  66. return new Code(data);
  67. }
  68.  
  69. constructor(data) {
  70. this._data = data;
  71. }
  72.  
  73. _getReplies(parentId) {
  74. const elem = this._replies.find(elem => elem.parentId == parentId);
  75. return elem ? elem.comments : [];
  76. }
  77.  
  78. _addReply(comment, parentId) {
  79. const elem = this._replies.find(elem => elem.parentId == parentId);
  80. if (elem) {
  81. elem.comments.push(comment);
  82. }
  83. else {
  84. this._replies.push({
  85. parentId,
  86. comments: [comment]
  87. });
  88. }
  89. }
  90.  
  91. async _loadReplies(parentId, count) {
  92. const elem = this._replies.find(elem => elem.parentId == parentId);
  93. const index = elem ? elem.comments.length : 0;
  94. const data = await Store.postAction("https://api3.sololearn.com/Discussion/GetCodeComments", {
  95. codeId: this._data.code.id,
  96. count,
  97. index,
  98. orderBy: 1,
  99. parentId
  100. });
  101. for (let comment of data.comments) {
  102. this._addReply(comment, parentId);
  103. }
  104. return data;
  105. }
  106.  
  107. _clearComments() {
  108. this._comments = [];
  109. this._replies = [];
  110. }
  111.  
  112. getComments(parentId = null) {
  113. if (parentId == null) {
  114. return this._comments;
  115. }
  116. return this._getReplies(parentId);
  117. }
  118.  
  119. async loadComments(parentId = null, count = 20) {
  120. if (parentId) {
  121. const data = await this._loadReplies(parentId, count);
  122. return data.comments;
  123. }
  124. const index = this._comments.length;
  125. const data = await Store.postAction("https://api3.sololearn.com/Discussion/GetCodeComments", {
  126. codeId: this._data.code.id,
  127. count,
  128. index,
  129. orderBy: 1,
  130. parentId
  131. });
  132. for (let comment of data.comments) {
  133. this._comments.push(comment);
  134. }
  135. return data.comments;
  136. }
  137.  
  138. async createComment(message, parentId = null) {
  139. const data = await Store.postAction("https://api3.sololearn.com/Discussion/CreateCodeComment", {
  140. codeId: this._data.code.id,
  141. message,
  142. parentId
  143. });
  144. const comment = data.comment;
  145. if (parentId) {
  146. this._addReply(comment, parentId);
  147. }
  148. else {
  149. this._comments.push(comment);
  150. }
  151. return data.comment;
  152. }
  153.  
  154. async deleteComment(id) {
  155. let toDelete;
  156. toDelete = this._comments.find(elem => elem.id == id);
  157. if (toDelete) {
  158. let idx;
  159. idx = this._comments.indexOf(toDelete);
  160. this._comments.splice(idx, 1);
  161. const elem = this._replies.find(elem => elem.parentId == id);
  162. if (elem) {
  163. idx = this._replies.indexOf(elem);
  164. this._replies.splice(idx, 1);
  165. }
  166. }
  167. else {
  168. for (let elem of this._replies) {
  169. for (let comment of elem.comments) {
  170. if (comment.id == id) {
  171. const idx = elem.comments.indexOf(comment);
  172. elem.comments.splice(idx, 1);
  173. }
  174. }
  175. }
  176. }
  177. await Store.postAction("https://api3.sololearn.com/Discussion/DeleteCodeComment", {
  178. id
  179. });
  180. }
  181.  
  182. async editComment(message, id) {
  183. await Store.postAction("https://api3.sololearn.com/Discussion/EditCodeComment", {
  184. id,
  185. message
  186. });
  187.  
  188. }
  189.  
  190. render(root) {
  191. const modal = document.createElement("div");
  192. modal.style.display = "flex";
  193. modal.style.position = "absolute";
  194. modal.style.zIndex = 9999;
  195. modal.style.left = "0";
  196. modal.style.top = "0";
  197. modal.style.width = "100%";
  198. modal.style.height = "100%";
  199. modal.style.backgroundColor = "rgba(128, 128, 128, 0.5)";
  200. modal.style.alignItems = "center";
  201. modal.style.justifyContent = "center";
  202.  
  203. const container = document.createElement("div");
  204. container.style.position = "relative";
  205. container.style.width = "600px";
  206. container.style.height = "800px";
  207. container.style.backgroundColor = "#fff";
  208. container.style.padding = "18px 12px";
  209. modal.appendChild(container);
  210.  
  211. const closeBtn = document.createElement("button");
  212. closeBtn.innerHTML = "×";
  213. closeBtn.style.position = "absolute";
  214. closeBtn.style.right = "0";
  215. closeBtn.style.top = "0";
  216. closeBtn.addEventListener("click", () => {
  217. modal.style.display = "none";
  218. });
  219.  
  220. const title = document.createElement("h1");
  221. title.textContent = this._data.code.comments + " comments";
  222. title.style.textAlign = "center";
  223. container.appendChild(title);
  224. container.appendChild(closeBtn);
  225.  
  226. const commentsBody = document.createElement("div");
  227. commentsBody.style.width = "100%";
  228. commentsBody.style.height = "calc(100% - 60px)";
  229. commentsBody.style.overflowY = "auto";
  230. container.appendChild(commentsBody);
  231.  
  232. const renderCreateCommentForm = () => {
  233. const createCommentForm = document.createElement("div");
  234. createCommentForm.style.display = "none";
  235. createCommentForm.style.position = "absolute";
  236. createCommentForm.style.width = "100%";
  237.  
  238. const input = document.createElement("textarea");
  239. input.style.width = "100%";
  240. input.style.height = "120px";
  241. input.placeholder = "Write your comment here...";
  242. createCommentForm.appendChild(input);
  243.  
  244. const buttonContainer = document.createElement("div");
  245. createCommentForm.appendChild(buttonContainer);
  246.  
  247. const postButton = document.createElement("button");
  248. buttonContainer.appendChild(postButton);
  249. postButton.textContent = "Post";
  250.  
  251. const cancelButton = document.createElement("button");
  252. buttonContainer.appendChild(cancelButton);
  253. cancelButton.textContent = "Cancel";
  254.  
  255. return {
  256. createCommentForm,
  257. input,
  258. postButton,
  259. cancelButton
  260. };
  261. }
  262.  
  263. const createComment = (comment) => {
  264. const container = document.createElement("div");
  265. container.style.width = "100%";
  266.  
  267. const m = new Date(comment.date);
  268. const dateString = m.getUTCFullYear() + "/" +
  269. ("0" + (m.getUTCMonth() + 1)).slice(-2) + "/" +
  270. ("0" + m.getUTCDate()).slice(-2) + " " +
  271. ("0" + m.getUTCHours()).slice(-2) + ":" +
  272. ("0" + m.getUTCMinutes()).slice(-2) + ":" +
  273. ("0" + m.getUTCSeconds()).slice(-2);
  274.  
  275. container.innerHTML = `<div style="display:flex; gap: 6px; padding: 6px 8px; margin-bottom: 8px;">
  276. <img style="width: 64px; height: 64px; border-radius: 50%; overflow: hidden; flex-shrink: 0;" src="${comment.avatarUrl}" alt="${comment.userName} - avatar">
  277. <div style="display: flex; flex-direction: column; flex-grow: 1;">
  278. <div style="display: flex; direction: row; justify-content: space-between;">
  279. <div>${comment.userName}</div>
  280. <div>${dateString}</div>
  281. </div>
  282. <div style="white-space: pre-wrap;">${comment.message.trim().replace(/</g, "&lt;").replace(/>/g, "&gt;")}</div>
  283. <div style="display: flex; justify-content: flex-end;">
  284. <div style="display: flex; gap: 4px;">
  285. <button ${comment.parentID === null ? "" : "disabled"} data-id="${comment.id}" class="toggle-replies-btn">${comment.replies} replies</button>
  286. <button ${comment.parentID === null ? "" : "disabled"} data-id="${comment.id}" class="reply-btn">Reply</button>
  287. </div>
  288. </div>
  289. </div>
  290. </div>
  291. <div data-id="${comment.id}" class="replies" style="display: none; border-top: 1px solid #000; border-bottom: 1px solid #000; padding: 4px 0;"></div>
  292. `;
  293.  
  294. return container;
  295. }
  296.  
  297. const renderLoadButton = (parentId, body) => {
  298. const container = document.createElement("button");
  299. container.textContent = "...";
  300. container.addEventListener("click", () => {
  301. body.removeChild(container);
  302. loadComments(body, parentId);
  303. });
  304. body.appendChild(container);
  305. }
  306.  
  307. const loadComments = (body, parentId = null) => {
  308. this.loadComments(parentId)
  309. .then(comments => {
  310. for (let comment of comments) {
  311. body.append(createComment(comment));
  312. }
  313. if (comments.length) {
  314. renderLoadButton(parentId, body);
  315. }
  316. });
  317. }
  318.  
  319. const { createCommentForm, input, postButton, cancelButton } = renderCreateCommentForm();
  320. container.appendChild(createCommentForm);
  321.  
  322. const openCommentForm = (parentId = null) => {
  323. createCommentForm.style.display = "block";
  324. createCommentForm.dataset.parentId = parentId;
  325. }
  326.  
  327. const getRepliesContainer = (commentId) => {
  328. let out = null;
  329. const replies = document.querySelectorAll(".replies");
  330. replies.forEach(elem => {
  331. if (commentId == elem.dataset.id) {
  332. out = elem;
  333. }
  334. });
  335. return out;
  336. }
  337.  
  338. const showCommentFormButton = document.createElement("button");
  339. showCommentFormButton.textContent = "Post comment";
  340. container.appendChild(showCommentFormButton);
  341. showCommentFormButton.addEventListener("click", () => openCommentForm());
  342.  
  343. const postComment = () => {
  344. const parentId = createCommentForm.dataset.parentId == "null" ? null : +createCommentForm.dataset.parentId;
  345. this.createComment(input.value, parentId)
  346. .then(comment => {
  347. input.value = "";
  348. createCommentForm.style.display = "none";
  349. comment.userName = Store.profile.name;
  350. comment.avatarUrl = Store.profile.avatarUrl;
  351. comment.replies = 0;
  352. if (parentId === null) {
  353. commentsBody.prepend(createComment(comment));
  354. }
  355. else {
  356. getRepliesContainer(parentId).append(createComment(comment));
  357. const toggleReplyButtons = document.querySelectorAll(".toggle-replies-btn");
  358. toggleReplyButtons.forEach(elem => {
  359. if (parentId == elem.dataset.id) {
  360. elem.textContent = (+elem.textContent.split(" ")[0] + 1) + " replies";
  361. }
  362. });
  363. }
  364. });
  365. }
  366.  
  367. postButton.addEventListener("click", () => postComment());
  368. cancelButton.addEventListener("click", () => createCommentForm.style.display = "none");
  369.  
  370. loadComments(commentsBody);
  371.  
  372. root.appendChild(modal);
  373.  
  374. addEventListener("click", ev => {
  375. if (ev.target.classList.contains("toggle-replies-btn")) {
  376. const elem = getRepliesContainer(ev.target.dataset.id);
  377. if (elem.classList.contains("replies_opened")) {
  378. elem.style.display = "none";
  379. }
  380. else {
  381. elem.style.display = "block";
  382. loadComments(elem, ev.target.dataset.id);
  383. }
  384. elem.classList.toggle("replies_opened");
  385. }
  386. else if (ev.target.classList.contains("reply-btn")) {
  387. const elem = getRepliesContainer(ev.target.dataset.id);
  388. if (!elem.classList.contains("replies_opened")) {
  389. elem.style.display = "block";
  390. loadComments(elem, ev.target.dataset.id);
  391. elem.classList.add("replies_opened");
  392. }
  393.  
  394. openCommentForm(ev.target.dataset.id);
  395. }
  396. });
  397. return modal;
  398. }
  399.  
  400. }
  401.  
  402. const main = async () => {
  403.  
  404. const userId = JSON.parse(localStorage.getItem("user")).data.id;
  405. const accessToken = JSON.parse(localStorage.getItem("accessToken")).data;
  406. const publicId = window.location.pathname.split("/")[2];
  407.  
  408. await Store.login(
  409. userId,
  410. accessToken
  411. );
  412.  
  413. const code = await Code.load(publicId);
  414. const modal = code.render(document.querySelector(".sl-playground-wrapper"));
  415. modal.style.display = "none";
  416.  
  417. const openModalButton = document.createElement("button");
  418. openModalButton.textContent = "Show comments";
  419. openModalButton.addEventListener("click", () => modal.style.display = "flex");
  420. document.querySelector(".sl-playground-left").appendChild(openModalButton);
  421. }
  422.  
  423. setTimeout(main, 1000);
  424.  
  425. function getCookie(cookieName) {
  426. let cookie = {};
  427. document.cookie.split(';').forEach(function(el) {
  428. let [key,value] = el.split('=');
  429. cookie[key.trim()] = value;
  430. });
  431. return cookie[cookieName];
  432. }
  433.  
  434. })();