-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.js
295 lines (257 loc) · 10.5 KB
/
content.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
/*
Better Youtube Replies:
This extension makes YouTube replies easier to follow. First it sorts the replies
based on how many likes the comment has. Then it organizes the replies into threads
that make it easy to see who is replying to who. In addition, it adds buttons into
these threads to allow users to hide and show conversations so that they can get to
the conversations that they want to read.
Defects:
1. Can not create threads properly when author changes their name or reply
gets deleted
2. Replies only load 10 at a time so it takes a while to load large reply threads
3. Many users do not use reply button so you can not tell who they are replying to
4. Buttons are updated at an interval instead of detecting DOM change
5. Page lags slightly when loading large reply threads
*/
//Store types for quick access
var buttonMap = new Map();
var commentMap = new Map();
var hiddenThreadMap = new Map();
//Detect when user opens a replies thread
var childList = {subtree: true, childList: true}
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
var observer = new MutationObserver(detectReplies);
observer.observe(document, childList);
//Buttons have to be updated when element height changes
setInterval(updateButtons, 200);
//Starts program when user has opened a replies thread and then opens
function detectReplies(mutationsList, observer){
for(var mutation of mutationsList){
if(mutation.target.id == "loaded-replies" && mutation.addedNodes.length){
observer.disconnect();
observer.takeRecords();
createThreads(mutation);
observer.observe(document, childList);
getMoreReplies(mutation.target);
}
}
}
//Logic necessary to create the threads
function createThreads(repliesMutation){
var replies = repliesMutation.target;
var storedReplies = storeReplies(repliesMutation);
var threads = Threads(storedReplies);
threads = organizeThreads(threads);
threads = getTopLevelThreads(threads);
threads = sortThreads(threads);
createButtons(threads);
replaceReplies(replies, threads);
}
//replies are stored so that the original ordering can be used for sorting when newReplies arrive
function storeReplies(repliesMutation){
var replies = repliesMutation.target;
var newReplies = Array.prototype.slice.call(repliesMutation.addedNodes);
var oldReplies = [];
if(commentMap.has(replies)) oldReplies = commentMap.get(replies);
commentMap.set(replies, oldReplies.concat(newReplies));
return commentMap.get(replies);
}
//thread objects are used to keep track of which threads are replying to which threads
function Threads(replies){
var threads = [];
for(var i=0; i<replies.length; i++){
var thread = {
node: replies[i],
numParents: 0,
children: []
}
threads.push(thread);
}
return threads;
}
//Only top level threads are necessary since other threads are nested in the structure
function getTopLevelThreads(threads){
var newThreads = [];
for(var i=0; i<threads.length; i++){
if(threads[i].numParents == 0){
newThreads.push(threads[i]);
}
}
return newThreads;
}
//for each thread finds all of its children and indents them
function organizeThreads(threads){
for(var child=0; child<threads.length; child++){
var childText = getText(threads[child].node);
for(var parent=child-1; parent>=0; parent--){
var parentAuthor = getAuthor(threads[parent].node);
if(childText.includes(parentAuthor) && threads[child].numParents == 0){
threads[child].numParents = threads[parent].numParents+1;
threads[child].node.style.marginLeft = (35 * threads[child].numParents) + "px";
threads[parent].children.push(threads[child]);
}
}
}
return threads;
}
//thread objects are nested inside each other, they must be converted into list for printing
function getPrintOrder(threads){
var printOrder = [];
for(var curr=0; curr<threads.length; curr++){
printOrder.push(threads[curr]);
var children = getPrintOrder(threads[curr].children);
printOrder = printOrder.concat(children);
}
return printOrder;
}
//in order to re-order children the must be removed and added in the correct order
function replaceReplies(replies, threads){
var printOrder = getPrintOrder(threads);
var p=0, r=0;
while(p < printOrder.length){
if(replies.children[r].className == "displayBtn"){
r++; continue;
}
if(replies.children[r] != printOrder[p].node){
replies.removeChild(printOrder[p].node);
replies.insertBefore(printOrder[p].node, replies.children[r]);
}
p++;r++;
}
}
//likes are used to sort reply threads
function getLikes(reply){
var likes = reply.querySelector("[id='vote-count-middle']").outerText.replace("K","000");
if(likes.includes(".")){
likes = likes.replace(".","");
likes = likes.replace("0","");
}
return likes;
}
//sort reply threads by amount of likes and sort all of its children
function sortThreads(threads){
threads.sort(function(a,b){return getLikes(b.node) - getLikes(a.node);});
for(var i=0; i<threads.length; i++){
threads[i].children = sortThreads(threads[i].children);
}
return threads;
}
//author is used to find who a reply is replying to
function getAuthor(reply){
return reply.querySelector("#author-text > span.style-scope").outerText;
}
//text is later searched to find who the reply is replying to
function getText(reply){
return reply.querySelector("#content-text").outerText;
}
//the more replies button needs to be repetedly clicked to get the replies 10 at a time
function getMoreReplies(replies){
var moreReplies = replies.parentElement.parentElement.querySelector("yt-next-continuation > paper-button");
if(moreReplies) moreReplies.click();
}
//when display button is clicked, the thread and all of its children have to be displayed
function displayThread(thread){
thread.node.style.display = "block";
for(var i=0; i<thread.children.length; i++){
displayThread(thread.children[i]);
}
}
//this method is triggered every time a display button is clicked
function expandThread(event){
var thread = buttonMap.get(event.target);
event.target.parentElement.removeChild(event.target);
hiddenThreadMap.delete(thread);
displayThread(thread);
}
//display button is used to reveal comments that have been hidden by the threadbtn
function newDisplayButton(thread){
console.log(thread);
var button = document.createElement("span");
var text = document.createTextNode("<" + getAuthor(thread.node) + " - " + getLikes(thread.node) + " likes>");
button.appendChild(text);
button.className = "displayBtn"
button.style.marginBottom = "8px";
button.style.color = "#606060";
button.style.marginLeft = thread.node.style.marginLeft;
button.onmouseover = function(){this.style.color = "#4e8de0";};
button.onmouseleave = function(){this.style.color = "#606060";};
button.onclick = function(event){expandThread(event)};
return button;
}
//Hides thread and all of its children
function hideThread(thread){
thread.node.style.display = "none";
for(var i=0; i<thread.children.length; i++){
if(hiddenThreadMap.has(thread.children[i])){
thread.children[i].node.parentElement.removeChild(hiddenThreadMap.get(thread.children[i]));
hiddenThreadMap.delete(thread.children[i]);
}
hideThread(thread.children[i]);
}
}
//When thread button is clicked it hides the thread
function collapseThread(clicked){
var thread = buttonMap.get(clicked);
var displayButton = newDisplayButton(thread);
observer.disconnect();
observer.takeRecords();
thread.node.parentElement.insertBefore(displayButton, thread.node);
observer.observe(document, childList);
buttonMap.set(displayButton, thread);
hiddenThreadMap.set(thread, displayButton);
hideThread(thread);
}
//height of all children necessary to create threadBtn
function getHeight(thread){
var height = thread.node.clientHeight;
if(thread.node.style.display == "none") return 18;
for(var child of thread.children){
height += getHeight(child) + 8;
}
return height;
}
//only creates one button per threadf
function newThreadButton(){
var button = document.createElement("a");
button.className = "threadBtn"
button.style.position = "absolute";
button.style.paddingLeft = "10px";
button.style.borderRight = "2px solid #e0e0e0";
button.onmouseover = function(){this.style.borderRight = "2px solid #60a2ff";};
button.onmouseleave = function(){this.style.borderRight = "2px solid #e0e0e0";};
button.onclick = function(){collapseThread(button)};
var rightInput = document.createElement("a");
button.appendChild(rightInput);
rightInput.style.position = "absolute";
rightInput.style.paddingRight = "12px";
rightInput.style.height = "100%";
rightInput.onmouseover = function(){button.onmouseover;};
rightInput.onmouseleave = function(){button.onmouseleave;};
return button;
}
//buttons necessary for hiding and displaying threads
function createButtons(threadsList){
for(var i=0; i<threadsList.length; i++){
var thread = threadsList[i];
var button = thread.node.lastChild;
if(button.className != "threadBtn"){
var button = newThreadButton();
thread.node.appendChild(button);
}
button.style.height = getHeight(thread) - 40 + "px";
button.style.marginTop = thread.node.clientHeight*-1 + 35 + "px";
buttonMap.set(button, thread);
createButtons(threadsList[i].children);
}
}
//client heights randomly change, so button heights have to be updated occasionally
function updateButtons(){
buttonMap.forEach(function(value,key,_){
var button = key;
var thread = value;
if(button.className == "threadBtn"){
button.style.height = getHeight(thread) - 40 + "px";
button.style.marginTop = thread.node.clientHeight*-1 + 35 + "px";
}
});
}