// ==UserScript==
// @name Sololearn Code Comments
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Use comment section features on web version of Sololearn 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
// ==/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 = [];
}
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) {
await Store.postAction("https://api3.sololearn.com/Discussion/EditCodeComment", {
id,
message
});
}
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.width = "600px";
container.style.height = "800px";
container.style.backgroundColor = "#fff";
container.style.padding = "18px 12px";
modal.appendChild(container);
const closeBtn = document.createElement("button");
closeBtn.innerHTML = "×";
closeBtn.style.position = "absolute";
closeBtn.style.right = "0";
closeBtn.style.top = "0";
closeBtn.addEventListener("click", () => {
modal.style.display = "none";
});
const title = document.createElement("h1");
title.textContent = this._data.code.comments + " comments";
title.style.textAlign = "center";
container.appendChild(title);
container.appendChild(closeBtn);
const commentsBody = document.createElement("div");
commentsBody.style.width = "100%";
commentsBody.style.height = "calc(100% - 60px)";
commentsBody.style.overflowY = "auto";
container.appendChild(commentsBody);
const renderCreateCommentForm = () => {
const createCommentForm = document.createElement("div");
createCommentForm.style.display = "none";
createCommentForm.style.position = "absolute";
createCommentForm.style.width = "100%";
const input = document.createElement("textarea");
input.style.width = "100%";
input.style.height = "120px";
input.placeholder = "Write your comment here...";
createCommentForm.appendChild(input);
const buttonContainer = document.createElement("div");
createCommentForm.appendChild(buttonContainer);
const postButton = document.createElement("button");
buttonContainer.appendChild(postButton);
postButton.textContent = "Post";
const cancelButton = document.createElement("button");
buttonContainer.appendChild(cancelButton);
cancelButton.textContent = "Cancel";
return {
createCommentForm,
input,
postButton,
cancelButton
};
}
const createComment = (comment) => {
const container = document.createElement("div");
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);
container.innerHTML = `<div style="display:flex; gap: 6px; padding: 6px 8px; margin-bottom: 8px;">
<img style="width: 64px; height: 64px; border-radius: 50%; overflow: hidden; flex-shrink: 0;" src="${comment.avatarUrl}" alt="${comment.userName} - avatar">
<div style="display: flex; flex-direction: column; flex-grow: 1;">
<div style="display: flex; direction: row; justify-content: space-between;">
<div>${comment.userName}</div>
<div>${dateString}</div>
</div>
<div style="white-space: pre-wrap;">${comment.message.trim().replace(/</g, "<").replace(/>/g, ">")}</div>
<div style="display: flex; justify-content: flex-end;">
<div style="display: flex; gap: 4px;">
<button ${comment.parentID === null ? "" : "disabled"} data-id="${comment.id}" class="toggle-replies-btn">${comment.replies} replies</button>
<button ${comment.parentID === null ? "" : "disabled"} data-id="${comment.id}" class="reply-btn">Reply</button>
</div>
</div>
</div>
</div>
<div data-id="${comment.id}" class="replies" style="display: none; border-top: 1px solid #000; border-bottom: 1px solid #000; padding: 4px 0;"></div>
`;
return container;
}
const renderLoadButton = (parentId, body) => {
const container = document.createElement("button");
container.textContent = "...";
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) => {
createCommentForm.style.display = "block";
createCommentForm.dataset.parentId = parentId;
}
const getRepliesContainer = (commentId) => {
let out = null;
const replies = document.querySelectorAll(".replies");
replies.forEach(elem => {
if (commentId == elem.dataset.id) {
out = elem;
}
});
return out;
}
const showCommentFormButton = document.createElement("button");
showCommentFormButton.textContent = "Post comment";
container.appendChild(showCommentFormButton);
showCommentFormButton.addEventListener("click", () => openCommentForm());
const postComment = () => {
const parentId = createCommentForm.dataset.parentId == "null" ? null : +createCommentForm.dataset.parentId;
this.createComment(input.value, parentId)
.then(comment => {
input.value = "";
createCommentForm.style.display = "none";
comment.userName = Store.profile.name;
comment.avatarUrl = Store.profile.avatarUrl;
comment.replies = 0;
if (parentId === null) {
commentsBody.prepend(createComment(comment));
}
else {
getRepliesContainer(parentId).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";
}
});
}
});
}
postButton.addEventListener("click", () => postComment());
cancelButton.addEventListener("click", () => createCommentForm.style.display = "none");
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 = "block";
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 = "block";
loadComments(elem, ev.target.dataset.id);
elem.classList.add("replies_opened");
}
openCommentForm(ev.target.dataset.id);
}
});
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.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];
}
})();