// ==UserScript== // @name Sololearn comments for code playground // @namespace http://tampermonkey.net/ // @version 1.1 // @description View and write comments in code playground // @author DonDejvo // @match https://www.sololearn.com/compiler-playground/* // @icon https://www.google.com/s2/favicons?sz=64&domain=sololearn.com // @grant none // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/457182/Sololearn%20comments%20for%20code%20playground.user.js // @updateURL https://update.greasyfork.icu/scripts/457182/Sololearn%20comments%20for%20code%20playground.meta.js // ==/UserScript== (async () => { 'use strict'; class Store { static _instance; _token; _profile; static _get() { if (this._instance == null) { this._instance = new Store(); } return this._instance; } static async login(userId, token) { this._get()._token = token; const data = await this.postAction("https://api3.sololearn.com/Profile/GetProfile", { excludestats: true, id: userId }); this._get()._profile = data.profile; } static async postAction(url, body) { const res = await fetch(url, { headers: { "Content-Type": "application/json", "Authorization": "Bearer " + this._get()._token }, referrer: "https://www.sololearn.com/", body: JSON.stringify(body), method: "POST", mode: "cors" }); return await res.json(); } static get profile() { return this._get()._profile; } } class Code { _data; _comments = []; _replies = []; static async load(publicId) { const data = await Store.postAction("https://api3.sololearn.com/Playground/GetCode", { publicId: publicId }); return new Code(data); } constructor(data) { this._data = data; } _getReplies(parentId) { const elem = this._replies.find(elem => elem.parentId == parentId); return elem ? elem.comments : []; } _addReply(comment, parentId) { const elem = this._replies.find(elem => elem.parentId == parentId); if (elem) { elem.comments.push(comment); } else { this._replies.push({ parentId, comments: [comment] }); } } async _loadReplies(parentId, count) { const elem = this._replies.find(elem => elem.parentId == parentId); const index = elem ? elem.comments.length : 0; const data = await Store.postAction("https://api3.sololearn.com/Discussion/GetCodeComments", { codeId: this._data.code.id, count, index, orderBy: 1, parentId }); for (let comment of data.comments) { this._addReply(comment, parentId); } return data; } _clearComments() { this._comments = []; this._replies = []; } _getCommentById(id) { let comment = this._comments.find(elem => elem.id == id); if(!comment) { for(let reply of this._replies) { comment = reply.comments.find(elem => elem.id == id); if(comment) { break; } } } return comment; } _getMentionString(id) { const comment = this._getCommentById(id); return `[user id="${comment.userID}"]${comment.userName}[/user]`; } getComments(parentId = null) { if (parentId == null) { return this._comments; } return this._getReplies(parentId); } async loadComments(parentId = null, count = 20) { if (parentId) { const data = await this._loadReplies(parentId, count); return data.comments; } const index = this._comments.length; const data = await Store.postAction("https://api3.sololearn.com/Discussion/GetCodeComments", { codeId: this._data.code.id, count, index, orderBy: 1, parentId }); for (let comment of data.comments) { this._comments.push(comment); } return data.comments; } async createComment(message, parentId = null) { const data = await Store.postAction("https://api3.sololearn.com/Discussion/CreateCodeComment", { codeId: this._data.code.id, message, parentId }); const comment = data.comment; if (parentId) { this._addReply(comment, parentId); } else { this._comments.push(comment); } return data.comment; } async deleteComment(id) { let toDelete; toDelete = this._comments.find(elem => elem.id == id); if (toDelete) { let idx; idx = this._comments.indexOf(toDelete); this._comments.splice(idx, 1); const elem = this._replies.find(elem => elem.parentId == id); if (elem) { idx = this._replies.indexOf(elem); this._replies.splice(idx, 1); } } else { for (let elem of this._replies) { for (let comment of elem.comments) { if (comment.id == id) { const idx = elem.comments.indexOf(comment); elem.comments.splice(idx, 1); } } } } await Store.postAction("https://api3.sololearn.com/Discussion/DeleteCodeComment", { id }); } async editComment(message, id) { const comment = this._getCommentById(id); comment.message = message; const data = await Store.postAction("https://api3.sololearn.com/Discussion/EditCodeComment", { id, message }); return data.comment; } render(root) { const modal = document.createElement("div"); modal.style.display = "flex"; modal.style.position = "absolute"; modal.style.zIndex = 9999; modal.style.left = "0"; modal.style.top = "0"; modal.style.width = "100%"; modal.style.height = "100%"; modal.style.backgroundColor = "rgba(128, 128, 128, 0.5)"; modal.style.alignItems = "center"; modal.style.justifyContent = "center"; const container = document.createElement("div"); container.style.position = "relative"; container.style.display = "flex"; container.style.flexDirection = "column"; container.style.gap = "8px"; container.style.width = "800px"; container.style.height = "800px"; container.style.maxHeight = "80vh"; container.style.backgroundColor = "#fff"; container.style.padding = "18px 12px"; container.style.borderRadius = "8px"; modal.appendChild(container); const closeBtn = document.createElement("div"); closeBtn.innerHTML = `
`; closeBtn.style.margin = "12px"; closeBtn.style.position = "absolute"; closeBtn.style.right = "0"; closeBtn.style.top = "0"; closeBtn.addEventListener("click", () => { modal.style.display = "none"; }); container.appendChild(closeBtn); const title = document.createElement("h1"); title.textContent = this._data.code.comments + " comments"; title.style.textAlign = "center"; title.style.fontSize = "20px"; container.appendChild(title); const commentsBody = document.createElement("div"); commentsBody.style.width = "100%"; commentsBody.style.height = "calc(100% - 64px)"; commentsBody.style.overflowY = "auto"; commentsBody.style.display = "flex"; commentsBody.style.flexDirection = "column"; commentsBody.style.gap = "6px"; container.appendChild(commentsBody); const showCommentFormButton = document.createElement("button"); showCommentFormButton.textContent = "Write comment"; showCommentFormButton.classList.add("sol-button", "sol-button-primary", "sol-button-block", "sol-button-s"); container.appendChild(showCommentFormButton); const renderCreateCommentForm = () => { const createCommentForm = document.createElement("div"); createCommentForm.style.display = "none"; createCommentForm.style.width = "100%"; createCommentForm.style.padding = "8px 16px"; createCommentForm.style.backgroundColor = "#f2f5f7"; createCommentForm.style.borderRadius = "8px"; const input = document.createElement("textarea"); input.style.border = "1px solid #c8d2db"; input.style.borderRadius = "4px"; input.style.padding = "10px"; input.style.resize = "none"; input.style.width = "100%"; input.style.height = "100px"; input.placeholder = "Write your comment here..."; createCommentForm.appendChild(input); const buttonContainer = document.createElement("div"); buttonContainer.style.display = "flex"; buttonContainer.style.gap = "8px"; buttonContainer.style.marginTop = "6px"; createCommentForm.appendChild(buttonContainer); const postButton = document.createElement("button"); postButton.classList.add("sol-button", "sol-button-primary", "sol-button-block", "sol-button-s"); buttonContainer.appendChild(postButton); postButton.textContent = "Submit"; const cancelButton = document.createElement("button"); cancelButton.classList.add("sol-button", "sol-button-primary", "sol-button-block", "sol-button-s"); buttonContainer.appendChild(cancelButton); cancelButton.textContent = "Cancel"; return { createCommentForm, input, postButton, cancelButton }; } let highlightedCommentId = null; const unhighlightAllComments = () => { if(highlightedCommentId !== null) { const commentBodies = document.querySelectorAll(".comment-body"); commentBodies.forEach(elem => { if(elem.parentElement.dataset.id == highlightedCommentId) { elem.style.backgroundColor = "#fff"; elem.style.border = "none"; } }); highlightedCommentId = null; } } const highlightComment = (id) => { unhighlightAllComments(); const commentBodies = document.querySelectorAll(".comment-body"); commentBodies.forEach(elem => { if(id == elem.parentElement.dataset.id) { elem.style.backgroundColor = "rgba(20, 158, 242, 0.1)"; elem.style.border = "2px solid #149ef2"; highlightedCommentId = id; } }); } const createComment = (comment) => { const container = document.createElement("div"); container.classList.add("comment"); container.dataset.id = comment.id; container.style.width = "100%"; const m = new Date(comment.date); const dateString = m.getUTCFullYear() + "/" + ("0" + (m.getUTCMonth() + 1)).slice(-2) + "/" + ("0" + m.getUTCDate()).slice(-2) + " " + ("0" + m.getUTCHours()).slice(-2) + ":" + ("0" + m.getUTCMinutes()).slice(-2) + ":" + ("0" + m.getUTCSeconds()).slice(-2); let html = `
${comment.userName} - avatar
${comment.userName}
${dateString}
${comment.message.trim().replace(//g, ">")}
`; if(comment.parentID === null) { html += ``; } container.innerHTML = html; return container; } const renderLoadButton = (parentId, body) => { const container = document.createElement("button"); container.textContent = "..."; container.classList.add("sol-button", "sl-action-button--secondary--dark", "sol-button-secondary", "sol-button-block", "sol-button-s"); container.style.alignSelf = "flex-start"; container.addEventListener("click", () => { body.removeChild(container); loadComments(body, parentId); }); body.appendChild(container); } const loadComments = (body, parentId = null) => { this.loadComments(parentId) .then(comments => { for (let comment of comments) { body.append(createComment(comment)); } if (comments.length) { renderLoadButton(parentId, body); } }); } const { createCommentForm, input, postButton, cancelButton } = renderCreateCommentForm(); container.appendChild(createCommentForm); const openCommentForm = (parentId = null, edit = false) => { showCommentFormButton.style.display = "none"; createCommentForm.style.display = "block"; createCommentForm.dataset.parentId = parentId; createCommentForm.dataset.edit = edit; if(edit) { const comment = this._getCommentById(parentId); input.value = comment.message; highlightComment(parentId); } else { input.value = parentId === null ? "" : this._getMentionString(parentId) + " "; if(parentId) { highlightComment(parentId); } } } const closeCommentForm = () => { input.value = ""; createCommentForm.style.display = "none"; unhighlightAllComments(); showCommentFormButton.style.display = "block"; } const getRepliesContainer = (commentId) => { let out = null; const replies = document.querySelectorAll(".replies"); replies.forEach(elem => { if (commentId == elem.dataset.id) { out = elem; } }); return out; } showCommentFormButton.addEventListener("click", () => openCommentForm()); const postComment = () => { let parentId = createCommentForm.dataset.parentId == "null" ? null : +createCommentForm.dataset.parentId; if(parentId !== null) { const comment = this._getCommentById(parentId); if(comment.parentID !== null) { parentId = comment.parentID; } } this.createComment(input.value, parentId) .then(comment => { closeCommentForm(); comment.userName = Store.profile.name; comment.avatarUrl = Store.profile.avatarUrl; comment.replies = 0; if (parentId === null) { commentsBody.prepend(createComment(comment)); } else { const replies = getRepliesContainer(parentId); if(replies.childElementCount > 0 && replies.lastChild instanceof HTMLButtonElement) { replies.lastChild.before(createComment(comment)); } else { replies.append(createComment(comment)); } const toggleReplyButtons = document.querySelectorAll(".toggle-replies-btn"); toggleReplyButtons.forEach(elem => { if (parentId == elem.dataset.id) { elem.textContent = (+elem.textContent.split(" ")[0] + 1) + " replies"; } }); } }); } const editComment = () => { const parentId = +createCommentForm.dataset.parentId; this.editComment(input.value, parentId) .then(comment => { const comments = document.querySelectorAll(".comment"); comments.forEach(elem => { if(elem.dataset.id == parentId) { const messageContainer = elem.querySelector(".comment-message"); messageContainer.textContent = comment.message.trim().replace(//g, ">"); } }); closeCommentForm(); }); } postButton.addEventListener("click", () => { const edited = createCommentForm.dataset.edit === "true"; if(edited) { editComment(); } else { postComment(); } }); cancelButton.addEventListener("click", () => { closeCommentForm(); }); loadComments(commentsBody); root.appendChild(modal); addEventListener("click", ev => { if (ev.target.classList.contains("toggle-replies-btn")) { const elem = getRepliesContainer(ev.target.dataset.id); if (elem.classList.contains("replies_opened")) { elem.style.display = "none"; } else { elem.style.display = "flex"; loadComments(elem, ev.target.dataset.id); } elem.classList.toggle("replies_opened"); } else if (ev.target.classList.contains("reply-btn")) { const elem = getRepliesContainer(ev.target.dataset.id); if (!elem.classList.contains("replies_opened")) { elem.style.display = "flex"; loadComments(elem, ev.target.dataset.id); elem.classList.add("replies_opened"); } openCommentForm(ev.target.dataset.id); } else if(ev.target.classList.contains("edit-comment-btn")) { openCommentForm(ev.target.dataset.id, true); } else if(ev.target.classList.contains("delete-comment-btn")) { closeCommentForm(); highlightComment(ev.target.dataset.id); if(confirm("Are you sure you want to delete the comment?")) { const parentId = this._getCommentById(ev.target.dataset.id).parentID; this.deleteComment(ev.target.dataset.id) .then(() => { const commentElements = document.querySelectorAll(".comment"); commentElements.forEach(elem => { if(elem.dataset.id == ev.target.dataset.id) { elem.parentElement.removeChild(elem); } }); if(parentId !== null) { const toggleReplyButtons = document.querySelectorAll(".toggle-replies-btn"); toggleReplyButtons.forEach(elem => { if (parentId == elem.dataset.id) { elem.textContent = (+elem.textContent.split(" ")[0] - 1) + " replies"; } }); } }); } else { unhighlightAllComments(); } } }); return modal; } } const main = async () => { const userId = JSON.parse(localStorage.getItem("user")).data.id; const accessToken = JSON.parse(localStorage.getItem("accessToken")).data; const publicId = window.location.pathname.split("/")[2]; await Store.login( userId, accessToken ); const code = await Code.load(publicId); const modal = code.render(document.querySelector(".sl-playground-wrapper")); modal.style.display = "none"; const openModalButton = document.createElement("button"); openModalButton.classList.add("sol-button", "sol-button-primary", "sol-button-block", "sol-button-s"); openModalButton.style.marginLeft = "12px"; openModalButton.textContent = "Show comments"; openModalButton.addEventListener("click", () => modal.style.display = "flex"); document.querySelector(".sl-playground-left").appendChild(openModalButton); } setTimeout(main, 1000); function getCookie(cookieName) { let cookie = {}; document.cookie.split(';').forEach(function(el) { let [key,value] = el.split('='); cookie[key.trim()] = value; }); return cookie[cookieName]; } })();