Last active
March 9, 2025 13:18
-
-
Save stanley2058/9b83aeaeb691e13e63ed4db2301fcfef to your computer and use it in GitHub Desktop.
This file contains hidden or 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 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