Last active
October 11, 2022 04:12
-
-
Save eth-p/236113e174748a32e5d8f8fd3046bcdc to your computer and use it in GitHub Desktop.
A userscript to use Instagram Direct messages as a PWA.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Instagram Direct as PWA | |
// @namespace http://tampermonkey.net/ | |
// @version 0.2.1 | |
// @description Create a Chrome "as window" shortcut to use Instagram DMs as a PWA. | |
// @author eth-p | |
// @match https://www.instagram.com/direct/* | |
// @match https://www.instagram.com/?utm_source=pwa_homescreen | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=instagram.com | |
// @grant none | |
// @run-at document-start | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
const SELECTOR_FOR_HEADER = '._acum'; | |
const SELECTOR_FOR_MAIN = '._a9-0'; | |
const SELECTOR_FOR_BODY_PADDING = '._aa5b'; | |
const SELECTOR_FOR_CONVERSATIONS_PANEL = `${SELECTOR_FOR_BODY_PADDING}>div>*:first-child`; | |
const STYLES = ` | |
${SELECTOR_FOR_HEADER} { | |
display: none !important; | |
} | |
${SELECTOR_FOR_MAIN} { | |
padding-top: 0 !important; | |
} | |
${SELECTOR_FOR_BODY_PADDING} { | |
padding: 0; | |
} | |
${SELECTOR_FOR_BODY_PADDING}>div { | |
max-width: none !important; | |
border: none !important; | |
border-radius: 0 !important; | |
} | |
${SELECTOR_FOR_CONVERSATIONS_PANEL} { | |
border-right: none !important; | |
} | |
${SELECTOR_FOR_BODY_PADDING}>div>*:first-child>*:last-child { | |
border-right: 1px solid; | |
border-right-color: inherit; | |
border-right-color: var(--border-color, inherit); | |
} | |
html, body { | |
} | |
`; | |
// =============== Redirect to DMs =============== | |
if (window.location.pathname === "/" && window.location.search === "?utm_source=pwa_homescreen") { | |
window.name = "Instagram PWA UserScript"; | |
history.replaceState({}, "", "/direct/t/?utm_source=pwa_homescreen"); | |
window.location = "/direct/t/?utm_source=pwa_homescreen"; | |
return; | |
} | |
if (window.name !== "Instagram PWA UserScript") { | |
return; | |
} | |
// =============== Utilities =============== | |
/** | |
* Append a node to the body. | |
*/ | |
function appendToBody(node) { | |
if (document.body != null) { | |
document.body.appendChild(node); | |
return; | |
} | |
document.addEventListener('DOMContentLoaded', () => { | |
document.body.appendChild(node); | |
}); | |
} | |
/** | |
* Append a node to the head. | |
*/ | |
function appendToHead(node) { | |
if (document.head != null) { | |
document.head.appendChild(node); | |
return; | |
} | |
document.addEventListener('DOMContentLoaded', () => { | |
document.head.appendChild(node); | |
}); | |
} | |
/** | |
* A "polyfill" for setInterval that doesn't fail to clear. | |
*/ | |
function setInterval(fn, interval, ...args) { | |
let cancelled = false; | |
const cancelFn = () => { | |
cancelled = true; | |
} | |
const executeFn = () => { | |
fn.call(/* this: */ cancelFn, ...args); | |
if (!cancelled) { | |
setTimeout(executeFn, interval); | |
} | |
} | |
setTimeout(executeFn, interval); | |
return cancelFn; | |
} | |
/** | |
* Poll until a selector becomes available. | |
*/ | |
function whenSelect(selector, callback, interval) { | |
const element = document.querySelector(selector); | |
if (element != null) { | |
callback(element); | |
return; | |
} | |
setInterval(function check() { | |
const element = document.querySelector(selector); | |
if (element != null) { | |
callback(element); | |
this(); | |
} | |
}, interval == null ? 10 : interval); | |
} | |
/** | |
* Guards a function so it doesnt spill its errors into the Instagram | |
* website's JavaScript. | |
*/ | |
function guarded(fn) { | |
const onException = (ex) => { | |
log.call(console.error, ex); | |
}; | |
return function(...args) { | |
try { | |
const res = fn.apply(this, args); | |
if (res instanceof Promise) { | |
return res.then(v => v, err => { | |
onException(err); | |
}); | |
} | |
} catch (ex) { | |
onException(ex); | |
} | |
} | |
} | |
function log(message, ...args) { | |
const logFn = (this == null ? console.log : this).bind(console); | |
const logColor = (this === console.error ? 'red' : 'green'); | |
logFn(`%c[PWA UserScript]%c ${message}`, `color: ${logColor}`, "color: unset", ...args); | |
} | |
// =============== Hooks into Page Modules =============== | |
const __requireAvailable = new Promise((resolve) => { | |
if (window.require != null) { | |
log("Found AMD require at eval time."); | |
return resolve(window.require); | |
} | |
// Poll for require. | |
const cancelInterval = setInterval(() => { | |
if (window.require != null) { | |
log("Found AMD require from polling."); | |
cancelInterval(); | |
return resolve(window.require); | |
} | |
}, 10); | |
// Wait for DOMContentLoaded. | |
document.addEventListener('DOMContentLoaded', () => { | |
if (window.require != null) { | |
log("Found AMD require at DOMContentLoaded."); | |
cancelInterval(); | |
return resolve(window.require); | |
} | |
}); | |
}); | |
const IG_require = guarded(async (modules) => { | |
const ids = modules instanceof Array ? modules : [modules]; | |
const require = await __requireAvailable; | |
const debug = require("__debug"); | |
// Checks to see if a module is ready. | |
const isModuleReady = (id) => { | |
const mod = debug.modulesMap[id]; | |
if (mod == null) { | |
return false; | |
} | |
return mod.factoryFinished && | |
mod.nextDepWaitingHead == null && | |
mod.nextDepWaitingNext == null; | |
}; | |
// Return a promise that waits. | |
return new Promise((resolve, reject) => { | |
function tryResolve() { | |
for (const id of ids) { | |
if (!isModuleReady(id)) { | |
return false; | |
} | |
} | |
resolve(ids.map(require)); | |
return true; | |
} | |
// Try to resolve it immediately. | |
if (tryResolve()) { | |
return; | |
} | |
// Wait for it. | |
let counter = 0; | |
setInterval(function() { | |
if (tryResolve()) { | |
this(); | |
return; | |
} | |
counter++; | |
if (counter > 10000) { | |
alert("ERROR trying to hook PWA"); | |
this(); | |
} | |
}, 10); | |
}); | |
}); | |
// =============== Inject/Hook =============== | |
/** | |
* Set up the stylesheet and theme color. | |
*/ | |
function setupStyles() { | |
let styles = document.querySelector('style[data-source=instagram-dm-pwa]'); | |
if (styles == null) { | |
styles = document.createElement("style"); | |
styles.setAttribute("data-source", "instagram-dm-pwa"); | |
appendToBody(styles); | |
} | |
styles.innerHTML = STYLES | |
.replace(/^[ \t]+/g, "") // Remove indents. | |
.replace(/^\/\/.*/g, ""); // Remove comments. | |
// Set up meta theme. | |
let metaTheme = document.querySelector('meta[name="theme-color"]'); | |
if (metaTheme == null) { | |
metaTheme = document.createElement("meta"); | |
metaTheme.setAttribute("name", "theme-color"); | |
appendToHead(metaTheme); | |
} | |
let metaThemeSource = 'none'; | |
whenSelect("body", (body) => { | |
if (metaThemeSource !== 'none') return; | |
metaThemeSource = 'body'; | |
metaTheme.setAttribute("content", window.getComputedStyle(body).backgroundColor); | |
log("Setting PWA title bar color to page background."); | |
}); | |
whenSelect(SELECTOR_FOR_CONVERSATIONS_PANEL + ">:first-child", (header) => { | |
metaThemeSource = 'header'; | |
metaTheme.setAttribute("content", window.getComputedStyle(header).backgroundColor); | |
log("Setting PWA title bar color to application header."); | |
}); | |
} | |
// Inject hooks. | |
/** | |
* Hook the SPA routing to open non-DM instagram pages externally. | |
*/ | |
IG_require(["browserHistory"]).then(([browserHistory]) => { | |
const original = browserHistory.browserHistory.push; | |
browserHistory.browserHistory.push = function(...args) { | |
const url = args[0]; | |
if (url.startsWith("/direct/")) { | |
return original.apply(this, args); | |
} | |
log("Opening URL externally: %c%s", "color: #08c", url); | |
window.open(url); | |
} | |
}); | |
// Setup | |
setupStyles(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment