mirror of
https://github.com/ItsVipra/ProToots
synced 2024-11-21 19:13:03 +00:00
initial commit
This commit is contained in:
commit
1e5e338c08
16 changed files with 6396 additions and 0 deletions
152
.gitignore
vendored
Normal file
152
.gitignore
vendored
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/node
|
||||||
|
|
||||||
|
# web-ext-artifacts
|
||||||
|
web-ext-artifacts
|
||||||
|
|
||||||
|
# note files
|
||||||
|
TODO
|
||||||
|
edgecases
|
||||||
|
notes
|
1
.node-version
Normal file
1
.node-version
Normal file
|
@ -0,0 +1 @@
|
||||||
|
v18.16.0
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"printWidth": 100,
|
||||||
|
"useTabs": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["esbenp.prettier-vscode"]
|
||||||
|
}
|
23
.vscode/settings.json
vendored
Normal file
23
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[markdown]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"files.trimFinalNewlines": true,
|
||||||
|
"files.insertFinalNewline": true
|
||||||
|
}
|
20
README.md
Normal file
20
README.md
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# ProToots (v0.99)
|
||||||
|
A Firefox extension which displays an author's pronouns next to their name on Mastodon.
|
||||||
|
![A Mastodon screenshot showing off pronouns next to a person's name](documentation\firefox_ehHwJufMau.png)
|
||||||
|
|
||||||
|
## Download
|
||||||
|
uuuuhhh i dunno, i've probably sent it to you
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
Why does it need permission for all websites?
|
||||||
|
|
||||||
|
> The addon needs to determine whether or not the site you are currently browsing is a Mastodon server. For that to work, it requires access to all sites. Otherwise, each existing Mastodon server would have to be explicitly added.
|
||||||
|
|
||||||
|
|
||||||
|
## setup
|
||||||
|
- install web-ext with `npm install --global web-ext`
|
||||||
|
- optionally:
|
||||||
|
- run `web-ext run --firefox-profile='$ProfileNameOfYourChoosing' --profile-create-if-mising`
|
||||||
|
- open that profile in firefox, log into fedi
|
||||||
|
- after that when you run `web-ext run -p='$ProfileNameOfYourChoosing'` you should be logged into your fedi account
|
||||||
|
- run the extension with `web-ext run -u -u="yourinstancehere"`
|
BIN
documentation/firefox_ehHwJufMau.png
Normal file
BIN
documentation/firefox_ehHwJufMau.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
42
manifest.json
Normal file
42
manifest.json
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"manifest_version": 2,
|
||||||
|
"name": "ProToots",
|
||||||
|
"version": "0.99",
|
||||||
|
|
||||||
|
|
||||||
|
"description": "puts pronouns next to usernames on mastodon",
|
||||||
|
"homepage_url": "https://github.com/ItsVipra/ProToots",
|
||||||
|
"permissions": [
|
||||||
|
"storage"
|
||||||
|
],
|
||||||
|
|
||||||
|
"browser_action": {
|
||||||
|
"default_icon": "src/icons/beasts-32.png",
|
||||||
|
"default_title": "ProToots",
|
||||||
|
"default_popup": "src/options/options.html"
|
||||||
|
},
|
||||||
|
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
"*://*/*"
|
||||||
|
],
|
||||||
|
"js" : [
|
||||||
|
"src/content_scripts/protoots.js"
|
||||||
|
],
|
||||||
|
"css": ["src/styles/proplate.css"],
|
||||||
|
"run_at": "document_start"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"options_ui": {
|
||||||
|
"page": "src/options/options.html"
|
||||||
|
},
|
||||||
|
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "protoots@mastodon.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
5555
package-lock.json
generated
Normal file
5555
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
6
package.json
Normal file
6
package.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^2.8.8",
|
||||||
|
"web-ext": "^7.6.2"
|
||||||
|
}
|
||||||
|
}
|
403
src/content_scripts/protoots.js
Normal file
403
src/content_scripts/protoots.js
Normal file
|
@ -0,0 +1,403 @@
|
||||||
|
// @ts-check
|
||||||
|
//const proplate = document.createElement("bdi");
|
||||||
|
//proplate.textContent = "pro/nouns";
|
||||||
|
//document.body.display-name__html.append();
|
||||||
|
|
||||||
|
// const max_age = 8.64e7
|
||||||
|
const max_age = 24*60*60*1000 //time after which cached pronouns should be checked again: 24h
|
||||||
|
const host_name = location.hostname
|
||||||
|
|
||||||
|
//before anything else, check whether we're on a Mastodon page
|
||||||
|
checkSite()
|
||||||
|
let logging;
|
||||||
|
|
||||||
|
function error() {
|
||||||
|
if (logging) console.error(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function warn() {
|
||||||
|
if (logging) console.warn(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function log() {
|
||||||
|
if (logging) console.log(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function info() {
|
||||||
|
if (logging) console.info(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function debug() {
|
||||||
|
if (logging) console.debug(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
await browser.storage.sync.get("logging").then((res) => {logging = res['logging']}, () => {logging = true});
|
||||||
|
let requestDest = location.protocol + '//' + host_name + '/api/v1/instance'
|
||||||
|
let response = await fetch(requestDest)
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
// debug('checksite response got', {'response' : response.json()})
|
||||||
|
|
||||||
|
document.addEventListener('readystatechange', main)
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
warn('Not a Mastodon instance')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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")) {
|
||||||
|
log('Mastodon instance, activating Protoots')
|
||||||
|
|
||||||
|
let lastUrl = location.href
|
||||||
|
new MutationObserver((mutations) => {
|
||||||
|
const url = location.href
|
||||||
|
if (url !== lastUrl) {
|
||||||
|
lastUrl = url
|
||||||
|
onUrlChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const m of mutations) {
|
||||||
|
m.addedNodes.forEach(n => {
|
||||||
|
if (!(n instanceof HTMLElement))
|
||||||
|
return
|
||||||
|
|
||||||
|
if (n.className == "column") {
|
||||||
|
debug("found a column: ", n)
|
||||||
|
createObserver(n)
|
||||||
|
//TODO: yet another bad hack, pls fix
|
||||||
|
//TODO: doesn't work when going from detailed-status to detailed-status
|
||||||
|
document.querySelectorAll(".detailed-status").forEach(el => addProplate(el))
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}).observe(document, { subtree: true, childList: true })}
|
||||||
|
else {
|
||||||
|
warn('Not a Mastodon instance')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches pronouns associated with account name.
|
||||||
|
* If cache misses status is fetched from the instance.
|
||||||
|
*
|
||||||
|
* @param {string | undefined} statusID ID of the status being requested, in case cache misses.
|
||||||
|
* @param {string} account_name The account name, used for caching. Should have the "@" prefix.
|
||||||
|
*/
|
||||||
|
async function fetchPronouns(statusID, account_name) {
|
||||||
|
// log(`searching for ${account_name}`);
|
||||||
|
|
||||||
|
let cacheResult = await browser.storage.local.get().then(getSuccess, onError);
|
||||||
|
|
||||||
|
if (account_name[0] == '@') account_name = account_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
|
||||||
|
if (!account_name.includes("@")) {
|
||||||
|
account_name = account_name + "@" + host_name
|
||||||
|
}
|
||||||
|
|
||||||
|
// debug(cacheResult);
|
||||||
|
|
||||||
|
if (Object.keys(cacheResult).length == 0) {
|
||||||
|
let pronounsCache = {}
|
||||||
|
await browser.storage.local.set({pronounsCache}).then(setSuccess, onError);
|
||||||
|
warn('created pronounsCache in storage');
|
||||||
|
}
|
||||||
|
|
||||||
|
let cacheKeys = Object.keys(cacheResult["pronounsCache"])
|
||||||
|
|
||||||
|
if (cacheKeys.includes(account_name)) {
|
||||||
|
let entryValue = cacheResult["pronounsCache"][account_name].value
|
||||||
|
let entryTimestamp = cacheResult["pronounsCache"][account_name].timestamp
|
||||||
|
if ((Date.now() - entryTimestamp) < max_age) {
|
||||||
|
info(`${account_name} in cache:`, {'cache entry': cacheResult["pronounsCache"][account_name]} )
|
||||||
|
return entryValue
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
info(`${account_name} entry is stale, refreshing`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statusID) {
|
||||||
|
console.warn(`Could not fetch pronouns for user ${account_name}, because no status ID was passed. This is an issue we're working on.`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
info(`${account_name} not in cache, fetching status`)
|
||||||
|
let status = await fetchStatus(statusID);
|
||||||
|
|
||||||
|
|
||||||
|
let PronounField = getPronounField(status, account_name);
|
||||||
|
if (PronounField == 'null') {
|
||||||
|
//TODO: if no field check bio
|
||||||
|
info(`no pronouns found for ${account_name}, cached null`);
|
||||||
|
|
||||||
|
}
|
||||||
|
return PronounField
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches status by statusID from host_name with user's access token.
|
||||||
|
*
|
||||||
|
* @param {string} statusID ID of status being requested.
|
||||||
|
* @returns {Promise<object>} Contents of the status in json form.
|
||||||
|
*/
|
||||||
|
async function fetchStatus(statusID) {
|
||||||
|
|
||||||
|
const accessToken = await getActiveAccessToken();
|
||||||
|
//fetch status from home server with access token
|
||||||
|
const response = await fetch(`https://${host_name}/api/v1/statuses/${statusID}`, { headers: { 'Authorization': `Bearer ${accessToken}` } });
|
||||||
|
|
||||||
|
let status = await response.json();
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for fields labelled "pronouns" in the statuses' author.
|
||||||
|
* If found returns the value of said field.
|
||||||
|
*
|
||||||
|
* @param {string} status
|
||||||
|
* @param {string} account_name
|
||||||
|
* @returns {string} Author pronouns if found. Otherwise returns "null"
|
||||||
|
*/
|
||||||
|
function getPronounField(status, account_name) {
|
||||||
|
debug(status)
|
||||||
|
// get account from status and pull out fields
|
||||||
|
let account = status["account"];
|
||||||
|
let fields = account["fields"];
|
||||||
|
|
||||||
|
for (let field of fields) {
|
||||||
|
//match fields against "pronouns"
|
||||||
|
//TODO: multiple languages
|
||||||
|
if (field['name'].toLowerCase().includes("pronouns")) {
|
||||||
|
debug(`${account['acct']}: ${field['value']}`);
|
||||||
|
|
||||||
|
if (!(field['value'].includes("a href"))) { //filter links
|
||||||
|
let pronounSet = generatePronounSet(account_name, field['value'])
|
||||||
|
cachePronouns(account_name, pronounSet)
|
||||||
|
return(field['value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//if not returned by this point no field with pronouns was found
|
||||||
|
let pronounSet = generatePronounSet(account_name, 'null')
|
||||||
|
|
||||||
|
cachePronouns(account_name, pronounSet)
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates object with pronoun related data to be saved to storage.
|
||||||
|
*
|
||||||
|
* @param {string} account Full account name as generated in fetchPronouns()
|
||||||
|
* @param {string} value Contents of the found field's value
|
||||||
|
* @returns {object} Object containing account name, timestamp and pronouns
|
||||||
|
*/
|
||||||
|
function generatePronounSet(account, value) {
|
||||||
|
return { 'acct': account, 'timestamp': Date.now(), 'value': value }; //TODO: this should be just account right
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends an entry to the "pronounsCache" object in local storage.
|
||||||
|
*
|
||||||
|
* @param {string} account The account ID
|
||||||
|
* @param {{ acct: any; timestamp: number; value: any; }} set The data to cache.
|
||||||
|
*/
|
||||||
|
async function cachePronouns(account, set) {
|
||||||
|
let result = await browser.storage.local.get("pronounsCache").then(getSuccess, onError);
|
||||||
|
let pronounsCache = result['pronounsCache']
|
||||||
|
pronounsCache[account] = set
|
||||||
|
await browser.storage.local.set({pronounsCache}).then(setSuccess, onError);
|
||||||
|
debug(`caching ${account}`)
|
||||||
|
// return
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSuccess(result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSuccess() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(error) {
|
||||||
|
error('Failed save to storage!');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 "status" class or
|
||||||
|
* - <article> with the "detailed-status" class.
|
||||||
|
*
|
||||||
|
* Although it's possible to pass raw {@type Element}s, the method only does things on elements of type {@type HTMLElement}.
|
||||||
|
*
|
||||||
|
* @param {Element | HTMLElement} element The status where the element should be added.
|
||||||
|
*/
|
||||||
|
async function addProplate (element) {
|
||||||
|
if (!(element instanceof HTMLElement)) return;
|
||||||
|
|
||||||
|
//check whether element has already had a proplate added
|
||||||
|
if (element.hasAttribute("protoots-checked")) return
|
||||||
|
|
||||||
|
//if not add the attribute
|
||||||
|
element.setAttribute("protoots-checked", "true")
|
||||||
|
let 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
let 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
//get the name element and apply CSS
|
||||||
|
let 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
nametagEl.style.display = "flex";
|
||||||
|
nametagEl.style.alignItems = "baseline";
|
||||||
|
|
||||||
|
//create plate
|
||||||
|
const proplate = document.createElement("span");
|
||||||
|
let pronouns = await fetchPronouns(statusId, accountName)
|
||||||
|
if (pronouns == "null" && !logging) {return}
|
||||||
|
proplate.textContent = pronouns;
|
||||||
|
proplate.classList.add("protoots-proplate");
|
||||||
|
if ((host_name == "queer.group" && (accountName == "@vivien" || accountName == "@jasmin")) || accountName == "@jasmin@queer.group" || accountName == "@vivien@queer.group") {
|
||||||
|
//i think you can figure out what this does on your own
|
||||||
|
proplate.classList.add("pog")
|
||||||
|
}
|
||||||
|
|
||||||
|
//add plate to nametag
|
||||||
|
nametagEl.appendChild(proplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createObserver(element) {
|
||||||
|
// select column as observation target
|
||||||
|
// const targetNode = document.querySelector(".column");
|
||||||
|
const targetNode = element
|
||||||
|
|
||||||
|
// observe childlist and subtree events
|
||||||
|
// docs: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
|
||||||
|
const config = { childList: true, subtree: true, attributes: true };
|
||||||
|
|
||||||
|
// define callback inline
|
||||||
|
const callback = (/** @type {MutationRecord[]} */ mutationList, /** @type {MutationObserver} */ observer) => {
|
||||||
|
for (const mutation of mutationList) {
|
||||||
|
|
||||||
|
mutation.addedNodes.forEach((n) => {
|
||||||
|
if (!(n instanceof HTMLElement)) return;
|
||||||
|
|
||||||
|
//case for all the other normal statuses
|
||||||
|
if (containsClass(n.classList, "status") && !containsClass(n.classList, "status__prepend")) { //|| containsClass(n.classList, "detailed-status"))) {
|
||||||
|
addProplate(n);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//for nodes that have a broken classlist
|
||||||
|
let statusElement = n.querySelector(".status")
|
||||||
|
if (statusElement) {
|
||||||
|
addProplate(statusElement);
|
||||||
|
}
|
||||||
|
//potential solution for dirty hack?
|
||||||
|
// debug(".status not found looking for .detailed-status", {"element:": n})
|
||||||
|
// statusElement = n.querySelector(".detailed-status")
|
||||||
|
// if (statusElement != null) {
|
||||||
|
// addProplate(statusElement);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//TODO: bad hack, please remove
|
||||||
|
document.querySelectorAll(".status").forEach(el => addProplate(el));
|
||||||
|
document.querySelectorAll(".detailed-status").forEach(el => addProplate(el));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create an observer instance linked to the callback function
|
||||||
|
const observer = new MutationObserver(callback);
|
||||||
|
|
||||||
|
// Start observing the target node for configured mutations
|
||||||
|
observer.observe(targetNode, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by MutationObserver when the url changes.
|
||||||
|
* Creates a new MutationObserver for each column on the page.
|
||||||
|
*/
|
||||||
|
function onUrlChange() {
|
||||||
|
//select all columns for advanced interface
|
||||||
|
document.querySelectorAll(".column").forEach(el => {createObserver(el)});
|
||||||
|
// createObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DOMTokenList} classList The class list.
|
||||||
|
* @param {string} cl The class to check for.
|
||||||
|
* @returns Whether the classList contains the class.
|
||||||
|
*/
|
||||||
|
function containsClass(classList, cl) {
|
||||||
|
if (!classList|| !cl) return false;
|
||||||
|
|
||||||
|
for (const c of classList) {
|
||||||
|
if (c === cl) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the current access token for the user.
|
||||||
|
* @returns {Promise<string>} The accessToken for the current user if we are logged in.
|
||||||
|
*/
|
||||||
|
async function getActiveAccessToken() {
|
||||||
|
// Fortunately, Mastodon provides the initial state in a <script> element at the beginning of the page.
|
||||||
|
// Besides a lot of other information, it contains the access token for the current user.
|
||||||
|
const initialStateEl = document.getElementById("initial-state")
|
||||||
|
if (!initialStateEl) {
|
||||||
|
error("user not logged in yet")
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the JSON inside the script tag and extract the meta element from it.
|
||||||
|
const { meta } = JSON.parse(initialStateEl.innerText)
|
||||||
|
return meta.access_token;
|
||||||
|
}
|
BIN
src/icons/beasts-32.png
Normal file
BIN
src/icons/beasts-32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
74
src/options/options.css
Normal file
74
src/options/options.css
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
--background: hsl(249, 11%, 90%);
|
||||||
|
--hover: hsl(249, 11%, 75%);
|
||||||
|
--border: hsl(0, 0%, 50%);
|
||||||
|
--link: hsl(0, 0%, 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.protoots-settings{
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
background-color: var(--background);
|
||||||
|
margin: 0.5em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch{
|
||||||
|
display: block;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25em;
|
||||||
|
padding-left: 0.75em;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input{
|
||||||
|
margin: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
/* up right down left */
|
||||||
|
padding: 0 0 0 0.75em;
|
||||||
|
margin: 0 0 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background-color: var(--background);
|
||||||
|
padding: 1em;
|
||||||
|
border-top: 1px dashed var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.75em 1em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: #1c1b22;
|
||||||
|
color: hsl(0, 0%, 90%);
|
||||||
|
--background: hsl(249, 11%, 25%);
|
||||||
|
--hover: hsl(249, 11%, 75%);
|
||||||
|
--border: hsl(0, 0%, 90%);
|
||||||
|
--link: hsl(0, 0%, 75%);
|
||||||
|
}
|
||||||
|
}
|
25
src/options/options.html
Normal file
25
src/options/options.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="options.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form class="protoots-settings">
|
||||||
|
<div class="row">
|
||||||
|
<div class="text">
|
||||||
|
<label for="logging" class="switch">Enable logging</label>
|
||||||
|
<p id="logging-description" class="description">Log debug information to browser console</p>
|
||||||
|
</div>
|
||||||
|
<input type="checkbox" id="logging" name="logging" aria-describedby="logging-description">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
<a target="_blank" href="https://github.com/ItsVipra/ProToots">More info / help on Github</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="options.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
22
src/options/options.js
Normal file
22
src/options/options.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
function saveOptions(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
browser.storage.sync.set({
|
||||||
|
logging: document.querySelector("#logging").checked
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreOptions() {
|
||||||
|
function setCurrentChoice(result) {
|
||||||
|
document.querySelector("#logging").checked = result.logging || off;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(error) {
|
||||||
|
console.log(`Error: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let getting = browser.storage.sync.get("logging");
|
||||||
|
getting.then(setCurrentChoice, onError);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", restoreOptions);
|
||||||
|
document.querySelector("form").addEventListener("submit", saveOptions);
|
64
src/styles/proplate.css
Normal file
64
src/styles/proplate.css
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
.protoots-proplate {
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
display: inline-flex;
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* dark theme */
|
||||||
|
.theme-default .protoots-proplate {
|
||||||
|
/* header background */
|
||||||
|
background-color: #313543;
|
||||||
|
|
||||||
|
/* pulled from nowhere, basically just as low contrast as we can go while keeping AA rating */
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* dark theme on detailed status */
|
||||||
|
.theme-default .detailed-status .protoots-proplate {
|
||||||
|
/* scrollable background */
|
||||||
|
background-color: #282c37;
|
||||||
|
|
||||||
|
/* pulled from nowhere, basically just as low contrast as we can go while keeping AA rating */
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* light theme */
|
||||||
|
.theme-mastodon-light .protoots-proplate {
|
||||||
|
/* body background */
|
||||||
|
background-color: #eff3f5;
|
||||||
|
/* search bar foreground */
|
||||||
|
color: #282c37;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* high contrast */
|
||||||
|
.theme-contrast .protoots-proplate {
|
||||||
|
background-color: black;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-macaron .protoots-proplate {
|
||||||
|
background-color: #c8c4dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-fairy-floss .protoots-proplate {
|
||||||
|
background-color: #474059;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rainbow pronouns for jasmin and i cause why not */
|
||||||
|
.pog:hover {
|
||||||
|
background-image: linear-gradient(45deg,
|
||||||
|
rgba(255, 0, 0, 1) 0%,
|
||||||
|
rgba(255, 154, 0, 1) 10%,
|
||||||
|
rgba(208, 222, 33, 1) 20%,
|
||||||
|
rgba(79, 220, 74, 1) 30%,
|
||||||
|
rgba(63, 218, 216, 1) 40%,
|
||||||
|
rgba(47, 201, 226, 1) 50%,
|
||||||
|
rgba(28, 127, 238, 1) 60%,
|
||||||
|
rgba(95, 21, 242, 1) 70%,
|
||||||
|
rgba(186, 12, 248, 1) 80%,
|
||||||
|
rgba(251, 7, 217, 1) 90%,
|
||||||
|
rgba(255, 0, 0, 1) 100%);
|
||||||
|
}
|
Loading…
Reference in a new issue