Skip to content

Commit

Permalink
Fix class hashes (#179)
Browse files Browse the repository at this point in the history
* Start fixing class hashes

-Removes BUTTON_CLASSES, used for storing the old class names. We're trying to move away from these because they are unstable.
-Add a "kae-program-button" class to the Copy Link and vote buttons, which is currently unused.
-Get program buttons by `a[role="button"]`
-Move styles for all extension custom buttons into general.css (instead of trying to set a class that KA will match)
-Update the class used to find and place the Editor Settings Toggle button,
-Refactor the Editor Settings Toggle button so that if that class changes, stored settings will still be applied.

* Grab buttons in TS and add a class

Instead of trying to do trash CSS selectors that didn't even work, I grabbed the program buttons in TypeScript and added the ".kae-program-button" class to them. Simplifies the CSS accordingly.

* Refactor buttons

Refactor buttons.ts to export one function, which is responsible for finding the buttons wrap and calling other modifications.

* Update programdata

Tie to the user link instead of the updated class. Confirmed to work even with child account programs. Unconfirmed with anonymous programs.
Also adds the table a sibling to the program buttons and user info, instead of as a child of the updated element or one higher than whereever it was.
Fixes a bug with the toggle editor settings button where it thought it was an anchor but was previously updated to a button.

* Move flag button code into buttons.ts

Move flag button code into buttons.ts, fixing it at the same time to run off the same querySelectorPromise as the other button events.
Removes the flag file!

* Remove old button css

* Move report button from report.ts to buttons.ts

Moved it onto the same querySelectorPromise as everything else!

* Update discussion page check and program button spacing

* Fix lint errors

* Add some discussion buttons listeners

Find all discussion tab buttons (Questions, T&T, Help Requests), and add event listeners to them to trigger looking for new comments.
Find the Load More Comments button and attach a listener to that.

* Add check for replies button & refactor

-Add an event listener to the show replies button
-Move code that handles top-level comments into a specific check
-Add the number of flags to a comment even if there are no flags (makes it clear when the Exension's checking flags)
-Add a bunch of comments

* Refactor load more comments button listener

-We assume that there is at least 1 unalteredComments, because the querySelector fired, and then assume it has a 4 times removed parent (reasonable, if even if we find the wrong element, given the depth of KA's DOM tree). We then grab the last of it's children. If that's a button, we add an event listener, otherwise we exit silently.
-Increase time we search for new comments from 1sec to 2sec, I had it fail loading questions once, and I've cut down on the number times where it shouldn't find comments.

* Listener for answers

-Add an event listener for the button to show answers to questions
-Update error handling to ignore the promise rejection error and still console.error other, legitimate, errors

* Fix lint errors

* 4.6.3

-Remove debug code
-Increase comment searching timer (2s -> 4s)
-Changelog and version bumps to 4.6.3
  • Loading branch information
matthiassaihttam authored May 30, 2019
1 parent 37e5cee commit 74a6c27
Show file tree
Hide file tree
Showing 11 changed files with 305 additions and 194 deletions.
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "The Khan Academy Extension",
"short_name": "The KA Extension",
"version": "4.6.2",
"version": "4.6.3",
"description": "A Browser Extension for Khan Academy that adds more features to the site",
"minimum_chrome_version": "33.0.1750",
"icons": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ka-extension-ts",
"version": "4.6.2",
"version": "4.6.3",
"description": "A chrome extension for the Khan Academy Computer Programming section",
"private": true,
"scripts": {
Expand Down
11 changes: 11 additions & 0 deletions resources/update-log.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
[
{
"version": "4.6.3",
"new": [
"Nothing"
],
"fixes": [
"The most critical features of the extension, after a major KA source update.",
"A number of discussion features after the previous discussion update.",
"There are still known bugs. 4.6.4 is already planned."
]
},
{
"version": "4.6.2",
"new": [
Expand Down
253 changes: 164 additions & 89 deletions src/buttons.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,184 @@
import { Program } from "./types/data";
import { querySelectorPromise } from "./util/promise-util";
import { QUEUE_ROOT } from "./types/names";
import { getCSRF } from "./util/cookie-util";

const enum BUTTON_CLASSES {
default = "link_1uvuyao-o_O-computing_77ub1h",
}
import { buildQuery } from "./util/text-util";

//Replace KA's vote button with one that updates after you vote and allows undoing votes
function replaceVoteButton (program: Program): void {
querySelectorPromise(".voting-wrap .discussion-meta-controls span")
.then(wrap => {
if (!(wrap instanceof HTMLElement) || !wrap.innerText.includes("Vote")) {
console.log("Voting failed to load.", wrap, wrap.firstChild);
return;
}

const VOTE_URL = "/api/internal/discussions/voteentity";

let voted = wrap.innerText.includes("Voted Up");

const orgVotes = program.sumVotesIncremented - (voted ? 1 : 0);

const newWrap = document.createElement("span");
const voteButton = document.createElement("a");
const voteText = document.createElement("span");
voteButton.appendChild(voteText);
voteButton.classList.add(BUTTON_CLASSES.default);
newWrap.appendChild(voteButton);

function updateVoteDisplay () {
voteText.innerText = (voted ? "Voted Up!" : "Vote Up") + " • " + (orgVotes + (voted ? 1 : 0));
}
function replaceVoteButton (buttons: HTMLDivElement, program: Program): void {
const wrap = buttons.querySelector(".voting-wrap .discussion-meta-controls span");

//TODO: Handle non-English
if (!(wrap instanceof HTMLElement) || !wrap.innerText.includes("Vote")) {
console.log("Voting failed to load.", buttons, wrap, wrap && wrap.firstChild);
return;
}

const VOTE_URL = "/api/internal/discussions/voteentity";

let voted = wrap.innerText.includes("Voted Up");

const orgVotes = program.sumVotesIncremented - (voted ? 1 : 0);

const newWrap = document.createElement("span");
const voteButton = document.createElement("a");
const voteText = document.createElement("span");
voteButton.setAttribute("role", "button");
voteButton.classList.add("kae-program-button");
voteButton.appendChild(voteText);
newWrap.appendChild(voteButton);

function updateVoteDisplay () {
voteText.innerText = (voted ? "Voted Up!" : "Vote Up") + " • " + (orgVotes + (voted ? 1 : 0));
}
updateVoteDisplay();

const profileData = window.KA._userProfileData;
if (!profileData || profileData.isPhantom) {
console.log("Not logged in.", voteButton);
voteButton.setAttribute("style", "cursor: default !important");
voteButton.addEventListener("click", function () {
alert("You must be logged in in order to vote.");
});
} else {
newWrap.addEventListener("click", function () {
voted = !voted;
updateVoteDisplay();

const profileData = window.KA._userProfileData;
if (!profileData || profileData.isPhantom) {
console.log("Not logged in.", voteButton);
voteButton.setAttribute("style", "cursor: default !important");
voteButton.addEventListener("click", function () {
alert("You must be logged in in order to vote.");
});
} else {
newWrap.addEventListener("click", function () {
fetch(`${VOTE_URL}?entity_key=${program.key}&vote_type=${voted ? 1 : 0}`, {
method: "POST",
headers: { "X-KA-FKey": getCSRF() },
credentials: "same-origin"
}).then((response: Response): void => {
//If there's an error, undo the vote
if (response.status !== 204) {
response.json().then((res: { error?: string }): void => {
if (res.error) {
alert("Failed with error:\n\n" + res.error);
}
}).catch(() => {
alert(`Voting failed with status ${response.status}`);
});
voted = !voted;
updateVoteDisplay();
}
}).catch(console.error);
});
}

fetch(`${VOTE_URL}?entity_key=${program.key}&vote_type=${voted ? 1 : 0}`, {
method: "POST",
headers: { "X-KA-FKey": getCSRF() },
credentials: "same-origin"
}).then((response: Response): void => {
if (response.status === 204) { return; }
voted = !voted;
updateVoteDisplay();
}).catch(console.error);
});
}
if (wrap.parentNode) {
wrap.parentNode.insertBefore(newWrap, wrap);
wrap.parentNode.removeChild(wrap);
}
}

if (wrap.parentNode) {
wrap.parentNode.insertBefore(newWrap, wrap);
wrap.parentNode.removeChild(wrap);
//Add a "Copy Link" button
function addLinkButton (buttons: HTMLDivElement, program: Program): void {
const copyLinkButton: HTMLAnchorElement = document.createElement("a");
copyLinkButton.id = "kae-link-button";

copyLinkButton.setAttribute("role", "button");
copyLinkButton.innerHTML = "<span>Copy Link</span>";
copyLinkButton.classList.add("kae-program-button");
copyLinkButton.addEventListener("click", function () {
if (window.navigator.hasOwnProperty("clipboard")) {
/* tslint:disable:no-any */
(window.navigator as any).clipboard.writeText(`https://khanacademy.org/cs/i/${program.id}`).catch((err: Error) => {
alert("Copying failed with error:\n" + err);
});
/* tslint:enable:no-any */
} else {
try {
const textArea = document.createElement("textarea");
textArea.value = `https://khanacademy.org/cs/i/${program.id}`;
copyLinkButton.parentElement!.insertBefore(textArea, copyLinkButton);
textArea.focus();
textArea.select();

document.execCommand("copy");

copyLinkButton.parentElement!.removeChild(textArea);
} catch (err) {
alert("Copying failed with error:\n" + err);
}
}).catch(console.error);
}
});

buttons.insertBefore(copyLinkButton, buttons.children[buttons.children.length - 1]);
buttons.insertBefore(document.createTextNode(" "), copyLinkButton.nextSibling);
}

//Add a "Copy Link" button
function addLinkButton (program: Program): void {
querySelectorPromise(".buttons_vponqv")
//Add the number of flags and title text to the program flag button
function addProgramFlags (buttons: HTMLDivElement, program: Program) {
const controls = buttons.querySelector(".discussion-meta-controls");

if (!controls) {
console.log(buttons);
throw new Error("Button controls should be loaded.");
}

const programFlags: string[] = program.flags;
const flagButton: HTMLElement = <HTMLElement>controls.childNodes[2];
const reasons: string = programFlags.length > 0 ? programFlags.reduce((total, current) => total += `${current}\n`) : "No flags here!";
const profileData = window.KA._userProfileData;
//TODO: Allow viewing flags on your own program (where there's normally not a flag button)
//TODO: Bug: errors on offical programs (no flag button)
if (profileData && profileData.kaid !== program.kaid && profileData.isModerator === false) {
flagButton.textContent += ` • ${programFlags.length}`;
flagButton.title = reasons;
}
}

//Add a button to report the program
function addProgramReportButton (buttons: HTMLDivElement, program: Program, kaid: string) {
if (kaid !== program.kaid) {
const reportButton: HTMLAnchorElement = document.createElement("a");
reportButton.id = "kae-report-button";
reportButton.classList.add("kae-program-button");
reportButton.href = `${QUEUE_ROOT}submit?${buildQuery({
type: "program",
id: program.id.toString(),
callback: window.location.href
})}`;
reportButton.setAttribute("role", "button");
reportButton.innerHTML = "<span>Report</span>";
buttons.insertBefore(reportButton, buttons.children[1]);
buttons.insertBefore(document.createTextNode(" "), reportButton.nextSibling);
}
}

function findOtherButtons (buttons: HTMLDivElement): void {
/*Add the kae-program-button class to all other program buttons so we can restyle them */
Array.from(buttons.querySelectorAll("a[role=\"button\"]")).forEach(el => el.classList.add("kae-program-button"));
}

function loadButtonMods (program: Program): void {
const kaid:string = window.KA.kaid;

querySelectorPromise(".voting-wrap")
.then(votingWrap => votingWrap.parentNode)
.then(buttons => buttons as HTMLDivElement)
.then(buttons => {
console.log(buttons);
const copyLinkButton: HTMLAnchorElement = document.createElement("a");
copyLinkButton.id = "kae-link-button";

copyLinkButton.setAttribute("role", "button");
copyLinkButton.innerHTML = "<span>Copy Link</span>";
copyLinkButton.classList.add(BUTTON_CLASSES.default);

copyLinkButton.addEventListener("click", function () {
if (window.navigator.hasOwnProperty("clipboard")) {
/* tslint:disable:no-any */
(window.navigator as any).clipboard.writeText(`https://khanacademy.org/cs/i/${program.id}`).catch((err: Error) => {
alert("Copying failed with error:\n" + err);
});
/* tslint:enable:no-any */
} else {
try {
const textArea = document.createElement("textarea");
textArea.value = `https://khanacademy.org/cs/i/${program.id}`;
copyLinkButton.parentElement!.insertBefore(textArea, copyLinkButton);
textArea.focus();
textArea.select();

document.execCommand("copy");

copyLinkButton.parentElement!.removeChild(textArea);
} catch (err) {
alert("Copying failed with error:\n" + err);
}
}
});
findOtherButtons(buttons);
addLinkButton(buttons, program);
replaceVoteButton(buttons, program);
addProgramFlags(buttons, program);
addProgramReportButton(buttons, program, kaid);
});

buttons.insertBefore(copyLinkButton, buttons.children[buttons.children.length - 1]);
buttons.insertBefore(document.createTextNode(" "), copyLinkButton.nextSibling);
querySelectorPromise("#child-account-notice")
.then(childNotice => childNotice as HTMLSpanElement)
.then(childNotice => childNotice.parentNode)
.then(buttons => buttons as HTMLDivElement)
.then(buttons => {
findOtherButtons(buttons);
addLinkButton(buttons, program);
//TODO, let voting run here too
});

//TODO:
//Find the buttons wrap on offical project pages (maybe for voting or link copying. Might not be useful)
//https://www.khanacademy.org/computing/computer-programming/programming/drawing-basics/pc/challenge-waving-snowman
}

export { BUTTON_CLASSES, addLinkButton, replaceVoteButton };
export { loadButtonMods };
Loading

0 comments on commit 74a6c27

Please sign in to comment.