// ==UserScript==
// @name Lemmy Universal Link Switcher
// @namespace http://azzurite.tv/
// @license GPLv3
// @version 1.3.4
// @description Ensures that all URLs to Lemmy instances always point to your main/home instance.
// @homepageURL https://gitlab.com/azzurite/lemmy-universal-link-switcher
// @supportURL https://gitlab.com/azzurite/lemmy-universal-link-switcher/-/issues
// @author Azzurite
// @match *://*/*
// @icon https://gitlab.com/azzurite/lemmy-universal-link-switcher/-/raw/main/resources/favicon.png?inline=true
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.xmlHttpRequest
// @grant GM.registerMenuCommand
// @connect *
// @require https://unpkg.com/@popperjs/core@2
// @require https://unpkg.com/tippy.js@6
// ==/UserScript==
(() => {
// src/debug.js
var DEBUG = false;
function debug() {
if (DEBUG)
console.debug(`Rewriter | `, ...arguments);
}
function trace() {
if (DEBUG === `trace`)
console.debug(`Rewriter Trace | `, ...arguments);
}
// src/instances.js
function isLemmyInstance(url) {
return isInstance(INSTANCES_LEMMY, url);
}
function isKbinInstance(url) {
return isInstance(INSTANCES_KBIN, url);
}
function isInstance(instances, url) {
if (url.origin) {
return instances.has(url.origin);
} else {
return false;
}
}
trace(`Define instances sets start`);
var INSTANCES_LEMMY = /* @__PURE__ */ new Set([
"http://lemmy.brdsnest.net",
"https://0d.gs",
"https://0xdd.org.ru",
"https://1337lemmy.com",
"https://acqrs.co.uk",
"https://adultswim.fan",
"https://aggregatet.org",
"https://agoraverse.sh",
"https://alien.top",
"https://ani.social",
"https://ascy.mooo.com",
"https://aussie.zone",
"https://awful.systems",
"https://badatbeing.social",
"https://baraza.africa",
"https://bbs.9tail.net",
"https://bdl.owu.one",
"https://beehaw.org",
"https://belfry.rip",
"https://biglemmowski.win",
"https://bin.pztrn.name",
"https://bitforged.space",
"https://blendit.bsd.cafe",
"https://board.minimally.online",
"https://bolha.forum",
"https://bookwormstory.social",
"https://borg.chat",
"https://buddyverse.one",
"https://campfyre.nickwebster.dev",
"https://casavaga.com",
"https://catata.fish",
"https://champserver.net",
"https://chinese.lol",
"https://civilloquy.com",
"https://communick.news",
"https://corndog.social",
"https://corrigan.space",
"https://crazypeople.online",
"https://dendarii.alaeron.com",
"https://dev.automationwise.com",
"https://diagonlemmy.social",
"https://digipres.cafe",
"https://discover.deltanauten.de",
"https://discuss.icewind.me",
"https://discuss.jacen.moe",
"https://discuss.online",
"https://discuss.tchncs.de",
"https://distress.digital",
"https://dit.reformed.social",
"https://doomscroll.n8e.dev",
"https://dormi.zone",
"https://downonthestreet.eu",
"https://drlemmy.net",
"https://ds9.lemmy.ml",
"https://dubvee.org",
"https://dumbdomain.xyz",
"https://endlesstalk.org",
"https://enterprise.lemmy.ml",
"https://eventfrontier.com",
"https://eviltoast.org",
"https://expats.zone",
"https://falconry.party",
"https://fanaticus.social",
"https://fasheng.ing",
"https://fed.dyne.org",
"https://feddit.cl",
"https://feddit.dk",
"https://feddit.eu",
"https://feddit.it",
"https://feddit.nl",
"https://feddit.nu",
"https://feddit.org",
"https://feddit.rocks",
"https://feddit.site",
"https://feddit.uk",
"https://federation.red",
"https://fedii.me",
"https://fedit.pl",
"https://feditown.com",
"https://feed.newt.wtf",
"https://fenmou.cyou",
"https://fjdk.uk",
"https://flamewar.social",
"https://foros.fediverso.gal",
"https://forum.ayom.media",
"https://forum.uncomfortable.business",
"https://futurology.today",
"https://g00n.cloud",
"https://gearhead.town",
"https://gioia.news",
"https://gregtech.eu",
"https://group.lt",
"https://h4x0r.host",
"https://hackertalks.com",
"https://halubilo.social",
"https://happysl.app",
"https://hardware.watch",
"https://hexbear.net",
"https://hilariouschaos.com",
"https://hobbit.world",
"https://hoihoi.superboi.eu.org",
"https://info.prou.be",
"https://infosec.pub",
"https://jlai.lu",
"https://krabb.org",
"https://kutsuya.dev",
"https://kuu.kohana.fi",
"https://kyu.de",
"https://l.7rg1nt.moe",
"https://l.dongxi.ca",
"https://l.henlo.fi",
"https://l.hostux.net",
"https://l.mathers.fr",
"https://l.mchome.net",
"https://l.roofo.cc",
"https://l.shoddy.site",
"https://l.sw0.com",
"https://l3mmy.com",
"https://lazysoci.al",
"https://le.meto.lol",
"https://leaf.dance",
"https://lebowski.social",
"https://lef.li",
"https://leftopia.org",
"https://lem.a3a2.uk",
"https://lem.afiz.org",
"https://lem.cochrun.xyz",
"https://lem.free.as",
"https://lem.monster",
"https://lem.ph3j.com",
"https://lem.serkozh.me",
"https://lem.trashbrain.org",
"https://lemdro.id",
"https://leminal.space",
"https://lemm.ee",
"https://lemmings.sopelj.ca",
"https://lemmings.world",
"https://lemmit.online",
"https://lemmus.org",
"https://lemmy-api.ten4ward.social",
"https://lemmy-mormonsatan-u23030.vm.elestio.app",
"https://lemmy.0upti.me",
"https://lemmy.100010101.xyz",
"https://lemmy.4d2.org",
"https://lemmy.86thumbs.net",
"https://lemmy.8bitar.io",
"https://lemmy.8th.world",
"https://lemmy.absolutesix.com",
"https://lemmy.activitypub.academy",
"https://lemmy.ahall.se",
"https://lemmy.aicampground.com",
"https://lemmy.amethyst.name",
"https://lemmy.amxl.com",
"https://lemmy.ananace.dev",
"https://lemmy.anonion.social",
"https://lemmy.anymore.nl",
"https://lemmy.asc6.org",
"https://lemmy.asudox.dev",
"https://lemmy.azamserver.com",
"https://lemmy.baie.me",
"https://lemmy.balamb.fr",
"https://lemmy.beagle.quest",
"https://lemmy.belegost.net",
"https://lemmy.beru.co",
"https://lemmy.best",
"https://lemmy.bestiver.se",
"https://lemmy.billiam.net",
"https://lemmy.bismith.net",
"https://lemmy.bit-refined.eu",
"https://lemmy.blackeco.com",
"https://lemmy.blahaj.zone",
"https://lemmy.blugatch.tube",
"https://lemmy.bmck.au",
"https://lemmy.bosio.info",
"https://lemmy.brad.ee",
"https://lemmy.brandyapple.com",
"https://lemmy.brief.guru",
"https://lemmy.browntown.dev",
"https://lemmy.byrdcrouse.com",
"https://lemmy.byteunion.com",
"https://lemmy.ca",
"https://lemmy.cafe",
"https://lemmy.caliban.io",
"https://lemmy.cat",
"https://lemmy.catgirl.biz",
"https://lemmy.ch3n2k.com",
"https://lemmy.chiisana.net",
"https://lemmy.ciechom.eu",
"https://lemmy.cixoelectronic.pl",
"https://lemmy.cloudhub.social",
"https://lemmy.co.nz",
"https://lemmy.cogindo.net",
"https://lemmy.com.tr",
"https://lemmy.conorab.com",
"https://lemmy.coupou.fr",
"https://lemmy.crimedad.work",
"https://lemmy.cringecollective.io",
"https://lemmy.criticalbasics.xyz",
"https://lemmy.croc.pw",
"https://lemmy.cronyakatsuki.xyz",
"https://lemmy.cryonex.net",
"https://lemmy.csupes.page",
"https://lemmy.cultimean.group",
"https://lemmy.darvit.nl",
"https://lemmy.dbzer0.com",
"https://lemmy.decronym.xyz",
"https://lemmy.deedium.nl",
"https://lemmy.deepspace.gay",
"https://lemmy.dexlit.xyz",
"https://lemmy.digitalcharon.in",
"https://lemmy.digitalfall.net",
"https://lemmy.doesnotexist.club",
"https://lemmy.dogboy.xyz",
"https://lemmy.dormedas.com",
"https://lemmy.dropdoos.nl",
"https://lemmy.duck.cafe",
"https://lemmy.dudeami.win",
"https://lemmy.eco.br",
"https://lemmy.emerald.show",
"https://lemmy.enchanted.social",
"https://lemmy.esquiretheduke.nohost.me",
"https://lemmy.eus",
"https://lemmy.evangineer.net",
"https://lemmy.fait.ch",
"https://lemmy.federate.cc",
"https://lemmy.federate.lol",
"https://lemmy.fedi.zutto.fi",
"https://lemmy.fedifriends.social",
"https://lemmy.fediverse.jp",
"https://lemmy.fish",
"https://lemmy.fosshost.com",
"https://lemmy.foxden.party",
"https://lemmy.freewilltiger.page",
"https://lemmy.fromshado.ws",
"https://lemmy.frozeninferno.xyz",
"https://lemmy.funami.tech",
"https://lemmy.fwgx.uk",
"https://lemmy.gf4.pw",
"https://lemmy.giftedmc.com",
"https://lemmy.glasgow.social",
"https://lemmy.graphics",
"https://lemmy.greatpyramid.social",
"https://lemmy.grey.fail",
"https://lemmy.grys.it",
"https://lemmy.hacktheplanet.be",
"https://lemmy.haley.io",
"https://lemmy.halfbro.xyz",
"https://lemmy.helios42.de",
"https://lemmy.helvetet.eu",
"https://lemmy.hogru.ch",
"https://lemmy.horwood.cloud",
"https://lemmy.hosted.frl",
"https://lemmy.hybridsarcasm.xyz",
"https://lemmy.imagisphe.re",
"https://lemmy.imontheweb.net",
"https://lemmy.inbutts.lol",
"https://lemmy.installation00.org",
"https://lemmy.itsallbadsyntax.com",
"https://lemmy.iys.io",
"https://lemmy.jacaranda.club",
"https://lemmy.jamesj999.co.uk",
"https://lemmy.janiak.cc",
"https://lemmy.javant.xyz",
"https://lemmy.jaypg.pw",
"https://lemmy.jelliefrontier.net",
"https://lemmy.jhjacobs.nl",
"https://lemmy.jlh.name",
"https://lemmy.johnnei.org",
"https://lemmy.jrvs.cc",
"https://lemmy.kaytse.fun",
"https://lemmy.kde.social",
"https://lemmy.kfed.org",
"https://lemmy.killtime.online",
"https://lemmy.klein.ruhr",
"https://lemmy.kmoneyserver.com",
"https://lemmy.kokomo.cloud",
"https://lemmy.kopieczek.com",
"https://lemmy.kya.moe",
"https://lemmy.laitinlok.com",
"https://lemmy.lantian.pub",
"https://lemmy.libertarianfellowship.org",
"https://lemmy.librebun.com",
"https://lemmy.linden.social",
"https://lemmy.linuxuserspace.show",
"https://lemmy.lqx.net",
"https://lemmy.lukeog.com",
"https://lemmy.lundgrensjostrom.com",
"https://lemmy.magnor.ovh",
"https://lemmy.mariusdavid.fr",
"https://lemmy.marud.fr",
"https://lemmy.masto.community",
"https://lemmy.mats.ooo",
"https://lemmy.max-p.me",
"https://lemmy.mbl.social",
"https://lemmy.mebitek.com",
"https://lemmy.meissners.me",
"https://lemmy.menf.in",
"https://lemmy.mengsk.org",
"https://lemmy.menos.gotdns.org",
"https://lemmy.michaelsasser.org",
"https://lemmy.mindoki.com",
"https://lemmy.minecloud.ro",
"https://lemmy.minie4.de",
"https://lemmy.mkwarman.com",
"https://lemmy.ml",
"https://lemmy.mlaga97.space",
"https://lemmy.mods4ever.com",
"https://lemmy.monster",
"https://lemmy.moocloud.party",
"https://lemmy.moonling.nl",
"https://lemmy.mrm.one",
"https://lemmy.muffalings.com",
"https://lemmy.multivers.cc",
"https://lemmy.my-box.dev",
"https://lemmy.myserv.one",
"https://lemmy.nannoda.com",
"https://lemmy.nauk.io",
"https://lemmy.ndlug.org",
"https://lemmy.nekusoul.de",
"https://lemmy.nerdcore.social",
"https://lemmy.nexus",
"https://lemmy.nicknakin.com",
"https://lemmy.nikore.net",
"https://lemmy.nine-hells.net",
"https://lemmy.noellesporn.de",
"https://lemmy.nope.ly",
"https://lemmy.norbipeti.eu",
"https://lemmy.notmy.cloud",
"https://lemmy.nowsci.com",
"https://lemmy.nz",
"https://lemmy.obrell.se",
"https://lemmy.ohaa.xyz",
"https://lemmy.okr765.com",
"https://lemmy.oldtr.uk",
"https://lemmy.one",
"https://lemmy.onlylans.io",
"https://lemmy.org",
"https://lemmy.packitsolutions.net",
"https://lemmy.parastor.net",
"https://lemmy.paulstevens.org",
"https://lemmy.pe1uca.dev",
"https://lemmy.peoplever.se",
"https://lemmy.petecca.com",
"https://lemmy.physfluids.fr",
"https://lemmy.pierre-couy.fr",
"https://lemmy.pit.ninja",
"https://lemmy.pixelcollider.net",
"https://lemmy.plaureano.nohost.me",
"https://lemmy.prograhamming.com",
"https://lemmy.pt",
"https://lemmy.pussthecat.org",
"https://lemmy.radio",
"https://lemmy.razbot.xyz",
"https://lemmy.remorse.us",
"https://lemmy.reysic.com",
"https://lemmy.rhymelikedi.me",
"https://lemmy.rimkus.it",
"https://lemmy.rochegmr.com",
"https://lemmy.runesmite.com",
"https://lemmy.saik0.com",
"https://lemmy.scam-mail.me",
"https://lemmy.schlunker.com",
"https://lemmy.schoenwolf-schroeder.com",
"https://lemmy.sdf.org",
"https://lemmy.sebbem.se",
"https://lemmy.secnd.me",
"https://lemmy.self-hosted.site",
"https://lemmy.selfhostcat.com",
"https://lemmy.services.coupou.fr",
"https://lemmy.setzman.synology.me",
"https://lemmy.shiny-task.com",
"https://lemmy.shtuf.eu",
"https://lemmy.sieprawski.pl",
"https://lemmy.sietch.online",
"https://lemmy.simpl.website",
"https://lemmy.skoops.social",
"https://lemmy.skyjake.fi",
"https://lemmy.smgames.club",
"https://lemmy.snoot.tube",
"https://lemmy.socdojo.com",
"https://lemmy.sotu.casa",
"https://lemmy.spacestation14.com",
"https://lemmy.spronkus.xyz",
"https://lemmy.ssba.com",
"https://lemmy.stad.social",
"https://lemmy.staphup.nl",
"https://lemmy.starlightkel.xyz",
"https://lemmy.stefanoprenna.com",
"https://lemmy.steken.xyz",
"https://lemmy.stonansh.org",
"https://lemmy.stuart.fun",
"https://lemmy.studio",
"https://lemmy.sudovanilla.org",
"https://lemmy.sumuun.net",
"https://lemmy.svlachos.duckdns.org",
"https://lemmy.syrasu.com",
"https://lemmy.t-rg.ws",
"https://lemmy.team",
"https://lemmy.technosorcery.net",
"https://lemmy.technowizardry.net",
"https://lemmy.telaax.com",
"https://lemmy.tespia.org",
"https://lemmy.teuto.icu",
"https://lemmy.tf",
"https://lemmy.tgxn.net",
"https://lemmy.thebias.nl",
"https://lemmy.thefloatinglab.world",
"https://lemmy.thewooskeys.com",
"https://lemmy.tobyvin.dev",
"https://lemmy.today",
"https://lemmy.toldi.eu",
"https://lemmy.tomkoreny.com",
"https://lemmy.toot.pt",
"https://lemmy.trevor.coffee",
"https://lemmy.trippy.pizza",
"https://lemmy.ubergeek77.chat",
"https://lemmy.uhhoh.com",
"https://lemmy.umucat.day",
"https://lemmy.unboiled.info",
"https://lemmy.unfiltered.social",
"https://lemmy.uninsane.org",
"https://lemmy.unryzer.eu",
"https://lemmy.urbanhost.top",
"https://lemmy.vg",
"https://lemmy.vyizis.tech",
"https://lemmy.w9r.de",
"https://lemmy.wentam.net",
"https://lemmy.whynotdrs.org",
"https://lemmy.world",
"https://lemmy.wtf",
"https://lemmy.xeviousx.eu",
"https://lemmy.xoynq.com",
"https://lemmy.yachts",
"https://lemmy.z0r.co",
"https://lemmy.zhukov.al",
"https://lemmy.zimage.com",
"https://lemmy.zip",
"https://lemmy.zwanenburg.info",
"https://lemmygrad.ml",
"https://lemmyis.fun",
"https://lemmyland.com",
"https://lemmynsfw.com",
"https://lemmyusa.com",
"https://lemux.minnix.dev",
"https://lemy.leuker.me",
"https://lemy.lol",
"https://lemy.nl",
"https://level-up.zone",
"https://libretechni.ca",
"https://linkage.ds8.zone",
"https://links.gayfr.online",
"https://links.rocks",
"https://linux.community",
"https://linz.city",
"https://literature.cafe",
"https://lm.boing.icu",
"https://lm.inu.is",
"https://lm.korako.me",
"https://lm.madiator.cloud",
"https://lm.paradisus.day",
"https://lm.sethp.cc",
"https://lm.williampuckering.com",
"https://lmy.sagf.io",
"https://lonestarlemmy.mooo.com",
"https://lsmu.schmurian.xyz",
"https://lu.skbo.net",
"https://lululemmy.com",
"https://lx.pontual.social",
"https://mander.xyz",
"https://meinreddit.com",
"https://metapowers.org",
"https://midwest.social",
"https://mimiclem.me",
"https://mlem.eldritch.gift",
"https://monero.town",
"https://monyet.cc",
"https://moose.best",
"https://moto.teamswollen.org",
"https://mouse.chitanda.moe",
"https://mtgzone.com",
"https://mujico.org",
"https://news.idlestate.org",
"https://no.lastname.nz",
"https://nodesphere.site",
"https://notdigg.com",
"https://nsfwaiclub.com",
"https://odin.lanofthedead.xyz",
"https://orbi.camp",
"https://orbiting.observer",
"https://orcas.enjoying.yachts",
"https://overctrl.dbzer0.com",
"https://parenti.sh",
"https://pawb.social",
"https://ponder.cat",
"https://popplesburger.hilciferous.nl",
"https://poptalk.scrubbles.tech",
"https://preserve.games",
"https://pricefield.org",
"https://programming.dev",
"https://proit.org",
"https://providence.root.sx",
"https://quokk.au",
"https://r-sauna.fi",
"https://r.nf",
"https://radiation.party",
"https://rblind.com",
"https://real.lemmy.fan",
"https://realbitcoin.cash",
"https://reddeet.com",
"https://reddthat.com",
"https://redlemmy.com",
"https://rekabu.ru",
"https://rentadrunk.org",
"https://retarded.dev",
"https://retrolemmy.com",
"https://roanoke.social",
"https://rollenspiel.forum",
"https://rqd2.net",
"https://sammich.es",
"https://scribe.disroot.org",
"https://selfhosted.forum",
"https://sh.itjust.works",
"https://sha1.nl",
"https://showeq.com",
"https://slangenettet.pyjam.as",
"https://slrpnk.net",
"https://soc.ebmn.io",
"https://soccer.forum",
"https://social.belowland.com",
"https://social.ggbox.fr",
"https://social.jears.at",
"https://social.jrruethe.info",
"https://social.nerdhouse.io",
"https://social.p80.se",
"https://social.packetloss.gg",
"https://social.pwned.page",
"https://social.rocketsfall.net",
"https://social.sour.is",
"https://social2.williamyam.com",
"https://sopuli.xyz",
"https://spgrn.com",
"https://stammtisch.hallertau.social",
"https://startrek.website",
"https://sub.wetshaving.social",
"https://supernova.place",
"https://suppo.fi",
"https://swg-empire.de",
"https://switter.su",
"https://szmer.info",
"https://t.bobamilktea.xyz",
"https://tacobu.de",
"https://thelemmy.club",
"https://timesink.p3nguin.org",
"https://tkohhh.social",
"https://toast.ooo",
"https://treadst.one",
"https://ttrpg.network",
"https://tucson.social",
"https://ukfli.uk",
"https://unreachable.cloud",
"https://upvote.au",
"https://usenet.lol",
"https://va11halla.bar",
"https://vegantheoryclub.org",
"https://vger.social",
"https://voyager.lemmy.ml",
"https://walledgarden.xyz",
"https://welppp.com",
"https://whemic.xyz",
"https://wired.bluemarch.art",
"https://x69.org",
"https://xn--mh-fkaaaaaa.schuetze.link",
"https://yall.theatl.social",
"https://yiffit.net",
"https://ythreektech.com",
"https://zerobytes.monster"
]);
var INSTANCES_KBIN = /* @__PURE__ */ new Set([
"https://artemis.camp",
"https://atakbin.spacehost.dev",
"https://bin.pol.social",
"https://community.yshi.org",
"https://dgngrnder.com",
"https://feddit.online",
"https://fedinews.net",
"https://fusionpatrol.social",
"https://hotaudiofiction.social",
"https://jlailu.social",
"https://k.fe.derate.me",
"https://karab.in",
"https://kayb.ee",
"https://kbin-u3.vm.elestio.app",
"https://kbin.bitgoblin.tech",
"https://kbin.cafe",
"https://kbin.chat",
"https://kbin.ectolab.net",
"https://kbin.fedi.cr",
"https://kbin.life",
"https://kbin.nz",
"https://kbin.pieho.me",
"https://kbin.pithyphrase.net",
"https://kbin.projectsegfau.lt",
"https://kbin.social",
"https://kbin.tenkuu.social",
"https://kbin.thicknahalf.com",
"https://kbin.wangwood.house",
"https://kglitch.social",
"https://kopnij.in",
"https://kx.pontual.social",
"https://link.fossdle.org",
"https://longley.ws",
"https://nadajnik.org",
"https://polesie.pol.social",
"https://rainy.place",
"https://social.tath.link",
"https://supermeter.social",
"https://teacup.social",
"https://wiku.hu"
]);
trace(`Define instances sets end`);
// src/our-changes.js
var OUR_CHANGES = { addedNodes: {} };
function getAddedNodesSelectors() {
return Object.values(OUR_CHANGES.addedNodes);
}
function registerAddedNode(id, selector) {
OUR_CHANGES.addedNodes[id] = selector;
}
// src/constants.js
var constants_default = {
ICON_CLASS: withNS(`icon`),
ICON_LOADING_CLASS: withNS(`loading`),
ICON_STYLES_ID: withNS(`icon-styles`),
ICON_LINK_CLASS: withNS(`icon-link`),
ICON_LINK_SYMBOL_ID: withNS(`icon-link-symbol`),
ICON_SPINNER_CLASS: withNS(`icon-spinner`),
ICON_SPINNER_SYMBOL_ID: withNS(`icon-spinner-symbol`),
ORIGINAL_LINK_CLASS: withNS(`original-link`),
SHOW_AT_HOME_BUTTON_CLASS: withNS(`show-at-home`),
ICON_SVG_TEMPLATE_ID: withNS(`icon-template`),
AUTH_WRONG: `AUTH_WRONG`,
AUTH_MISSING: `AUTH_MISSING`,
REWRITE_STATUS: withNSCamelCase(`localUrlStatus`),
REWRITE_STATUS_PENDING: `pending`,
REWRITE_STATUS_SUCCESS: `success`,
REWRITE_STATUS_ERROR: `error`,
REWRITE_STATUS_UNRESOLVED: `unresolved`,
SETUP_AUTH_MESSAGE: `Lemmy Universal Link Switcher: Visit your home instance once to set up post/comment rewriting`,
SETTINGS_BUTTON_ID: withNS(`open-settings-button`),
SETTINGS_MENU_ID: withNS(`settings`),
SETTINGS_STYLES_ID: withNS(`settings-styles`)
};
function withNS(identifier) {
return `lemmy-rewrite-urls-` + identifier;
}
function withNSCamelCase(identifier) {
return `lemmyRewriteUrls` + identifier.charAt(0).toUpperCase() + identifier.slice(1);
}
// src/rewriting/helpers.js
function isHashLink(link) {
return link.hash && link.origin + link.pathname + link.search === location.origin + location.pathname + location.search;
}
function isSamePage(url1, url2) {
return url1.host === url2.host && url1.pathname === url2.pathname;
}
function isV17() {
return window.isoData?.site_res?.version.startsWith(`0.17`);
}
var stopEventHandler = (event) => {
event.preventDefault();
event.stopPropagation();
};
// src/gm.js
async function setValue(key, value) {
trace(`GM.setValue key ${key}, value ${value}`);
await GM.setValue(key, value);
}
async function getValue(key) {
return await GM.getValue(key);
}
function parseResponse(response) {
try {
return JSON.parse(response.response);
} catch (e) {
debug(`Error parsing response JSON`, e);
return response.response;
}
}
function logRequest(response, data) {
trace(
`FinalUrl`,
response.finalUrl,
`status`,
response.status,
`text`,
response.statusText,
`response`,
data || response.response
);
trace(`responseHeaders`, response.responseHeaders);
}
function performXmlHttpRequest(url, doneCallback) {
GM.xmlHttpRequest({
url,
onloadend: (response) => {
const data = parseResponse(response);
logRequest(response, data);
doneCallback(response, data);
}
});
}
function registerMenuCommand(name, onClick) {
GM.registerMenuCommand(name, onClick);
}
// src/rewriting/auth.js
var AUTH;
function getAuthFromCookie() {
return document.cookie.split("; ").find((row) => row.startsWith("jwt="))?.split("=")[1];
}
async function setAuth(auth) {
AUTH = auth;
await setValue(`auth`, auth);
}
async function initAuth() {
const curAuth = await getAuth();
if (curAuth) {
AUTH = curAuth;
return;
}
if (location.origin === HOME) {
const newAuth = getAuthFromCookie();
await setAuth(newAuth);
if (newAuth && await getValue(`authNoticeShown`)) {
alert(`Lemmy Universal Link Switcher: Post/comment rewriting has been set up successfully`);
await setValue(`authNoticeShown`, null);
}
} else if (HOME && !await getValue(`authNoticeShown`)) {
await setValue(`authNoticeShown`, `true`);
alert(constants_default.SETUP_AUTH_MESSAGE);
}
}
function updateAuthPeriodically() {
setInterval(async () => {
const prev = AUTH;
const newAuth = location.origin === HOME ? getAuthFromCookie() : await getAuth();
if (prev !== newAuth) {
debug(`Auth changed`);
await setAuth(newAuth);
clearMissingUrlsInCache();
triggerRewrite();
}
}, 1234);
}
async function getAuth() {
return await getValue(`auth`);
}
// src/rewriting/url-mapping.js
function splitPaths(url) {
return url.pathname.split(`/`).slice(1);
}
function isRemoteLemmyUrl(url) {
return !isHomeInstance(url) && isLemmyInstance(url);
}
function isRemoteKbinUrl(url) {
return !isHomeInstance(url) && isKbinInstance(url);
}
function isRemoteUrl(url) {
return isRemoteLemmyUrl(url) || isRemoteKbinUrl(url);
}
function findLocalUrlForStandardAtFormat(url, rootPath) {
const paths = splitPaths(url);
const name = paths[1].includes(`@`) ? paths[1] : paths[1] + `@` + url.host;
return `${HOME}/${rootPath || paths[0]}/${name}` + url.search + url.hash;
}
function findLocalUrlForLemmyUrl(url) {
if (isLemmyUserOrCommunityUrl(url)) {
return findLocalUrlForStandardAtFormat(url);
} else {
return null;
}
}
function findLocalUrlForKbinUserUrl(url) {
const paths = splitPaths(url);
const user = paths[1].startsWith(`@`) ? paths[1].substring(1) : paths[1];
const name = user.includes(`@`) ? user : user + `@` + url.host;
return `${HOME}/u/${name}` + url.search + url.hash;
}
function findLocalUrlForKbinUrl(url) {
if (isKbinMagazineUrl(url)) {
return findLocalUrlForStandardAtFormat(url, mappedKbinRootPath(url));
} else if (isKbinUserUrl(url)) {
return findLocalUrlForKbinUserUrl(url);
} else {
return null;
}
}
function findLocalUrl(url) {
if (isRemoteLemmyUrl(url))
return findLocalUrlForLemmyUrl(url);
if (isRemoteKbinUrl(url))
return findLocalUrlForKbinUrl(url);
return null;
}
async function fetchApIdFromRemote(url) {
const endpoint = isLemmyPostUrl(url) ? `post` : `comment`;
const paths = splitPaths(url);
const id = paths[1];
return new Promise((resolve, reject) => {
performXmlHttpRequest(`${url.origin}/api/v3/${endpoint}?id=${id}`, (response, data) => {
const apId = data[`${endpoint}_view`]?.[endpoint]?.ap_id;
if (response.status === 200 && apId) {
resolve(apId);
} else {
handleFailedRequest(`fetching AP ID`, response, reject);
}
});
});
}
function handleFailedRequest(requestName, response, reject) {
if (response.status >= 200 && response.status <= 299) {
reject(`${requestName}: Unhandled successful response, status: ${response.status}`);
} else if (response.status >= 400 && response.status <= 599) {
reject(`${requestName}: Error, status: ${response.status}`);
} else {
reject(`${requestName}: Something weird happened, status: ${response.status}`);
}
}
async function resolveObjectFromHome(url) {
return new Promise(async (resolve, reject) => {
const auth = await getAuth();
if (!auth) {
debug(`No auth token found`);
reject(constants_default.AUTH_MISSING);
}
performXmlHttpRequest(
`${HOME}/api/v3/resolve_object?auth=${auth}&q=${encodeURIComponent(url.href)}`,
(response, data) => {
if (response.status === 200 && data.post?.post?.id) {
resolve(`${HOME}/post/${data.post.post.id}${url.search}${url.hash}`);
} else if (response.status === 200 && data.comment?.comment?.id) {
resolve(`${HOME}/comment/${data.comment.comment.id}${url.search}${url.hash}`);
} else if (response.status === 400 && data?.error === `couldnt_find_object`) {
resolve(null);
} else if (response.status === 400 && data?.error === `not_logged_in`) {
reject(constants_default.AUTH_WRONG);
} else {
handleFailedRequest(`resolving object`, response, reject);
}
}
);
});
}
var urlCache = {};
function clearMissingUrlsInCache() {
for (const value of Object.values(urlCache)) {
if (value.error)
delete value.error;
if (value.localUrl === null)
delete value.localUrl;
}
}
function getCacheKey(url) {
return url.host + url.pathname + url.search;
}
function cacheResult(url, localUrl) {
const key = getCacheKey(url);
if (!urlCache[key]) {
urlCache[key] = {};
}
if (urlCache[key].error)
delete urlCache[key].error;
urlCache[key].localUrl = localUrl;
return localUrl;
}
function cacheErrorResult(url, error) {
const key = getCacheKey(url);
if (!urlCache[key]) {
urlCache[key] = {};
}
urlCache[key].error = error;
}
function getLocalUrlfromCache(url) {
const key = getCacheKey(url);
if (urlCache[key]?.error) {
throw urlCache[key]?.error;
} else {
return urlCache[key]?.localUrl;
}
}
async function fetchLocalUrl(url, loadFromCache = true) {
if (loadFromCache) {
const cached = getLocalUrlfromCache(url);
if (cached !== void 0) {
trace(`Found URL ${url} in cache: ${cached}`);
return cached;
}
}
try {
return cacheResult(url, await fetchLocalUrlNoCache(url));
} catch (e) {
debug(`fetchLocalUrl error`, e);
cacheErrorResult(url, e);
throw e;
}
}
async function fetchLocalUrlNoCache(url) {
trace(`Trying to resolve URL ${url} directly`);
const localUrl = await resolveObjectFromHome(url);
if (localUrl !== null) {
return localUrl;
} else {
trace(`Did not find URL ${url} directly`);
}
const apId = new URL(await fetchApIdFromRemote(url));
trace(`Found AP ID for URL ${url}: ${apId}`);
if (!apId.search) {
apId.search = url.search;
}
if (!apId.hash) {
apId.hash = url.hash;
}
if (isSamePage(url, apId)) {
trace(`Previous URL was AP ID already, URL not federated for some reason`);
return null;
} else {
return await resolveObjectFromHome(apId);
}
}
function isInstantlyRewritable(url) {
return isRemoteLemmyUrl(url) && isLemmyUserOrCommunityUrl(url) || isRemoteKbinUrl(url) && (isKbinMagazineUrl(url) || isKbinUserUrl(url));
}
function isRewritableAfterResolving(url) {
return isRemoteLemmyUrl(url) && (isLemmyPostUrl(url) || isLemmyCommentUrl(url));
}
function isLemmyPostUrl(url) {
const paths = splitPaths(url);
return paths[0] === `post`;
}
function isLemmyCommentUrl(url) {
const paths = splitPaths(url);
return paths[0] === `comment`;
}
function isLemmyUserOrCommunityUrl(url) {
const paths = splitPaths(url);
return paths[0] === `c` || paths[0] === `u`;
}
function isKbinPostUrl(url) {
const paths = splitPaths(url);
return paths[0] === `m` && paths.length > 2 && paths[2] === `t`;
}
function isKbinMicroblogUrl(url) {
const paths = splitPaths(url);
return paths[0] === `m` && paths.length > 2 && paths[2] === `p`;
}
function isKbinMicroblogOverviewUrl(url) {
const paths = splitPaths(url);
return paths[0] === `m` && paths.length > 2 && paths[2] === `microblog`;
}
function isKbinMagazinePeopleUrl(url) {
const paths = splitPaths(url);
return paths[0] === `m` && paths.length > 2 && paths[2] === `people`;
}
function isKbinMagazineUrl(url) {
const paths = splitPaths(url);
return paths[0] === `m` && !isKbinPostUrl(url) && !isKbinMagazinePeopleUrl(url) && !isKbinMicroblogUrl(url) && !isKbinMicroblogOverviewUrl(url);
}
function mappedKbinRootPath(url) {
const paths = splitPaths(url);
if (paths[0] === `m`) {
return `c`;
} else {
return null;
}
}
function isKbinUserUrl(url) {
const paths = splitPaths(url);
return paths.length === 2 && paths[0] === `u`;
}
// src/rewriting/links/icon.js
function getIcon(link) {
return link.querySelector(`.` + constants_default.ICON_CLASS);
}
function createIcon(link) {
ensureTemplateAvailable();
ensureIconStylesAdded();
const wrapper = document.createElement(`span`);
registerAddedNode(constants_default.ICON_CLASS, `.` + constants_default.ICON_CLASS);
wrapper.classList.add(constants_default.ICON_CLASS);
if (link.children.length === 0 || getComputedStyle(link.children[link.children.length - 1]).marginRight === `0px`) {
wrapper.style.marginLeft = `0.5em`;
}
const linkIcon = createSVG();
linkIcon.classList.add(constants_default.ICON_LINK_CLASS);
linkIcon.innerHTML = `<use href=#${constants_default.ICON_LINK_SYMBOL_ID} />`;
wrapper.append(linkIcon);
const spinnerIcon = createSVG();
spinnerIcon.classList.add(constants_default.ICON_SPINNER_CLASS);
spinnerIcon.innerHTML = `<use href=#${constants_default.ICON_SPINNER_SYMBOL_ID} />`;
wrapper.append(spinnerIcon);
return wrapper;
}
function createSVG() {
return document.createElementNS(`http://www.w3.org/2000/svg`, `svg`);
}
function ensureTemplateAvailable() {
if (document.querySelector(`#` + constants_default.ICON_SVG_TEMPLATE_ID))
return;
const template = createSVG();
template.id = constants_default.ICON_SVG_TEMPLATE_ID;
template.innerHTML = `<defs>
<symbol id=${constants_default.ICON_LINK_SYMBOL_ID} viewBox="0 0 100 100"><path d="M52.8 34.6c.8.8 1.8 1.2 2.8 1.2s2-.4 2.8-1.2c1.5-1.5 1.5-4 0-5.6l-5.2-5.2h26v30.6c0 2.2 1.8 3.9 3.9 3.9 2.2 0 3.9-1.8 3.9-3.9V19.8c0-2.2-1.8-3.9-3.9-3.9h-30l5.2-5.2c1.5-1.5 1.5-4 0-5.6s-4-1.5-5.6 0l-11.8 12c-1.5 1.5-1.5 4 0 5.6l11.9 11.9zM31.1 28.7V11c0-3-2.5-5.5-5.5-5.5H8C5 5.5 2.5 8 2.5 11v17.7c0 3 2.5 5.5 5.5 5.5h17.7c3 0 5.4-2.5 5.4-5.5zM47.2 65.4c-1.5-1.5-4-1.5-5.6 0s-1.5 4 0 5.6l5.2 5.2h-26V45.6c0-2.2-1.8-3.9-3.9-3.9S13 43.5 13 45.6v34.5c0 2.2 1.8 3.9 3.9 3.9h30l-5.2 5.2c-1.5 1.5-1.5 4 0 5.6.8.8 1.8 1.2 2.8 1.2s2-.4 2.8-1.2l11.9-11.9c1.5-1.5 1.5-4 0-5.6l-12-11.9zM92 65.8H74.4c-3 0-5.5 2.5-5.5 5.5V89c0 3 2.5 5.5 5.5 5.5H92c3 0 5.5-2.5 5.5-5.5V71.3c0-3-2.5-5.5-5.5-5.5z"/></symbol>
<symbol id=${constants_default.ICON_SPINNER_SYMBOL_ID} viewBox="0 0 32 32"><path d="M16 32c-4.274 0-8.292-1.664-11.314-4.686s-4.686-7.040-4.686-11.314c0-3.026 0.849-5.973 2.456-8.522 1.563-2.478 3.771-4.48 6.386-5.791l1.344 2.682c-2.126 1.065-3.922 2.693-5.192 4.708-1.305 2.069-1.994 4.462-1.994 6.922 0 7.168 5.832 13 13 13s13-5.832 13-13c0-2.459-0.69-4.853-1.994-6.922-1.271-2.015-3.066-3.643-5.192-4.708l1.344-2.682c2.615 1.31 4.824 3.313 6.386 5.791 1.607 2.549 2.456 5.495 2.456 8.522 0 4.274-1.664 8.292-4.686 11.314s-7.040 4.686-11.314 4.686z"/></symbol>
</defs>`;
registerAddedNode(constants_default.ICON_SVG_TEMPLATE_ID, `#` + constants_default.ICON_SVG_TEMPLATE_ID);
document.head.append(template);
}
function ensureIconStylesAdded() {
if (document.querySelector(`#` + constants_default.ICON_STYLES_ID))
return;
const style = document.createElement(`style`);
style.id = constants_default.ICON_STYLES_ID;
style.innerHTML = `
.${constants_default.ICON_SPINNER_CLASS} {
display: none;
animation: spins 2s linear infinite;
}
.${constants_default.ICON_LINK_CLASS} {
display: inline-block;
}
.${constants_default.ICON_LOADING_CLASS} > .${constants_default.ICON_LINK_CLASS} {
display: none;
}
.${constants_default.ICON_LOADING_CLASS} > .${constants_default.ICON_SPINNER_CLASS} {
display: inline-block;
}
.${constants_default.ICON_CLASS} > svg {
vertical-align: sub;
height: 1em; width: 1em;
stroke: currentColor;
fill: currentColor;
}`;
registerAddedNode(constants_default.ICON_STYLES_ID, `#` + constants_default.ICON_STYLES_ID);
document.head.append(style);
}
// src/tippy.js
var tippy_default = window.tippy;
// src/rewriting/links/tooltip.js
function getOriginalLinkHtml(originalHref) {
registerAddedNode(constants_default.ORIGINAL_LINK_CLASS, `.` + constants_default.ORIGINAL_LINK_CLASS);
return `Original link: <a class="${constants_default.ORIGINAL_LINK_CLASS}" href="${originalHref}">${originalHref}</a>`;
}
function defaultOptions(link) {
return {
appendTo: () => link.parentNode,
allowHTML: true,
interactive: true,
animation: false,
placement: "bottom",
hideOnClick: false
};
}
function createOriginalLinkTooltip(link, originalHref) {
trace(`Create original link tooltip`, link, originalHref);
getIcon(link).addEventListener(`click`, stopEventHandler);
return createLinkTooltip(link, getOriginalLinkHtml(originalHref));
}
function createLinkTooltip(link, content) {
return tippy_default(getIcon(link), {
...defaultOptions(link),
content
});
}
function createLinkLoadTooltip(link) {
trace(`Create link load tooltip`, link);
getIcon(link).classList.add(constants_default.ICON_LOADING_CLASS);
return createLinkTooltip(link, `Loading home URL...<br />Don't want to wait? ${getOriginalLinkHtml(link.href)}`);
}
function linkLoadTooltipSuccess(tooltip, originalHref) {
linkLoadResult(tooltip, `\u2714\uFE0F Changed link to home instance`, getOriginalLinkHtml(originalHref));
}
function linkLoadTooltipError(tooltip, error) {
linkLoadResult(tooltip, `\u274C ` + error);
}
function linkLoadResult(tooltip, result, finalContent = result) {
const icon = tooltip.reference;
icon.classList.remove(constants_default.ICON_LOADING_CLASS);
icon.addEventListener(`click`, stopEventHandler);
if (tooltip.state.isVisible) {
tooltip.setContent(result);
setTimeout(() => {
tooltip.hide();
tooltip.setContent(finalContent);
}, 2e3);
} else {
tooltip.setContent(finalContent);
}
}
// src/rewriting/links/links.js
function changeLinkHref(link, localUrl) {
const treeWalker = document.createTreeWalker(link, NodeFilter.SHOW_TEXT, (node) => {
if (node.textContent.toLowerCase().trim() === link.href.toLowerCase().trim()) {
return NodeFilter.FILTER_ACCEPT;
} else {
return NodeFilter.FILTER_SKIP;
}
});
let textNode;
while ((textNode = treeWalker.nextNode()) !== null) {
textNode.textContent = localUrl;
}
link.href = localUrl;
link.addEventListener(`click`, (event) => {
if (event.button === 0 && !event.ctrlKey && !event.metaKey && link.target !== `_blank`) {
location.href = localUrl;
}
});
}
function appendIconTo(elem, icon) {
if (elem.children.length === 0 || getComputedStyle(elem.children[elem.children.length - 1]).display !== `inline-block`) {
elem.append(icon);
} else {
appendIconTo(elem.children[elem.children.length - 1], icon);
}
}
function addFetchLocalUrlHandler(link) {
let tooltip;
const handler = async (event) => {
if (event.type === `click`) {
stopEventHandler(event);
if (tooltip)
tooltip.show();
return;
}
link.removeEventListener(`focus`, handler);
link.removeEventListener(`mouseenter`, handler);
if (link.dataset[constants_default.REWRITE_STATUS] === constants_default.REWRITE_STATUS_PENDING)
return;
link.dataset[constants_default.REWRITE_STATUS] = constants_default.REWRITE_STATUS_PENDING;
tooltip = createLinkLoadTooltip(link);
try {
const localUrl = await fetchLocalUrl(link);
if (!localUrl) {
debug(`Local URL for ${link.href} could not be found`);
linkLoadTooltipError(tooltip, `Home URL could not be found`);
return;
}
trace(`Local URL for ${link.href} found: ${localUrl}`);
const oldHref = link.href;
changeLinkHref(link, localUrl);
linkLoadTooltipSuccess(tooltip, oldHref);
link.dataset[constants_default.REWRITE_STATUS] = constants_default.REWRITE_STATUS_SUCCESS;
} catch (e) {
debug(`Error while trying to resolve local URL`, e);
let msg;
if (e === constants_default.AUTH_WRONG) {
msg = `Saved login expired. Return to your home instance and log in again.`;
} else if (e === constants_default.AUTH_MISSING) {
msg = constants_default.SETUP_AUTH_MESSAGE;
} else {
msg = `Error while trying to find home URL`;
}
linkLoadTooltipError(tooltip, msg);
link.dataset[constants_default.REWRITE_STATUS] = constants_default.REWRITE_STATUS_ERROR;
} finally {
link.removeEventListener(`click`, handler);
}
};
link.addEventListener(`click`, handler);
link.addEventListener(`focus`, handler);
link.addEventListener(`mouseenter`, handler);
}
function isFediverseLink(link) {
const svg = link.querySelector(`svg`);
if (!svg)
return false;
if (svg.children.length === 0)
return false;
return svg.children[0].getAttribute(`xlink:href`)?.includes(`#icon-fedilink`);
}
function rewriteToLocal(link) {
if (!link.parentNode)
return false;
if (link.classList.contains(constants_default.ORIGINAL_LINK_CLASS))
return false;
if (link.dataset[constants_default.REWRITE_STATUS] === constants_default.REWRITE_STATUS_SUCCESS)
return false;
if (isHashLink(link))
return false;
if (!isRemoteUrl(link))
return false;
if (isFediverseLink(link))
return false;
if (isInstantlyRewritable(link)) {
const localUrl = findLocalUrl(link);
if (!localUrl)
return false;
if (isSamePage(new URL(localUrl), location))
return false;
const oldHref = link.href;
changeLinkHref(link, localUrl);
const icon = createIcon(link);
appendIconTo(link, icon);
createOriginalLinkTooltip(link, oldHref);
link.dataset[constants_default.REWRITE_STATUS] = constants_default.REWRITE_STATUS_SUCCESS;
trace(`Rewrite link`, link, ` from`, oldHref, `to`, localUrl);
return true;
} else if (isRewritableAfterResolving(link)) {
if (!getIcon(link)) {
appendIconTo(link, createIcon(link));
}
if (!link.dataset[constants_default.REWRITE_STATUS]) {
link.dataset[constants_default.REWRITE_STATUS] = constants_default.REWRITE_STATUS_UNRESOLVED;
addFetchLocalUrlHandler(link);
}
}
}
function findLinksInChange(change) {
if (change.type === `childList`) {
const links = Array.from(change.addedNodes).flatMap((addedNode) => {
if (addedNode.tagName?.toLowerCase() === `a`) {
return addedNode;
} else if (addedNode.querySelectorAll) {
return Array.from(addedNode.querySelectorAll(`a`));
} else {
return [];
}
});
if (links.length > 0)
trace(`Change`, change, `contained the links`, links);
return links;
} else if (change.type === `attributes`) {
return change.target.matches?.(`a`) ? change.target : [];
} else {
return [];
}
}
function findLinksToRewrite(changes) {
if (!changes) {
return document.querySelectorAll(`a`);
}
return changes.flatMap(findLinksInChange);
}
async function rewriteLinksToLocal(changes) {
const links = findLinksToRewrite(changes);
const chunkSize = 50;
return await async function processChunk(currentChunk) {
const startIdx = currentChunk * chunkSize;
const endChunkIdx = (currentChunk + 1) * chunkSize;
const endIdx = Math.min(links.length, endChunkIdx);
debug(
`Processing ${links.length} links, current chunk `,
currentChunk,
`processing links ${startIdx} to ${endIdx}`
);
let anyRewritten = false;
for (let i = startIdx; i < endIdx; ++i) {
const rewritten = rewriteToLocal(links[i]);
anyRewritten = anyRewritten || rewritten;
}
debug(`Processed links ${startIdx} to ${endIdx}`);
if (endChunkIdx >= links.length) {
return anyRewritten;
}
const chunkResult = await new Promise((resolve) => setTimeout(async () => {
resolve(await processChunk(currentChunk + 1));
}, 0));
return anyRewritten || chunkResult;
}(0);
}
// src/settings.js
var refreshMethods = [];
async function refresh() {
for (const refreshMethod of refreshMethods) {
await refreshMethod();
}
}
function closeSettings() {
document.querySelector(`#` + constants_default.SETTINGS_MENU_ID)?.remove();
refreshMethods = [];
}
function initSettings() {
registerMenuCommand(`Open Settings`, showSettings);
}
function styles(elem, style) {
for (const [prop, val] of Object.entries(style)) {
elem.style[prop] = val;
}
}
var defaultBorder = `1px solid #AAA`;
function createButton(content, options = {}) {
const button = document.createElement(`div`);
button.innerHTML = content;
styles(button, {
display: options.inline ? `inline-block` : `block`,
backgroundColor: `#666`,
textAlign: `center`,
border: defaultBorder,
cursor: `pointer`,
borderRadius: `8px 8px`,
padding: `5px`
});
return button;
}
function ensureMenuStylesAdded() {
if (document.querySelector(`#` + constants_default.SETTINGS_STYLES_ID))
return;
const style = document.createElement(`style`);
style.id = constants_default.SETTINGS_STYLES_ID;
style.innerHTML = `
#${constants_default.SETTINGS_MENU_ID} * {
box-sizing: border-box;
}
`;
document.head.append(style);
}
function showSettings() {
if (document.querySelector(`#` + constants_default.SETTINGS_MENU_ID))
return;
if (window !== window.top)
return;
ensureMenuStylesAdded();
const background = document.createElement(`div`);
background.id = constants_default.SETTINGS_MENU_ID;
registerAddedNode(constants_default.SETTINGS_MENU_ID, `#` + constants_default.SETTINGS_MENU_ID);
styles(background, {
position: `fixed`,
top: `0`,
left: `0`,
zIndex: 9999,
width: `100vw`,
height: `100vh`,
backgroundColor: `#00000099`,
backdropFilter: `blur(6px)`,
display: `grid`,
grid: `grid`,
alignItems: `center`,
justifyContent: `space-around`
});
background.addEventListener(`click`, (event) => {
if (event.target === background) {
closeSettings();
}
});
const menu = document.createElement(`div`);
styles(menu, {
position: `relative`,
color: `#ddd`,
maxWidth: `90vw`,
maxHeight: `90vh`,
backgroundColor: `#555`,
padding: `20px`,
border: defaultBorder,
borderRadius: `8px`,
overflow: `hidden auto`
});
background.append(menu);
const closeButton = createButton(`\u{1F5D9}`);
styles(closeButton, {
position: `absolute`,
top: `-1px`,
right: `-1px`,
width: `30px`,
height: `30px`,
lineHeight: `25px`,
fontSize: `25px`,
borderRadius: `0 8px`,
padding: 0
});
closeButton.addEventListener(`click`, () => closeSettings());
menu.append(closeButton);
const menuHeader = createSettingHeader(`Lemmy Universal Link Switcher Settings`, 1.5);
styles(menuHeader, {
textDecoration: `underline`,
marginTop: 0
});
menu.append(menuHeader);
addMainHomeInstanceSetting(menu);
addMoreHomeInstancesSettings(menu);
document.body.append(background);
}
function createSettingHeader(text, sizeMultiplicator = 1) {
const header = document.createElement(`div`);
header.innerHTML = text;
styles(header, {
marginRight: `10px`,
fontSize: `${sizeMultiplicator * 1.4}em`,
lineHeight: `${sizeMultiplicator * 1.4}em`,
fontWeight: `bold`,
marginTop: `${sizeMultiplicator * 25}px`,
marginBottom: `${sizeMultiplicator * 10}px`
});
return header;
}
function addInfo(elem, content) {
const info = document.createElement(`span`);
info.innerHTML = ` \u{1F6C8}`;
tippy_default(info, {
content,
placement: "bottom",
triggerTarget: elem
});
elem.append(info);
}
function createInput(options) {
const inputElement = document.createElement(`input`);
styles(inputElement, {
width: `100%`,
margin: 0
});
if (options.placeholder) {
inputElement.placeholder = options.placeholder;
}
if (options.getter) {
Promise.resolve(options.getter()).then((result) => inputElement.value = result);
}
if (!inputElement.value) {
inputElement.setCustomValidity(`empty`);
}
styles(inputElement, { border: defaultBorder });
if (!options.validator && !options.setter) {
return;
}
let validator = options.validator || (() => true);
let setter = options.setter || (() => {
});
inputElement.addEventListener(`input`, async () => {
const validated = await validator(inputElement.value);
if (!inputElement.value || validated) {
inputElement.setCustomValidity(`empty`);
styles(inputElement, {
border: defaultBorder
});
}
if (inputElement.value) {
if (validated) {
inputElement.setCustomValidity(``);
await setter(inputElement.value);
} else if (!validated) {
inputElement.setCustomValidity(`fail`);
styles(inputElement, {
border: `1px solid red`
});
}
}
});
return inputElement;
}
var urlValidator = (value) => {
try {
new URL(value);
return true;
} catch (e) {
return false;
}
};
var instancePlaceholder = `The full link (including http(s)://) to the instance`;
function addMainHomeInstanceSetting(addTo) {
const header = createSettingHeader(`Main Home Instance`);
addInfo(header, `All links will be rewritten to this instance, except for links to your secondary home instances.`);
addTo.append(header);
const mainInstance = createInput({
getter: () => HOME,
setter: (value) => {
const url = new URL(value);
if (HOME !== url.origin) {
setHome(url.origin);
refresh();
}
},
validator: urlValidator,
placeholder: instancePlaceholder
});
addTo.append(mainInstance);
const homeButtonWrapper = document.createElement(`div`);
const refreshMakeHomeButton = () => {
homeButtonWrapper.replaceChildren();
if (location.origin === HOME)
return;
const makeHomeButton = createButton(`Use current page as home instance`);
styles(makeHomeButton, {
borderRadius: `0 0 8px 8px`
});
makeHomeButton.addEventListener(`click`, () => {
mainInstance.value = location.origin;
mainInstance.dispatchEvent(new Event(`input`));
});
homeButtonWrapper.append(makeHomeButton);
};
refreshMakeHomeButton();
refreshMethods.push(refreshMakeHomeButton);
addTo.append(homeButtonWrapper);
}
async function addMoreHomeInstancesSettings(addTo) {
const header = createSettingHeader(`Secondary Home Instances`);
addInfo(header, `All links pointing to these instances will not be changed.`);
addTo.append(header);
addTo.append(createListInput(
async () => await getSecondaryHomeInstances(),
async (value) => {
const homeInstances = await getSecondaryHomeInstances();
homeInstances.push(new URL(value).origin);
await setSecondaryHomeInstances(homeInstances);
},
async (value) => {
const homeInstances = await getSecondaryHomeInstances();
var index = homeInstances.indexOf(new URL(value).origin);
if (index > -1) {
homeInstances.splice(index, 1);
}
await setSecondaryHomeInstances(homeInstances);
}
));
}
function createListItem(item, onClick) {
const listItem = createButton(item + ` \u{1F5D9}`, { inline: true });
styles(listItem, {
margin: `0px 5px 5px 0`
});
listItem.addEventListener(`click`, onClick);
return listItem;
}
function createListInput(getter, add, remove) {
const wrapper = document.createElement(`div`);
const list = document.createElement(`div`);
styles(list, {
marginBottom: `8px`
});
const refreshList = async () => {
const items = (await getter()).sort().map((item) => createListItem(item, async () => {
await remove(item);
refresh();
}));
if (items.length) {
list.replaceChildren(...items);
} else {
list.replaceChildren(`<None>`);
}
};
refreshList();
refreshMethods.push(refreshList);
wrapper.append(list);
const addInput = createInput({
validator: async (value) => {
return urlValidator(value) && !(await getter()).includes(value);
},
placeholder: instancePlaceholder
});
wrapper.append(addInput);
const buttonWrapper = document.createElement(`div`);
const refreshButtons = async () => {
buttonWrapper.replaceChildren();
const isCurrentPageHome = HOME === location.origin || (await getter()).includes(location.origin);
const addButton2 = createButton(`Add`);
styles(addButton2, {
borderRadius: isCurrentPageHome ? `0 0 8px 8px` : `0`
});
addButton2.addEventListener(`click`, async () => {
if (addInput.validity.valid) {
await add(addInput.value);
addInput.value = ``;
refresh();
}
});
buttonWrapper.append(addButton2);
if (!isCurrentPageHome) {
const addCurrentButton = createButton(`Add current page`);
styles(addCurrentButton, {
borderRadius: `0 0 8px 8px`
});
addCurrentButton.addEventListener(`click`, async () => {
await add(location.origin);
addInput.value = ``;
refresh();
});
buttonWrapper.append(addCurrentButton);
}
};
refreshButtons();
refreshMethods.push(refreshButtons);
wrapper.append(buttonWrapper);
return wrapper;
}
// src/rewriting/settings-buttons.js
function addSettingsButton() {
if (location.pathname !== `/settings`)
return false;
if (!document.querySelector(`[name="Description"][content="Lemmy"]`))
return false;
if (document.querySelector(`#` + constants_default.SETTINGS_BUTTON_ID))
return false;
const insertAfter = document.querySelector(`#user-password`)?.closest(`.card`);
if (!insertAfter)
return;
const button = document.createElement(`button`);
button.id = constants_default.SETTINGS_BUTTON_ID;
button.setAttribute(`class`, `btn btn-block btn-primary mr-4 w-100`);
button.innerHTML = `Lemmy Universal Link Switcher Settings`;
button.addEventListener(`click`, showSettings);
registerAddedNode(constants_default.SETTINGS_BUTTON_ID, `#` + constants_default.SETTINGS_BUTTON_ID);
insertAfter.insertAdjacentElement("afterend", button);
return true;
}
// src/rewriting/show-at-home.js
function showAtHomeButtonText() {
const host = new URL(HOME).hostname;
return `Show on ${host}`;
}
function createShowAtHomeAnchor(localUrl) {
const showAtHome = document.createElement(`a`);
showAtHome.dataset.creationHref = location.href;
showAtHome.classList.add(constants_default.SHOW_AT_HOME_BUTTON_CLASS);
showAtHome.innerHTML = showAtHomeButtonText();
showAtHome.href = localUrl;
registerAddedNode(constants_default.SHOW_AT_HOME_BUTTON_CLASS, `.` + constants_default.SHOW_AT_HOME_BUTTON_CLASS);
return showAtHome;
}
function addLemmyShowAtHomeButton(localUrl) {
const logo = document.querySelector(`a.navbar-brand`);
const navbarIcons = isV17() ? document.querySelector(`[title="Search"]`)?.closest(`.navbar-nav`) : document.querySelector(`#navbarIcons`);
if (!logo || !navbarIcons) {
debug(`Could not find position to insert ShowAtHomeButton at`);
return false;
}
const mobile = createShowAtHomeAnchor(localUrl);
mobile.classList.add(`d-md-none`);
mobile.style[`margin-right`] = `8px`;
mobile.style[`margin-left`] = `auto`;
if (isV17()) {
document.querySelector(`.navbar-nav.ml-auto`)?.classList.remove(`ml-auto`);
}
logo.insertAdjacentElement("afterend", mobile);
const desktop = createShowAtHomeAnchor(localUrl);
desktop.classList.add(`d-md-inline`);
desktop.style[`margin-right`] = `8px`;
navbarIcons.insertAdjacentElement("beforebegin", desktop);
return true;
}
function addKbinShowAtHomeButton(localUrl) {
const prependTo = document.querySelector(`#header menu:not(.head-nav__menu)`);
if (!prependTo) {
debug(`Could not find position to insert ShowAtHomeButton at`);
return false;
}
const item = document.createElement(`li`);
item.append(createShowAtHomeAnchor(localUrl));
prependTo.prepend(item);
return true;
}
function addButton(localUrl) {
const oldButton = document.querySelectorAll(`.` + constants_default.SHOW_AT_HOME_BUTTON_CLASS);
if (oldButton.length > 0 && oldButton[0].dataset.creationHref !== location.href) {
debug(`Removing old show at home button`);
oldButton.forEach((btn) => btn.remove());
} else if (oldButton.length > 0) {
debug(`Old show at home button still exists`);
return false;
}
if (!localUrl) {
debug(`No local URL for show at home button found`);
return false;
} else if (isRemoteLemmyUrl(location)) {
return addLemmyShowAtHomeButton(localUrl);
} else if (isRemoteKbinUrl(location)) {
return addKbinShowAtHomeButton(localUrl);
} else {
return false;
}
}
async function addShowAtHomeButton() {
if (isInstantlyRewritable(location)) {
return addButton(findLocalUrl(location));
} else if (isRewritableAfterResolving(location)) {
try {
return addButton(await fetchLocalUrl(location));
} catch (e) {
debug(`Error while trying to add "show at home" button`, e);
}
}
}
// src/rewriting/rewrite.js
function triggerRewrite() {
doAllDomChanges();
}
function isOrHasOurAddedNode(node) {
return getAddedNodesSelectors().some((selector) => node.matches?.(selector) || node.querySelector?.(selector));
}
function isIrrelevantChange(change) {
if (change.type === `childList`) {
if (Array.from(change.removedNodes).some(isOrHasOurAddedNode)) {
trace(`Change`, change, `is relevant because a removed node is/has ours`);
return false;
}
if (!Array.from(change.addedNodes).every(isOrHasOurAddedNode)) {
trace(`Change`, change, `is relevant because not every added node is/has ours`);
return false;
}
} else if (change.type === `attributes` && isRemoteUrl(new URL(change.target.href, location.origin))) {
trace(`Change`, change, `is relevant because href changed to a remote URL`);
return false;
}
trace(`Change`, change, `is irrelevant`);
return true;
}
async function startRewriting() {
new MutationObserver((changes, observer) => {
debug(`MutationObserver triggered`, changes);
if (changes.every(isIrrelevantChange)) {
debug(`All observed changes are irrelevant`);
return;
}
doAllDomChanges(changes);
}).observe(document.body, {
subtree: true,
childList: true,
attributeFilter: [`href`]
});
await doAllDomChanges();
}
async function doAllDomChanges(changes) {
debug(`doAllDomChanges start`);
const addedSettingsButtons = addSettingsButton();
if (addedSettingsButtons)
debug(`Added Settings Buttons`);
const addedShowAtHomeButton = HOME ? addShowAtHomeButton() : false;
if (addedShowAtHomeButton)
debug(`Added Show At Home Button`);
const rewrittenLinks = HOME ? await rewriteLinksToLocal(changes) : false;
if (rewrittenLinks)
debug(`Rewritten some links`);
debug(`doAllDomChanges end`);
}
// src/home.js
var HOME;
var secondaryHomes = [];
async function initHome() {
HOME = await getValue(`home`);
secondaryHomes = await getSecondaryHomeInstances();
if (!HOME && isLemmyInstance(location) && confirm(`Lemmy Universal Link Switcher: Set this instance to be your home instance to which all URLs get rewritten to?`)) {
setHome(location.origin);
}
}
async function setHome(newHome) {
trace(`Set HOME from ${HOME} to ${newHome}`);
if (typeof newHome !== `string`) {
newHome = null;
}
HOME = newHome;
await setValue(`home`, newHome);
}
async function getHome() {
return await getValue(`home`);
}
function updateHomePeriodically() {
trace(`Current HOME`, HOME);
setInterval(async () => {
const prevHome = HOME;
const prevSecondaries = secondaryHomes;
HOME = await getHome();
secondaryHomes = await getSecondaryHomeInstances();
if (prevHome !== HOME) {
debug(`HOME changed externally from`, prevHome, `to`, HOME);
triggerRewrite();
} else if (prevSecondaries.length !== secondaryHomes.length || !prevSecondaries.every((v) => secondaryHomes.includes(v))) {
debug(`secondaryHomes changed externally from`, prevSecondaries, `to`, secondaryHomes);
triggerRewrite();
}
}, 1337);
}
function isHomeInstance(url) {
return secondaryHomes.concat(HOME).includes(url.origin);
}
async function getSecondaryHomeInstances() {
const homeInstancesStr = await getValue(`secondaryHomes`);
return homeInstancesStr ? JSON.parse(homeInstancesStr) : [];
}
async function setSecondaryHomeInstances(homeInstances) {
await setValue(`secondaryHomes`, JSON.stringify(homeInstances));
}
// src/main.js
(async () => {
await initHome();
updateHomePeriodically();
await initAuth();
updateAuthPeriodically();
initSettings();
startRewriting();
})();
})();