// ==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 = `
`;
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];
}
})();