-
Notifications
You must be signed in to change notification settings - Fork 4
/
protoots.js
381 lines (336 loc) · 12 KB
/
protoots.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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
// @ts-check
// obligatory crime. because be gay, do crime.
// 8======D
const contributorList = [
];
import { fetchPronouns } from "../libs/fetchPronouns";
import {
accountVisibility,
conversationVisibility,
getSettings,
isLogging,
notificationVisibility,
statusVisibility,
} from "../libs/settings";
import { warn, log } from "../libs/logging";
import {
findAllDescendants,
hasClasses,
insertAfter,
waitForElement,
waitForElementRemoved,
} from "../libs/domhelpers";
import {
accountNameFromURL,
addTypeAttribute,
normaliseAccountName,
} from "../libs/protootshelpers.js";
import { debug } from "../libs/logging.js";
//before anything else, check whether we're on a Mastodon page
checkSite();
// log("hey vippy, du bist cute <3")
/**
* Checks whether site responds to Mastodon API Calls.
* If so creates an 'readystatechange' EventListener, with callback to main()
*/
async function checkSite() {
getSettings();
document.addEventListener("readystatechange", main, { once: true });
}
/**
* Evaluates the result of document.querySelector("#mastodon") and only creates a MutationObserver if the site is Mastodon.
* Warns that site is not Mastodon otherwise.
* - This prevents any additional code from being run.
*
*/
function main() {
// debug('selection for id mastodon', {'result': document.querySelector("#mastodon")})
if (!document.querySelector("#mastodon")) {
warn("Not a Mastodon instance");
return;
}
//All of this is Mastodon specific - factor out into mastodon.js?
log("Mastodon instance, activating Protoots");
//create a global tootObserver to handle all article objects
const tootObserver = new IntersectionObserver((entries) => {
onTootIntersection(entries);
});
// We are tracking navigation changes with the location and a MutationObserver on `document`,
// because the popstate event from the History API is only triggered with the back/forward buttons.
let lastUrl = location.href;
new MutationObserver((mutations) => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
}
/**
* Checks whether the given n is eligible to have a proplate added
* @param {Node} n
* @returns {Boolean}
*/
function isPronounableElement(n) {
return (
n instanceof HTMLElement &&
((n.nodeName == "ARTICLE" && n.hasAttribute("data-id")) ||
hasClasses(
n,
"detailed-status",
"status-public",
"status-unlisted",
"status-private",
"status-direct",
"conversation",
"account-authorize",
"notification",
"notification__message",
"account",
))
);
}
mutations
.flatMap((m) => Array.from(m.addedNodes).map((m) => findAllDescendants(m)))
.flat()
// .map((n) => console.log("found node: ", n));
.filter(isPronounableElement)
.forEach((a) => addtoTootObserver(a, tootObserver));
}).observe(document, { subtree: true, childList: true });
}
/**
* Callback for TootObserver
*
* Loops through all IntersectionObserver entries and checks whether each toot is on screen. If so a proplate will be added once the toot is ready.
*
* Once a toot has left the viewport its "protoots-checked" attribute will be removed.
* @param {IntersectionObserverEntry[]} observerentries
*/
function onTootIntersection(observerentries) {
for (const observation of observerentries) {
const ArticleElement = observation.target;
if (!observation.isIntersecting) {
waitForElementRemoved(ArticleElement, ".protoots-proplate", () => {
ArticleElement.removeAttribute("protoots-checked");
});
} else {
if (ArticleElement.getAttribute("protoots-type") == "conversation") {
waitForElement(ArticleElement, ".conversation__content__names", () =>
addProplate(ArticleElement),
);
} else if (ArticleElement.nodeName == "ASIDE") {
//glitch-soc notifications
waitForElement(ArticleElement, ".status__display-name", () => {
addProplate(ArticleElement);
});
} else {
waitForElement(ArticleElement, ".display-name", () => addProplate(ArticleElement));
}
}
}
}
/**
* Adds ActionElement to the tootObserver, if it has not been added before.
* @param {HTMLElement} ActionElement
* @param {IntersectionObserver} tootObserver Observer to add the element to
*/
function addtoTootObserver(ActionElement, tootObserver) {
// console.log(ActionElement);
if (ActionElement.hasAttribute("protoots-tracked")) return;
addTypeAttribute(ActionElement);
ActionElement.setAttribute("protoots-tracked", "true");
tootObserver.observe(ActionElement);
}
/**
* Adds the pro-plate to the element. The caller needs to ensure that the passed element
* is defined and that it's either a:
*
* - <article> with the "protoots-type" attribute
*
* - <div> with the "protoots-type" of either "status" or "detailed-status"
*
* Although it's possible to pass raw {@type Element}s, the method only does things on elements of type {@type HTMLElement}.
*
* @param {Node | Element | HTMLElement} element The status where the element should be added.
*/
async function addProplate(element) {
if (!(element instanceof HTMLElement)) return;
if (element.hasAttribute("protoots-checked")) return;
const type = element.getAttribute("protoots-type");
//objects that are not statuses would be added twice,
//notifications and such do not have their own data-id, just their articles
if (element.nodeName == "DIV" && !(type === "status" || type === "detailed-status")) {
element.setAttribute("protoots-checked", "true");
return;
}
if (element.querySelector(".protoots-proplate")) return;
switch (type) {
case "status":
case "detailed-status":
if (statusVisibility()) addToStatus(element);
break;
case "notification":
if (notificationVisibility()) addToNotification(element);
break;
case "account":
case "account-authorize":
if (accountVisibility()) addToAccount(element);
break;
case "conversation":
if (conversationVisibility()) addToConversation(element);
break;
}
/**
* Generates a proplate and adds it as a sibling of the given nameTagEl
* @param {string|undefined} statusId Id of the target object
* @param {string|null} accountName Name of the account the plate is for
* @param {HTMLElement|null} nametagEl Element to add the proplate next to
* @param {string} type type of the target object
* @returns
*/
async function generateProPlate(statusId, accountName, nametagEl, type) {
debug("generateProPlate called with params", { statusId, accountName, nametagEl, type });
if (!statusId) throw new Error("empty statusId passed to proplate generation, aborting.");
if (!accountName) throw new Error("empty accountName passed to proplate generation, aborting.");
if (!nametagEl) throw new Error("empty nametagEl passed to proplate generation, aborting.");
//create plate
const proplate = document.createElement("span");
const pronouns = await fetchPronouns(statusId, accountName, type);
if (pronouns == "null" && !isLogging()) {
return;
}
proplate.innerText = pronouns;
proplate.title = pronouns;
proplate.classList.add("protoots-proplate");
if (contributorList.includes(accountName)) {
//i think you can figure out what this does on your own
proplate.classList.add("proplate-pog");
}
//add plate to nametag
insertAfter(proplate, nametagEl);
}
/**
* Gets the data-id from the given element
* @param {HTMLElement} element Element with data-id attribute
* @returns {string|undefined}
*/
function getID(element) {
let id = element.dataset.id;
if (!id) {
// We don't have a status ID, pronouns might not be in cache
warn(
"The element passed to addProplate does not have a data-id attribute, searching for article.",
element,
);
//if we couldn't get an id from the div try the closest article
id = element.closest("article[data-id]")?.dataset.id;
}
if (id) id = id.replace("f-", "");
return id;
}
/**
* Basically just element.querySelector, but outputs a warning if the element isn't found
* @param {HTMLElement} element
* @param {string} accountNameClass
* @returns {HTMLElement|null}
*/
function getAccountNameEl(element, accountNameClass) {
const accountNameEl = /** @type {HTMLElement|null} */ (element.querySelector(accountNameClass));
if (!accountNameEl) {
warn(
`The element passed to addProplate does not have a ${accountNameClass}, although it should have one.`,
element,
);
}
return accountNameEl;
}
/**
* Gets the given element's textcontent or given attribute
* @param {HTMLElement|null} element Element which textcontent is the account name
* @param {string} attribute Attribute from which to pull the account name
* @returns {string|null} Normalised account name or null if it can't be found.
*/
function getAccountName(element, attribute = "textContent") {
if (!element) return null;
let accountName = element.textContent;
if (attribute != "textContent") {
accountName = element.getAttribute(attribute);
}
if (!accountName) {
warn(
`Could not extract the account name from the element, using attribute ${attribute} aborting pronoun extraction:`,
element,
);
return null;
}
accountName = normaliseAccountName(accountName);
return accountName;
}
/**
*
* @param {HTMLElement} element
* @param {string} nametagClass
* @returns {HTMLElement|null}
*/
function getNametagEl(element, nametagClass) {
const nametagEl = /** @type {HTMLElement|null} */ (element.querySelector(nametagClass));
if (!nametagEl) {
warn(
"The element passed to addProplate does not have a .display-name__html, although it should have one.",
element,
);
}
return nametagEl;
}
async function addToStatus(element) {
let statusId = getID(element);
if (!statusId) {
if (type === "detailed-status") {
//if we still don't have an ID try the domain as a last resort
warn("Attempting to retrieve id from url - this may have unforseen consequences.");
statusId = location.pathname.split("/").pop();
}
}
const accountNameEl = getAccountNameEl(element, ".display-name__account");
const accountName = getAccountName(accountNameEl);
const nametagEl = getNametagEl(element, ".display-name__html");
nametagEl.parentElement.classList.add("has-proplate");
element.setAttribute("protoots-checked", "true");
// Add the checked attribute only _after_ we've passed the basic checks.
// This allows us to pass incomplete nodes into this method, because
// we only process them after we have all required information.
generateProPlate(statusId, accountName, nametagEl, "status");
}
async function addToNotification(element) {
//debug("adding to notification");
const statusId = getID(element);
let accountNameEl = getAccountNameEl(element, ".notification__display-name");
if (!accountNameEl) accountNameEl = getAccountNameEl(element, ".status__display-name");
let accountName = getAccountName(accountNameEl, "title");
if (!accountName) {
accountName = accountNameFromURL(getAccountName(accountNameEl, "href"));
}
let nametagEl = getNametagEl(element, ".notification__display-name");
if (!nametagEl) return;
element.setAttribute("protoots-checked", "true");
generateProPlate(statusId, accountName, nametagEl, "notification");
}
async function addToAccount(element) {
//debug("adding to account");
const statusId = getID(element);
const nametagEl = element.querySelector(".display-name__html");
const accountName = getAccountName(element.querySelector(".display-name__account"));
nametagEl.parentElement.classList.add("has-proplate");
element.setAttribute("protoots-checked", "true");
generateProPlate(statusId, accountName, nametagEl, "account");
}
async function addToConversation(element) {
const nametagEls = element.querySelectorAll(".display-name__html");
for (const nametagEl of nametagEls) {
const accountName = getAccountName(nametagEl.parentElement.parentElement, "title");
generateProPlate("null", accountName, nametagEl, "conversation");
}
element.setAttribute("protoots-checked", "true");
}
}