Skip to content

Instantly share code, notes, and snippets.

@eth-p
Last active October 11, 2022 04:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eth-p/236113e174748a32e5d8f8fd3046bcdc to your computer and use it in GitHub Desktop.
Save eth-p/236113e174748a32e5d8f8fd3046bcdc to your computer and use it in GitHub Desktop.
A userscript to use Instagram Direct messages as a PWA.
// ==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