Skip to content

Instantly share code, notes, and snippets.

@stanley2058
Last active March 9, 2025 13:18
Show Gist options
  • Save stanley2058/9b83aeaeb691e13e63ed4db2301fcfef to your computer and use it in GitHub Desktop.
Save stanley2058/9b83aeaeb691e13e63ed4db2301fcfef to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name HD Facebook Emoji
// @author STW
// @description Replace all emojis with 64x64 HD images
// @match https://www.facebook.com/*
// @match https://www.messenger.com/*
// @require https://unpkg.com/@reactivex/rxjs/dist/global/rxjs.umd.min.js
// @icon https://www.google.com/s2/favicons?sz=64&domain=facebook.com
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @version 0.0.1
// ==/UserScript==
const { fromEvent } = rxjs;
const { throttleTime } = rxjs.operators;
const imgCDN =
"https://unpkg.com/[email protected]/img/facebook/64/";
const fallbackCDN =
"https://unpkg.com/[email protected]/img/google/64/";
const DATA_CACHE_KEY = "EMOJI_LIST";
const FALLBACK_KEY = "EMOJI_LIST_FALLBACK";
const IMG_DATA_CACHE_KEY = "EMOJI_DATA_LRU";
const cached = GM_getValue(DATA_CACHE_KEY, null);
const toFallback = GM_getValue(FALLBACK_KEY, null);
const emojiMap = cached ? JSON.parse(cached) : {};
const fallback = new Set(toFallback ? JSON.parse(toFallback) : []);
const imgCacheRaw = GM_getValue(IMG_DATA_CACHE_KEY, null);
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return null;
// Move the accessed key to the end to show that it was recently used
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key); // Remove the old entry
} else if (this.cache.size >= this.capacity) {
// Remove the least recently used (LRU) item (first key in Map)
const lruKey = this.cache.keys().next().value;
this.cache.delete(lruKey);
}
this.cache.set(key, value);
}
toJSON() {
return JSON.stringify({
capacity: this.capacity,
cache: Array.from(this.cache.entries()),
});
}
static fromJSON(jsonString) {
const obj = JSON.parse(jsonString);
const lru = new LRUCache(obj.capacity);
obj.cache.forEach(([key, value]) => lru.cache.set(key, value));
return lru;
}
}
const imgCache =
cached && imgCacheRaw ? LRUCache.fromJSON(imgCacheRaw) : new LRUCache(100);
if (!cached) {
GM_xmlhttpRequest({
method: "GET",
url: "https://unpkg.com/[email protected]/emoji.json",
onload: function (response) {
const emojis = JSON.parse(response.responseText);
for (const emoji of emojis) {
const key = (emoji.non_qualified || emoji.unified)
.replace(/^0+/, "")
.replaceAll(/-/g, "_")
.toLowerCase();
emojiMap[key] = emoji.image;
if (!emoji.has_img_facebook) fallback.add(key);
}
GM_setValue(DATA_CACHE_KEY, JSON.stringify(emojiMap));
GM_setValue(FALLBACK_KEY, JSON.stringify([...fallback]));
},
});
}
function blobToDataURI(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
function clearAllEmojiImgSrc() {
const images = Array.from(document.querySelectorAll("img"));
images
.filter((img) => img.src.includes("emoji.php") && img.naturalHeight < 48)
.forEach((img) => {
const imgName = img.src.split("/").at(-1).replace(".png", "");
const replacement = emojiMap[imgName];
if (!replacement) return;
const cachedImg = imgCache.get(imgName);
if (cachedImg) img.src = cachedImg;
else {
const url = fallback.has(imgName)
? `${fallbackCDN}${replacement}`
: `${imgCDN}${replacement}`;
GM.xmlHttpRequest({
url,
responseType: "blob",
}).then((res) => {
if (res.status !== 200) return;
blobToDataURI(res.response).then((uri) => {
img.src = uri;
imgCache.put(imgName, uri);
});
});
}
});
}
unsafeWindow.flushEmojiCache = () => {
GM_deleteValue(DATA_CACHE_KEY);
GM_deleteValue(FALLBACK_KEY);
GM_deleteValue(IMG_DATA_CACHE_KEY);
};
window.addEventListener("beforeunload", () => {
GM_setValue(IMG_DATA_CACHE_KEY, imgCache.toJSON());
});
fromEvent(window, "wheel")
.pipe(throttleTime(300))
.subscribe(clearAllEmojiImgSrc);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment