mirror of
https://github.com/ItsVipra/ProToots
synced 2024-09-20 13:51:56 +00:00
parent
bcbba8e7c3
commit
8bb1709f69
3 changed files with 204 additions and 180 deletions
|
@ -67,24 +67,6 @@ function main() {
|
|||
lastUrl = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given n is an article or detailed status.
|
||||
* @param {Node} n
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function isArticleOrDetailedStatus(n) {
|
||||
return (
|
||||
n instanceof HTMLElement && (n.hasAttribute("data-id") || hasClasses(n, "detailed-status"))
|
||||
);
|
||||
}
|
||||
|
||||
function isName(n) {
|
||||
return (
|
||||
n instanceof HTMLElement &&
|
||||
(hasClasses(n, "display-name") || hasClasses(n, "notification__display-name"))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given n is eligible to have a proplate added
|
||||
* @param {Node} n
|
||||
|
@ -94,22 +76,18 @@ function main() {
|
|||
return (
|
||||
n instanceof HTMLElement &&
|
||||
((n.nodeName == "ARTICLE" && n.hasAttribute("data-id")) ||
|
||||
hasClasses(n, "detailed-status") ||
|
||||
hasClasses(n, "status") ||
|
||||
hasClasses(n, "conversation") ||
|
||||
hasClasses(n, "account-authorize") ||
|
||||
hasClasses(n, "notification"))
|
||||
hasClasses(
|
||||
n,
|
||||
"detailed-status",
|
||||
"status",
|
||||
"conversation",
|
||||
"account-authorize",
|
||||
"notification",
|
||||
"account",
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
function isProPlate(n) {
|
||||
return n.nodeName == "SPAN" && n.classList.contains("proplate");
|
||||
}
|
||||
|
||||
function isSpan(n) {
|
||||
return n.nodeName == "SPAN";
|
||||
}
|
||||
|
||||
mutations
|
||||
.flatMap((m) => Array.from(m.addedNodes).map((m) => findAllDescendants(m)))
|
||||
.flat()
|
||||
|
@ -140,7 +118,13 @@ function onTootIntersection(observerentries) {
|
|||
ArticleElement.removeAttribute("protoots-checked");
|
||||
});
|
||||
}
|
||||
waitForElement(ArticleElement, ".display-name", () => addProplate(ArticleElement));
|
||||
if (ArticleElement.getAttribute("protoots-type") == "conversation") {
|
||||
waitForElement(ArticleElement, ".conversation__content__names", () =>
|
||||
addProplate(ArticleElement),
|
||||
);
|
||||
} else {
|
||||
waitForElement(ArticleElement, ".display-name", () => addProplate(ArticleElement));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,86 +154,48 @@ function addtoTootObserver(ActionElement) {
|
|||
async function addProplate(element) {
|
||||
if (!(element instanceof HTMLElement)) return;
|
||||
|
||||
//check whether element OR article parent has already had a proplate added
|
||||
// if (hasClasses(element, "notification", "status")) {
|
||||
// let parent = element.parentElement;
|
||||
// while (parent && parent.nodeName != "ARTICLE") {
|
||||
// parent = parent.parentElement;
|
||||
// }
|
||||
// if (parent.hasAttribute("protoots-checked")) return;
|
||||
// }
|
||||
|
||||
if (element.hasAttribute("protoots-checked")) return;
|
||||
|
||||
console.log(element.querySelectorAll(".protoots-proplate"));
|
||||
const type = element.getAttribute("protoots-type");
|
||||
|
||||
if (element.querySelector(".protoots-proplate")) return; //TODO: does this work without the attribute check?
|
||||
//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") {
|
||||
element.setAttribute("protoots-checked", "true");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (element.getAttribute("protoots-type")) {
|
||||
if (element.querySelector(".protoots-proplate")) return;
|
||||
|
||||
switch (type) {
|
||||
case "status":
|
||||
addtostatus(element);
|
||||
break;
|
||||
case "detailed-status":
|
||||
addtoDetailedStatus(element);
|
||||
addtostatus(element);
|
||||
break;
|
||||
case "notification":
|
||||
addtonotification(element);
|
||||
break;
|
||||
case "account":
|
||||
case "account-authorize":
|
||||
addtoAccount(element);
|
||||
break;
|
||||
case "conversation":
|
||||
addtoConversation(element);
|
||||
break;
|
||||
}
|
||||
|
||||
async function addtostatus(element) {
|
||||
const statusId = element.dataset.id;
|
||||
if (!statusId) {
|
||||
// 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, although it should have one.",
|
||||
element,
|
||||
);
|
||||
}
|
||||
|
||||
const accountNameEl = element.querySelector(".display-name__account");
|
||||
if (!accountNameEl) {
|
||||
warn(
|
||||
"The element passed to addProplate does not have a .display-name__account, although it should have one.",
|
||||
element,
|
||||
);
|
||||
return;
|
||||
}
|
||||
let accountName = accountNameEl.textContent;
|
||||
if (!accountName) {
|
||||
warn("Could not extract the account name from the element.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (accountName[0] == "@") accountName = accountName.substring(1);
|
||||
// if the username doesn't contain an @ (i.e. the post we're looking at is from this instance)
|
||||
// append the host name to it, to avoid cache overlap between instances
|
||||
if (!accountName.includes("@")) {
|
||||
accountName = accountName + "@" + hostName;
|
||||
}
|
||||
|
||||
//get the name element and apply CSS
|
||||
const nametagEl = /** @type {HTMLElement|null} */ (element.querySelector(".display-name__html"));
|
||||
if (!nametagEl) {
|
||||
warn(
|
||||
"The element passed to addProplate does not have a .display-name__html, although it should have one.",
|
||||
element,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.
|
||||
element.setAttribute("protoots-checked", "true");
|
||||
|
||||
/**
|
||||
* Generates a proplate and adds it as a sibling of the given nameTagEl
|
||||
* @param {string} statusId Id of the target object
|
||||
* @param {string} accountName Name of the account the plate is for
|
||||
* @param {HTMLElement} nametagEl Element to add the proplate next to
|
||||
* @param {string} type type of the target object
|
||||
* @returns
|
||||
*/
|
||||
async function generateProPlate(statusId, accountName, nametagEl, type) {
|
||||
//create plate
|
||||
const proplate = document.createElement("span");
|
||||
const pronouns = await fetchPronouns(statusId, accountName, "status");
|
||||
const pronouns = await fetchPronouns(statusId, accountName, type);
|
||||
|
||||
if (pronouns == "null" && !isLogging()) {
|
||||
return;
|
||||
|
@ -260,113 +206,130 @@ async function addProplate(element) {
|
|||
//i think you can figure out what this does on your own
|
||||
proplate.classList.add("proplate-pog");
|
||||
}
|
||||
|
||||
proplate.style.fontWeight = "500";
|
||||
//add plate to nametag
|
||||
insertAfter(proplate, nametagEl);
|
||||
}
|
||||
|
||||
async function addtoDetailedStatus(element) {
|
||||
const statusId = element.dataset.id;
|
||||
if (!statusId) {
|
||||
/**
|
||||
* Gets the data-id from the given element
|
||||
* @param {HTMLElement} element Element with data-id attribute
|
||||
* @returns {string}
|
||||
*/
|
||||
function getID(element) {
|
||||
const 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, although it should have one.",
|
||||
element,
|
||||
);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
const accountNameEl = element.querySelector(".display-name__account");
|
||||
/**
|
||||
* 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 .display-name__account, although it should have one.",
|
||||
element,
|
||||
);
|
||||
return;
|
||||
}
|
||||
let accountName = accountNameEl.textContent;
|
||||
return accountNameEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the given element's textcontent or given attribute
|
||||
* @param {HTMLElement} element Element which textcontent is the account name
|
||||
* @param {string} attribute Attribute from which to pull the account name
|
||||
* @returns {string} Normalised account name
|
||||
*/
|
||||
function getAccountName(element, attribute = "textContent") {
|
||||
let accountName = element.textContent;
|
||||
if (attribute != "textContent") {
|
||||
accountName = element.getAttribute(attribute);
|
||||
}
|
||||
if (!accountName) {
|
||||
warn("Could not extract the account name from the element.");
|
||||
return;
|
||||
}
|
||||
|
||||
accountName = normaliseAccountName(accountName);
|
||||
}
|
||||
|
||||
//get the name element and apply CSS
|
||||
const nametagEl = /** @type {HTMLElement|null} */ (element.querySelector(".display-name__html"));
|
||||
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;
|
||||
}
|
||||
return nametagEl;
|
||||
}
|
||||
|
||||
async function addtostatus(element) {
|
||||
const statusId = getID(element);
|
||||
|
||||
const accountNameEl = getAccountNameEl(element, ".display-name__account");
|
||||
const accountName = getAccountName(accountNameEl);
|
||||
|
||||
const nametagEl = getNametagEl(element, ".display-name__html");
|
||||
|
||||
nametagEl.parentElement.style.display = "flex";
|
||||
|
||||
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) {
|
||||
const statusId = getID(element);
|
||||
|
||||
const accountNameEl = getAccountNameEl(element, ".notification__display-name");
|
||||
const accountName = getAccountName(accountNameEl, "title");
|
||||
|
||||
const nametagEl = getNametagEl(element, ".notification__display-name");
|
||||
|
||||
element.setAttribute("protoots-checked", "true");
|
||||
generateProPlate(statusId, accountName, nametagEl, "notification");
|
||||
}
|
||||
|
||||
async function addtoAccount(element) {
|
||||
const statusId = getID(element);
|
||||
const nametagEl = element.querySelector(".display-name__html");
|
||||
const accountName = getAccountName(element.querySelector(".display-name__account"));
|
||||
|
||||
nametagEl.parentElement.style.display = "flex";
|
||||
//create plate
|
||||
const proplate = document.createElement("span");
|
||||
const pronouns = await fetchPronouns(statusId, accountName, "status");
|
||||
|
||||
if (pronouns == "null" && !isLogging()) {
|
||||
return;
|
||||
}
|
||||
proplate.innerHTML = sanitizePronouns(pronouns);
|
||||
proplate.classList.add("protoots-proplate");
|
||||
if (accountName == "jasmin@queer.group" || accountName == "vivien@queer.group") {
|
||||
//i think you can figure out what this does on your own
|
||||
proplate.classList.add("proplate-pog");
|
||||
}
|
||||
|
||||
//add plate to nametag
|
||||
insertAfter(proplate, nametagEl);
|
||||
}
|
||||
async function addtonotification(element) {
|
||||
console.debug(element);
|
||||
|
||||
const statusId = element.dataset.id;
|
||||
const accountNameEl = element.querySelector(".notification__display-name");
|
||||
let accountName = accountNameEl.getAttribute("title");
|
||||
|
||||
if (!accountName) {
|
||||
warn("Could not extract the account name from the element.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (accountName[0] == "@") accountName = accountName.substring(1);
|
||||
// if the username doesn't contain an @ (i.e. the post we're looking at is from this instance)
|
||||
// append the host name to it, to avoid cache overlap between instances
|
||||
if (!accountName.includes("@")) {
|
||||
accountName = accountName + "@" + hostName;
|
||||
}
|
||||
|
||||
log(accountName);
|
||||
|
||||
const nametagEl = element.querySelector(".notification__display-name");
|
||||
|
||||
element.setAttribute("protoots-checked", "true");
|
||||
generateProPlate(statusId, accountName, nametagEl, "account");
|
||||
}
|
||||
|
||||
//create plate
|
||||
const proplate = document.createElement("span");
|
||||
const pronouns = await fetchPronouns(statusId, accountName, "notification");
|
||||
if (pronouns == "null" && !isLogging()) {
|
||||
return;
|
||||
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");
|
||||
}
|
||||
proplate.innerHTML = sanitizePronouns(pronouns);
|
||||
proplate.classList.add("protoots-proplate");
|
||||
if (accountName == "jasmin@queer.group" || accountName == "vivien@queer.group") {
|
||||
//i think you can figure out what this does on your own
|
||||
proplate.classList.add("proplate-pog");
|
||||
}
|
||||
|
||||
//add plate to nametag
|
||||
insertAfter(proplate, nametagEl);
|
||||
|
||||
//TODO: add to contained status
|
||||
element.setAttribute("protoots-checked", "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import { debug, error, info, log, warn } from "./logging";
|
||||
import { debug, error, info, warn } from "./logging";
|
||||
import { cachePronouns, getPronouns } from "./caching";
|
||||
import { normaliseAccountName } from "./protootshelpers";
|
||||
|
||||
const cacheMaxAge = 24 * 60 * 60 * 1000; // time after which cached pronouns should be checked again: 24h
|
||||
|
||||
/**
|
||||
* Fetches pronouns associated with account name.
|
||||
* If cache misses status is fetched from the instance.
|
||||
* If cache misses object is fetched from the instance.
|
||||
*
|
||||
* @param {string | undefined} statusID ID of the status being requested, in case cache misses.
|
||||
* @param {string | undefined} dataID ID of the object being requested, in case cache misses.
|
||||
* @param {string} accountName The account name, used for caching. Should have the "@" prefix.
|
||||
* @param {string} type Type of data-id
|
||||
*/
|
||||
export async function fetchPronouns(statusID, accountName, type) {
|
||||
export async function fetchPronouns(dataID, accountName, type) {
|
||||
// log(`searching for ${account_name}`);
|
||||
const cacheResult = await getPronouns();
|
||||
log(cacheResult);
|
||||
debug(cacheResult);
|
||||
// Extract the current cache by using object destructuring.
|
||||
if (accountName in cacheResult.pronounsCache) {
|
||||
const { value, timestamp } = cacheResult.pronounsCache[accountName];
|
||||
|
@ -29,20 +31,31 @@ export async function fetchPronouns(statusID, accountName, type) {
|
|||
|
||||
info(`${accountName} cache entry is stale, refreshing`);
|
||||
|
||||
if (!statusID) {
|
||||
warn(
|
||||
`Could not fetch pronouns for user ${accountName}, because no status ID was passed. This is an issue we're working on.`,
|
||||
);
|
||||
if (!dataID) {
|
||||
warn(`Could not fetch pronouns for user ${accountName}, because no status ID was passed.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
info(`${accountName} not in cache, fetching status`);
|
||||
// const status = await fetchStatus(statusID);
|
||||
|
||||
let status;
|
||||
if (type === "notification") {
|
||||
status = await fetchNotification(statusID);
|
||||
status = await fetchNotification(dataID);
|
||||
} else if (type === "account") {
|
||||
status = await fetchAccount(dataID);
|
||||
} else if (type === "conversation") {
|
||||
const conversations = await fetchConversations();
|
||||
for (const conversation of conversations) {
|
||||
for (const account of conversation.accounts) {
|
||||
//conversations can have multiple participants, check that we're passing along the right account
|
||||
if (normaliseAccountName(account.acct) == accountName) {
|
||||
//package the account object in an empty object for compatibility with getPronounField()
|
||||
status = { account: account };
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
status = await fetchStatus(statusID);
|
||||
status = await fetchStatus(dataID);
|
||||
}
|
||||
|
||||
const PronounField = getPronounField(status, accountName);
|
||||
|
@ -76,6 +89,12 @@ async function fetchStatus(statusID) {
|
|||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches notification by notificationID from host_name with user's access token.
|
||||
*
|
||||
* @param {string} notificationID ID of notification being requested.
|
||||
* @returns {Promise<object>} Contents of notification in json form.
|
||||
*/
|
||||
async function fetchNotification(notificationID) {
|
||||
const accessToken = await getActiveAccessToken();
|
||||
|
||||
|
@ -88,13 +107,54 @@ async function fetchNotification(notificationID) {
|
|||
|
||||
const notification = await response.json();
|
||||
|
||||
const actioner = notification.account; //person who performed notification action
|
||||
|
||||
debug(notification);
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches account by accountID from host_name with user's access token.
|
||||
*
|
||||
* @param {string} accountID ID of account being requested.
|
||||
* @returns {Promise<object>} Contents of account in json form.
|
||||
*/
|
||||
async function fetchAccount(accountID) {
|
||||
const accessToken = await getActiveAccessToken();
|
||||
|
||||
const response = await fetch(
|
||||
`${location.protocol}//${location.host}/api/v1/accounts/${accountID}`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
},
|
||||
);
|
||||
|
||||
const account = await response.json();
|
||||
|
||||
return { account: account };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the user's last <=40 direct message threads from host_name with user's access token.
|
||||
* @returns {Promise<Array>} Array containing direct message thread objects in json from.
|
||||
*
|
||||
* DOCS: https://docs.joinmastodon.org/methods/conversations/#response
|
||||
*/
|
||||
async function fetchConversations() {
|
||||
//the api wants status IDs, not conversation IDs
|
||||
//as a result we can only get pronouns for the first 40 conversations max
|
||||
//most of these should be in cache anyways
|
||||
const accessToken = await getActiveAccessToken();
|
||||
|
||||
const response = await fetch(
|
||||
`${location.protocol}//${location.host}/api/v1/conversations?limit=40`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
},
|
||||
);
|
||||
|
||||
const conversations = await response.json();
|
||||
|
||||
return conversations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for fields labelled "pronouns" in the statuses' author.
|
||||
* If found returns the value of said field.
|
||||
|
@ -104,15 +164,15 @@ async function fetchNotification(notificationID) {
|
|||
* @returns {string} Author pronouns if found. Otherwise returns "null"
|
||||
*/
|
||||
function getPronounField(status, accountName) {
|
||||
debug(status);
|
||||
// get account from status and pull out fields
|
||||
const account = status.account;
|
||||
const fields = account.fields;
|
||||
|
||||
for (const field of fields) {
|
||||
//match fields against "pronouns"
|
||||
//TODO: multiple languages
|
||||
//TODO: multiple languages -> match against list
|
||||
if (field.name.toLowerCase().includes("pronouns")) {
|
||||
//TODO: see https://github.com/ItsVipra/ProToots/issues/28
|
||||
debug(`${account.acct}: ${field.value}`);
|
||||
|
||||
cachePronouns(accountName, field.value);
|
||||
|
|
|
@ -6,6 +6,7 @@ import { hasClasses } from "./domhelpers";
|
|||
* @returns {string}
|
||||
*/
|
||||
export function normaliseAccountName(name) {
|
||||
if (!name) return "null";
|
||||
if (name[0] == "@") name = name.substring(1);
|
||||
// if the username doesn't contain an @ (i.e. the post we're looking at is from this instance)
|
||||
// append the host name to it, to avoid cache overlap between instances
|
||||
|
|
Loading…
Reference in a new issue