- // ==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();
- })();
- })();