diff --git a/css/templates/stackexchange.css b/css/templates/stackexchange.css new file mode 100644 index 0000000..b570eb2 --- /dev/null +++ b/css/templates/stackexchange.css @@ -0,0 +1,7 @@ +.survol-code-preview { + background-color: #1c1b1b; + color: white; + padding: 8px; + margin-top: 8px; + margin-bottom: 8px; +} \ No newline at end of file diff --git a/js/core.js b/js/core.js index d66ad55..29bda9a 100644 --- a/js/core.js +++ b/js/core.js @@ -101,8 +101,11 @@ document.addEventListener('DOMContentLoaded', () => { return new YoutubeHover(node, getDomain(CURRENT_TAB)); case 'twitter.com': return new TwitterHover(node, getDomain(CURRENT_TAB)); + case 'stackoverflow.com': + return new StackExchangeHover(node, getDomain(CURRENT_TAB)); case 'soundcloud.com': return new SoundCloudHover(node, getDomain(CURRENT_TAB)); + default: return previewMetadata ? new BaseHover(node, getDomain(CURRENT_TAB)) : null; //return new BaseHover(node); diff --git a/js/templates/base.js b/js/templates/base.js index 3726e95..d3f6401 100644 --- a/js/templates/base.js +++ b/js/templates/base.js @@ -14,7 +14,6 @@ class BaseHover { .then((res) => { let parser = new DOMParser(); let doc = parser.parseFromString(res.data, 'text/html'); - let title = doc.querySelector('title') ? doc.querySelector('title').innerText : null; let description = doc.querySelector('meta[name="description"]') ? doc.querySelector('meta[name="description"]').content : null; let thumbnail = doc.querySelector('meta[name="og:image"]') ? doc.querySelector('meta[name="og:image"]').content : null; diff --git a/js/templates/reddit.js b/js/templates/reddit.js index 9e2943f..ea71a3d 100644 --- a/js/templates/reddit.js +++ b/js/templates/reddit.js @@ -111,7 +111,7 @@ class RedditHover { const authorBold = document.createElement('b'); const authorName = document.createTextNode(`u/${json.author}`); - author.append(postedBy, authorBold, authorName) + author.append(postedBy, authorBold, authorName); return author; } @@ -125,6 +125,7 @@ class RedditHover { return subredditLink; } + static getUpVotes(json) { const scoreCommentDisplay = document.createElement('div'); const upvoteImage = document.createElement('img'); @@ -135,20 +136,24 @@ class RedditHover { scoreCommentDisplay.appendChild(document.createTextNode(` ${json.score} `)); return scoreCommentDisplay; } + static getDatePosted(json) { const date = this.timeSinceCreation(json.created_utc); if (date.time == 1) { date.unit = date.unit.substr(0, date.unit.length - 1); } + return document.createTextNode(date.time + ' ' + date.unit + ' ago'); } + static getPostTitle(json) { const title = document.createElement('b'); title.className = 'survol-reddit-post-title'; title.appendChild(document.createTextNode(json.title)); return title; } + static getPostImage(json) { const image = document.createElement('img'); image.classList.add('survol-reddit-image'); diff --git a/js/templates/stackexchange.js b/js/templates/stackexchange.js new file mode 100644 index 0000000..3b0af67 --- /dev/null +++ b/js/templates/stackexchange.js @@ -0,0 +1,237 @@ +/* Hover classes bound themselves to a node + */ +class StackExchangeHover { + constructor(node, CURRENT_TAB) { + this.boundNode = node; + this.redirectLink = node.href; + this.CURRENT_TAB = CURRENT_TAB; + this.linkType = this.checkLinkType(); + } + + /* Takes {String} link + * Returns {String} link + * Description: Returns the domain name associated to a full link + */ + getDomain(link) { + let subdomains = link.replace('http://', '').replace('https://', '').split('/')[0].split('.').length; + return link.replace('http://', '').replace('https://', '').split('/')[0].split('.').slice(subdomains - 2, subdomains).join('.'); + } + + /* Description: This function is unique to every Hover class, + * its goal is to get how many accepted answer are they . + * it can also give the code of the accepted answer. + */ + checkLinkType() { + // TODO: Make sure the website is not part of the stackexchange network and not limit ourselves to stackoverflow + if (this.CURRENT_TAB != 'stackoverflow.com' && this.redirectLink.includes('/questions/')) { + return 'question'; + } else { + return 'unknown'; + } + } + + bindToContainer(node, domain, container) { + + + if (this.linkType == 'question') { + + // Get the question ID and website from the URL + const questionID = node.href.split('/questions/')[1].split('/')[0]; + const site = this.getDomain(node.href).split('.')[0]; + + if (questionID) { + + // Build the API request links + // Get a question GET /questions/{ids} - + const QUESTION_URL = `https://api.stackexchange.com/2.2/questions/${questionID}?order=desc&sort=activity&site=${site}&filter=withBody`; + + // Get answers to a question GET /questions/{ids}/answers - + const ANSWERS_URL = `https://api.stackexchange.com/2.2/questions/${questionID}/answers?order=desc&sort=activity&site=${site}&filter=withBody`; + + window + .survolBackgroundRequest(QUESTION_URL) + .then((res) => { + // Make sure the question exists + if (res.data && res.data.items && res.data.items[0]) { + + let data = { + question: res.data.items[0] + }; + + // If the question has been answered query the answer + if (res.data.items[0].is_answered) { + window + .survolBackgroundRequest(ANSWERS_URL) + .then((res) => { + + // Make sure we can get the answers + if (res.data && res.data.items) { + data.answers = res.data.items; + } + + this.generateEmbed(container, node, data); + }); + } + + // Only display the question + else { + this.generateEmbed(container, node, data); + } + } + }) + .catch(console.error); + } + } + } + + timeSinceCreation(timestamp) { + let secondsSinceCreation = Math.floor(Date.now() / 1000) - timestamp; + + if (secondsSinceCreation < 60) { + return { time: secondsSinceCreation, unit: 'seconds' }; + } else if (secondsSinceCreation < 60 * 60) { + return { time: Math.floor(secondsSinceCreation / 60), unit: 'minutes' }; + } else if (secondsSinceCreation < 60 * 60 * 24) { + return { time: Math.floor(secondsSinceCreation / 60 / 60), unit: 'hours' }; + } else if (secondsSinceCreation < 60 * 60 * 24 * 30) { + return { time: Math.floor(secondsSinceCreation / 60 / 60 / 24), unit: 'days' }; + } else if (secondsSinceCreation < 60 * 60 * 24 * 365) { + return { time: Math.floor(secondsSinceCreation / 60 / 60 / 24 / 30), unit: 'months' }; + } else { + return { time: Math.floor(secondsSinceCreation / 60 / 60 / 24 / 365 * 10) / 10, unit: 'years' }; + } + } + + // + stripHTML(html) { + let doc = new DOMParser().parseFromString(html, 'text/html'); + return doc.body.textContent || ''; + } + + generateEmbed(container, node, data) { + console.log('Generating embed', data); + + + // Re-using reddit code + const generated = document.createElement('div'); + generated.classList.add('survol-reddit-container'); + + const divider = document.createElement('div'); + divider.className = 'survol-divider'; + + const footer = document.createElement('div'); + footer.className = 'survol-reddit-footer'; + + const postDetails = document.createElement('span'); + postDetails.className = 'survol-reddit-post-details'; + + const author = document.createElement('span'); + author.className = 'survol-reddit-author'; + + const title = document.createElement('strong'); + title.append(document.createTextNode(data.question.title)); + + author.append(title); + + const subredditLink = document.createElement('a'); + subredditLink.appendChild(document.createTextNode(`${data.question.answer_count} answers`)); + + const scoreCommentDisplay = document.createElement('div'); + const upvoteImage = document.createElement('img'); + upvoteImage.src = chrome.extension.getURL('images/upvote.png'); + upvoteImage.className = 'survol-reddit-upvote-icon'; + + scoreCommentDisplay.appendChild(upvoteImage); + scoreCommentDisplay.appendChild(document.createTextNode(` ${data.question.score} `)); + + const date = this.timeSinceCreation(data.question.last_activity_date); + + if (date.time == 1) { + date.unit = date.unit.substr(0, date.unit.length - 1); + } + + const datePosted = document.createTextNode(`Last activity : ${date.time} ${date.unit} ago`); + + let commentText = ''; + let acceptedAnswer = null; + + if (data.answers) { + acceptedAnswer = data.answers.filter((answers) => { return answers.is_accepted; })[0]; + } + + if (acceptedAnswer) { + commentText = document.createElement('div'); + let answerContainer = document.createElement('div'); + let codeDiv = null; + + // To have some insight about the text after splitting + let firstIndex = acceptedAnswer.body.startsWith('') ? 0 : 1; + + // Splitting the body to extract code + let splitted = []; + let answerBody = acceptedAnswer.body; + answerBody = answerBody.split(''); + + answerBody.forEach((part) => { + if (part.split('')[0]) { + splitted.push(part.split('')[0]); + } + + if (part.split('')[1]) { + splitted.push(part.split('')[1]); + } + }); + + splitted.forEach((text, index) => { + if (firstIndex == 0) { + // Code + if (index % 2 == 1) { + answerContainer.appendChild(document.createTextNode(this.stripHTML(text))); + } + + // Normal text + else { + codeDiv = document.createElement('div'); + codeDiv.className = 'survol-code-preview'; + codeDiv.appendChild(document.createTextNode(text)); + answerContainer.appendChild(codeDiv); + } + } else { + // Normal text + if (index % 2 == 1) { + codeDiv = document.createElement('div'); + codeDiv.className = 'survol-code-preview'; + codeDiv.appendChild(document.createTextNode(text)); + answerContainer.appendChild(codeDiv); + } + + // Code + else { + answerContainer.appendChild(document.createTextNode(this.stripHTML(text))); + } + } + }); + + commentText.appendChild(answerContainer); + } else { + commentText = document.createTextNode('Couldn\'t find an accepted answer'); + } + + const textElement = document.createElement('p'); + textElement.className = 'survol-reddit-selftext'; + textElement.appendChild(commentText); + generated.appendChild(textElement); + + postDetails.append(author, document.createElement('br'), subredditLink, scoreCommentDisplay, datePosted); + generated.append(divider, footer); + footer.append(postDetails); + + let postContainer = document.createElement('div'); + postContainer.className = 'survol-tooltiptext survol-tooltiptext-reddit-post'; + postContainer.appendChild(generated); + + if (window.lastHovered == node) { + container.appendChild(postContainer); + } + } +} \ No newline at end of file diff --git a/manifest.json b/manifest.json index 7b1d7f1..5d81ac2 100644 --- a/manifest.json +++ b/manifest.json @@ -31,7 +31,9 @@ "js/templates/reddit.js", "js/templates/twitter.js", "js/templates/youtube.js", + "js/templates/stackexchange.js", "js/templates/soundcloud.js", + "js/core.js" ], "css": [ @@ -40,7 +42,9 @@ "css/templates/reddit.css", "css/templates/twitter.css", "css/templates/youtube.css", + "css/templates/stackexchange.css", "css/templates/soundcloud.css" + ], "run_at": "document_start" }