add support for more contexts

this closes #25, #26, #27, #23,
This commit is contained in:
Vipra 2023-06-29 20:29:39 +02:00
parent bcbba8e7c3
commit 8bb1709f69
3 changed files with 204 additions and 180 deletions

View file

@ -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");
}
}
}

View file

@ -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);

View file

@ -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