`;
// Position picker near trigger button
document.body.appendChild(picker);
positionPicker(picker, triggerButton);
// Setup event handlers
const searchInput = picker.querySelector('.emoji-search');
const grid = picker.querySelector('.emoji-grid');
const categoryTabs = picker.querySelectorAll('.emoji-category-tab');
const skinToneBtns = picker.querySelectorAll('.skin-tone-btn');
// Skin tone selection
skinToneBtns.forEach(btn => {
btn.onclick = () => {
currentSkinTone = btn.dataset.skin;
localStorage.setItem('demojifyCurrentSkinTone', currentSkinTone);
// Update active state
skinToneBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Re-render emojis with new skin tone
const query = searchInput.value.trim();
if (query) {
renderEmojis(grid, searchEmojis(query), targetInput, picker);
} else {
renderEmojis(grid, getEmojisByCategory(activeCategory), targetInput, picker);
}
};
});
// Search functionality
searchInput.oninput = () => {
const query = searchInput.value.trim();
if (query) {
renderEmojis(grid, searchEmojis(query), targetInput, picker);
} else {
renderEmojis(grid, getEmojisByCategory(activeCategory), targetInput, picker);
}
};
// Category tabs and clear button
categoryTabs.forEach(tab => {
tab.onclick = (e) => {
// Handle clear button click for Recent tab
if (e.target.classList.contains('clear-recents-btn')) {
e.stopPropagation();
// Clear recents data
recents = [];
localStorage.setItem('demojifyEmojiRecent', '[]');
// Re-render the grid
renderEmojis(grid, getEmojisByCategory(activeCategory), targetInput, picker);
return;
}
activeCategory = tab.dataset.category;
categoryTabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
searchInput.value = '';
renderEmojis(grid, getEmojisByCategory(activeCategory), targetInput, picker);
};
});
// Initial render
renderEmojis(grid, getEmojisByCategory(activeCategory), targetInput, picker);
// Click outside to close - set up persistent event listener
const handleOutsideClick = (e) => {
if (!picker.contains(e.target) && e.target !== triggerButton) {
closePicker();
}
};
// Store the handler for cleanup
picker._outsideClickHandler = handleOutsideClick;
setTimeout(() => {
document.addEventListener('click', handleOutsideClick);
}, 100);
currentOpenPicker = picker;
return picker;
}
function renderEmojis(grid, emojis, targetInput, picker) {
grid.innerHTML = '';
emojis.forEach(emoji => {
const button = document.createElement('button');
button.className = 'emoji-item';
// Always display the neutral/base emoji in the grid, regardless of global skin tone
button.textContent = emoji.char;
// Removed title to avoid annoying hover text
// Add variation indicator if emoji has skin tones
if (emoji.hasVariations) {
button.classList.add('has-variations');
setupSkinTonePopup(button, emoji, targetInput, picker);
}
button.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
// Apply global skin tone if available for this emoji (only when adding to input)
const finalEmoji = getEmojiWithSkinTone(emoji);
targetInput.value += finalEmoji;
targetInput.dispatchEvent(new Event('input')); // Trigger any input handlers
recordUsage(finalEmoji);
saveRecent(finalEmoji);
// Hide any variation popups immediately for normal clicks
if (button._cleanupSkinTonePopup) {
button._cleanupSkinTonePopup();
}
// Don't close picker - allow multiple selections
};
grid.appendChild(button);
});
}
function positionPicker(picker, triggerButton) {
const triggerRect = triggerButton.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Default position: below and to the right of trigger
let left = triggerRect.right + 5;
let top = triggerRect.top;
// Adjust if picker would go off-screen
const pickerWidth = 320;
const pickerHeight = 400;
if (left + pickerWidth > viewportWidth - 10) {
left = triggerRect.left - pickerWidth - 5; // Show to the left instead
}
if (top + pickerHeight > viewportHeight - 10) {
top = viewportHeight - pickerHeight - 10; // Move up
}
if (left < 10) left = 10;
if (top < 10) top = 10;
picker.style.left = left + 'px';
picker.style.top = top + 'px';
}
function getCategoryIcon(category) {
const icons = {
'Recent': '๐',
'Smileys & Emotion': '๐',
'People & Body': '๐',
'Animals & Nature': '๐ถ',
'Food & Drink': '๐',
'Travel & Places': '๐',
'Activities': 'โฝ',
'Objects': '๐ก',
'Symbols': 'โค๏ธ',
'Flags': '๐ณ๏ธ'
};
return icons[category]; // No fallback - only show categories we have icons for
}
function getEmojiWithSkinTone(emoji) {
// Only apply global skin tone if it's NOT neutral and emoji has variations
if (!emoji.hasVariations || currentSkinTone === SkinTones.NEUTRAL) {
return emoji.char;
}
// Get all variations for this emoji using the same logic as the popup
const variations = getSkinToneVariations(emoji);
// Find the variation that matches the current skin tone
// The variations array is [neutral, light, medium-light, medium, medium-dark, dark]
// We need to map the currentSkinTone to the correct index
let targetIndex = 0; // Default to neutral
switch (currentSkinTone) {
case SkinTones.LIGHT:
targetIndex = 1;
break;
case SkinTones.MEDIUM_LIGHT:
targetIndex = 2;
break;
case SkinTones.MEDIUM:
targetIndex = 3;
break;
case SkinTones.MEDIUM_DARK:
targetIndex = 4;
break;
case SkinTones.DARK:
targetIndex = 5;
break;
default:
targetIndex = 0; // neutral
}
// Return the variation at the target index, or fallback to neutral
if (variations.length > targetIndex) {
return variations[targetIndex];
}
// Fallback to neutral if skin tone not found
return emoji.char;
}
function getSkinToneVariations(emoji) {
// For Recent tab items that might be skin-toned variants, find the original emoji
let baseEmoji = emoji;
if (!emoji.hasVariations && emoji.char) {
// This might be a skin-toned variant from recents - find the base emoji
const foundBase = allEmojis.find(e =>
e.hasVariations && getSkinToneVariationsForBase(e).includes(emoji.char)
);
if (foundBase) {
baseEmoji = foundBase;
}
}
const variations = [];
// Always start with the neutral version (yellow hand)
if (baseEmoji.unified) {
const neutralCps = baseEmoji.unified.split('-').map(u => parseInt(u, 16));
const neutralChar = String.fromCodePoint(...neutralCps);
variations.push(neutralChar);
} else {
// Fallback for items without unified data
variations.push(baseEmoji.char);
}
// Add skin tone variations
if (baseEmoji.hasVariations && baseEmoji.variations) {
baseEmoji.variations.forEach(variation => {
const cps = variation.split('-').map(u => parseInt(u, 16));
const variantChar = String.fromCodePoint(...cps);
// Don't duplicate the neutral version
if (!variations.includes(variantChar)) {
variations.push(variantChar);
}
});
}
return variations;
}
// Helper function for finding variations of a base emoji
function getSkinToneVariationsForBase(baseEmoji) {
const variations = [];
// Add neutral version
if (baseEmoji.unified) {
const neutralCps = baseEmoji.unified.split('-').map(u => parseInt(u, 16));
variations.push(String.fromCodePoint(...neutralCps));
} else {
variations.push(baseEmoji.char);
}
// Add skin tone variations
if (baseEmoji.hasVariations && baseEmoji.variations) {
baseEmoji.variations.forEach(variation => {
const cps = variation.split('-').map(u => parseInt(u, 16));
variations.push(String.fromCodePoint(...cps));
});
}
return variations;
}
function setupSkinTonePopup(button, emoji, targetInput, picker) {
let holdTimer = null;
let hoverTimer = null;
let variationPopup = null;
let isHolding = false;
let isDragging = false;
let mouseDown = false;
// Start hold timer on mouse/touch down
function startHold(e) {
e.preventDefault();
e.stopPropagation();
mouseDown = true;
isHolding = false;
isDragging = false;
holdTimer = setTimeout(() => {
if (mouseDown) { // Only show if still holding down
isHolding = true;
showVariationPopup();
}
}, 500); // 500ms hold time
}
// Handle mouse/touch up - either quick click or complete hold
function endHold(e) {
e.preventDefault();
e.stopPropagation();
mouseDown = false;
if (holdTimer) {
clearTimeout(holdTimer);
holdTimer = null;
}
// If we were holding and have a popup, check if we're over a variation button
if (isHolding && variationPopup) {
const elementUnder = document.elementFromPoint(e.clientX, e.clientY);
const varButton = elementUnder?.closest('.skin-tone-variation-popup button');
if (varButton) {
// Trigger the click on the variation button we're over
varButton.click();
hideVariationPopup();
isHolding = false;
isDragging = false;
return; // Don't proceed with normal click
}
// If holding but not over a variation, hide popup and don't trigger normal click
hideVariationPopup();
isHolding = false;
isDragging = false;
return;
}
// For quick clicks (not holding), hide any popup and let normal click proceed
if (!isHolding) {
hideVariationPopup();
// Normal click will be handled by the button's onclick handler
}
isHolding = false;
isDragging = false;
}
// Start hover timer for hover behavior
function startHover(e) {
if (isHolding || mouseDown) return; // Don't show on hover if already holding or mouse down
hoverTimer = setTimeout(() => {
if (!mouseDown && !isHolding) { // Only show if not in a click/hold sequence
showVariationPopup();
}
}, 300); // 300ms hover delay
}
// Cancel hold on mouse/touch leave
function cancelHold() {
mouseDown = false;
if (holdTimer) {
clearTimeout(holdTimer);
holdTimer = null;
}
if (hoverTimer) {
clearTimeout(hoverTimer);
hoverTimer = null;
}
// Don't immediately hide popup if we're in a hold state
if (!isHolding) {
// Hide popup after a short delay when leaving the button (unless over popup)
setTimeout(() => {
if (variationPopup && !variationPopup.matches(':hover') && !mouseDown) {
hideVariationPopup();
}
}, 200);
}
}
// Handle mouse leave from variation popup
function handlePopupMouseLeave() {
setTimeout(() => {
if (variationPopup && !variationPopup.matches(':hover') && !button.matches(':hover')) {
hideVariationPopup();
}
}, 200);
}
// Show variation popup
function showVariationPopup() {
hideVariationPopup(); // Remove any existing popup
const variations = getSkinToneVariations(emoji);
if (variations.length <= 1) return; // No variations to show
// Create variation popup
variationPopup = document.createElement('div');
variationPopup.className = 'skin-tone-variation-popup';
variationPopup.style.cssText = `
position: fixed;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 2147483650;
display: flex;
gap: 4px;
opacity: 0;
transform: scale(0.9);
transition: all 0.15s ease-out;
`;
// Add variation buttons
variations.forEach((variation, index) => {
const varButton = document.createElement('button');
varButton.style.cssText = `
width: 32px;
height: 32px;
border: 1px solid #ddd;
background: white;
border-radius: 6px;
font-size: 20px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
`;
varButton.textContent = variation;
varButton.title = `${emoji.name} - skin tone ${index}`;
varButton.onmouseenter = () => {
varButton.style.borderColor = '#e91e63';
varButton.style.transform = 'scale(1.1)';
};
varButton.onmouseleave = () => {
varButton.style.borderColor = '#ddd';
varButton.style.transform = 'scale(1)';
};
varButton.onclick = (e) => {
e.stopPropagation();
e.preventDefault();
targetInput.value += variation;
targetInput.dispatchEvent(new Event('input'));
recordUsage(variation);
saveRecent(variation);
// Update current skin tone based on selection
if (index > 0 && emoji.variations[index - 1]) {
const skinToneFromVariation = emoji.variations[index - 1].split('-').find(part =>
Object.values(SkinTones).includes(part)
);
if (skinToneFromVariation) {
setSkinTone(skinToneFromVariation);
}
} else if (index === 0) {
setSkinTone(SkinTones.NEUTRAL);
}
// Immediately hide the variation popup with forced cleanup
setTimeout(() => hideVariationPopup(), 0);
};
variationPopup.appendChild(varButton);
});
// Position popup
document.body.appendChild(variationPopup);
positionVariationPopup();
// Show with animation
requestAnimationFrame(() => {
variationPopup.style.opacity = '1';
variationPopup.style.transform = 'scale(1)';
});
// Add mouse leave handler to popup
variationPopup.addEventListener('mouseleave', handlePopupMouseLeave);
// Click outside to close
document.addEventListener('click', handleOutsideClick, true);
}
// Position variation popup relative to the emoji button
function positionVariationPopup() {
if (!variationPopup) return;
// Check if button still exists and is attached to DOM
if (!button.isConnected) {
hideVariationPopup();
return;
}
const buttonRect = button.getBoundingClientRect();
// If button has no dimensions (not visible), hide popup
if (buttonRect.width === 0 || buttonRect.height === 0) {
hideVariationPopup();
return;
}
// Force a reflow to get accurate popup dimensions
variationPopup.style.visibility = 'hidden';
variationPopup.style.display = 'flex';
const popupRect = variationPopup.getBoundingClientRect();
variationPopup.style.visibility = '';
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Default: center above the button
let left = buttonRect.left + (buttonRect.width / 2) - (popupRect.width / 2);
let top = buttonRect.top - popupRect.height - 10;
// Adjust if it would go off-screen horizontally
if (left < 10) {
left = 10;
} else if (left + popupRect.width > viewportWidth - 10) {
left = viewportWidth - popupRect.width - 10;
}
// If it would go off-screen vertically, show below instead
if (top < 10) {
top = buttonRect.bottom + 10;
}
// Ensure we have valid coordinates
if (isNaN(left) || isNaN(top) || left < 0 || top < 0) {
console.warn('[HiddenMsg] Invalid popup coordinates, hiding popup');
hideVariationPopup();
return;
}
variationPopup.style.left = left + 'px';
variationPopup.style.top = top + 'px';
}
// Hide variation popup
function hideVariationPopup() {
if (variationPopup) {
variationPopup.removeEventListener('mouseleave', handlePopupMouseLeave);
variationPopup.remove();
variationPopup = null;
document.removeEventListener('click', handleOutsideClick, true);
}
}
// Handle clicks outside the variation popup
function handleOutsideClick(e) {
if (!variationPopup?.contains(e.target) && !button.contains(e.target)) {
hideVariationPopup();
}
}
// Set up event listeners for hover/hold behavior
button.addEventListener('mousedown', startHold);
button.addEventListener('mouseup', endHold);
button.addEventListener('mouseenter', startHover);
button.addEventListener('mouseleave', cancelHold);
button.addEventListener('touchstart', startHold, { passive: false });
button.addEventListener('touchend', endHold, { passive: false });
button.addEventListener('touchcancel', cancelHold);
// Store cleanup function
button._cleanupSkinTonePopup = () => {
if (holdTimer) {
clearTimeout(holdTimer);
}
hideVariationPopup();
button.removeEventListener('mousedown', startHold);
button.removeEventListener('mouseup', endHold);
button.removeEventListener('mouseleave', cancelHold);
button.removeEventListener('touchstart', startHold);
button.removeEventListener('touchend', endHold);
button.removeEventListener('touchcancel', cancelHold);
};
}
function setSkinTone(skinTone) {
currentSkinTone = skinTone;
localStorage.setItem('demojifyCurrentSkinTone', currentSkinTone);
}
function closePicker() {
if (currentOpenPicker) {
// Clean up the outside click event listener
if (currentOpenPicker._outsideClickHandler) {
document.removeEventListener('click', currentOpenPicker._outsideClickHandler);
}
currentOpenPicker.remove();
currentOpenPicker = null;
}
}
// Initialize emoji data
loadData();
return {
createPicker,
closePicker,
isOpen: () => !!currentOpenPicker,
getCategories,
getEmojisByCategory,
searchEmojis,
recordUsage,
saveRecent,
getEmojiWithSkinTone,
getSkinToneVariations,
setSkinTone,
SkinTones
};
})();
// Enhanced encoding that preserves custom carrier format (safe for userscripts)
const EMOJIS = ['๐','๐','๐ฅณ','๐','๐ค','๐','๐','๐ฅ','๐ฏ','โญ','๐','๐','๐','๐ช','๐','โค๏ธ','๐','๐','๐ฆ','๐','๐','๐บ','๐ธ','๐ป','๐ท','๐','๐','โ๏ธ','โก','โจ','๐ฏ','๐','๐ช','๐ญ','๐จ','๐ต','๐ถ','๐ธ','๐ค','๐ฑ','๐ป','โ','๐ฎ','๐ท','๐ฎ','๐ก','๐','โฝ','๐','๐พ','โพ'];
const ALPHABET = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'];
// Enhanced encoding that preserves custom carrier format (safe for userscripts)
const encodeToCarriers = (message, carriersText) => {
console.log('[HiddenMsg] encodeToCarriers called with:', { message, carriersText });
if (!carriersText.length) {
console.log('[HiddenMsg] encodeToCarriers: No carriers text provided');
return null;
}
try {
// Convert string to byte array manually to avoid TypedArray issues
const bytes = [];
for (let i = 0; i < message.length; i++) {
const code = message.charCodeAt(i);
if (code < 128) {
bytes.push(code);
} else if (code < 2048) {
bytes.push(192 | (code >> 6));
bytes.push(128 | (code & 63));
} else if (code < 65516) {
bytes.push(224 | (code >> 12));
bytes.push(128 | ((code >> 6) & 63));
bytes.push(128 | (code & 63));
} else {
bytes.push(240 | (code >> 18));
bytes.push(128 | ((code >> 12) & 63));
bytes.push(128 | ((code >> 6) & 63));
bytes.push(128 | (code & 63));
}
}
// Find positions where we can encode (avoid double spacing)
const chars = [...carriersText];
const encodePositions = [];
for (let i = 0; i < chars.length; i++) {
const char = chars[i];
const prevChar = i > 0 ? chars[i - 1] : null;
const nextChar = i + 1 < chars.length ? chars[i + 1] : null;
// Encode into characters that:
// 1. Are not spaces
// 2. Are NOT preceded by a space (avoids visual double spacing)
// 3. Prefer characters at the end of words or standalone characters
if (char !== ' ' && prevChar !== ' ') {
encodePositions.push(i);
}
}
if (encodePositions.length === 0) {
// Fallback: use all non-space characters
for (let i = 0; i < chars.length; i++) {
if (chars[i] !== ' ') {
encodePositions.push(i);
}
}
}
console.log('[HiddenMsg] Encode positions:', encodePositions);
// Distribute bytes across encode positions
const bytesPerPosition = Math.ceil(bytes.length / encodePositions.length);
let result = '';
let byteIndex = 0;
for (let i = 0; i < chars.length; i++) {
result += chars[i];
// If this is an encode position and we have bytes left
if (encodePositions.includes(i) && byteIndex < bytes.length) {
// Add variation selectors for this position's portion of bytes
for (let j = 0; j < bytesPerPosition && byteIndex < bytes.length; j++) {
result += toVS(bytes[byteIndex++]);
}
}
}
console.log('[HiddenMsg] encodeToCarriers result:', {
originalLength: message.length,
bytesLength: bytes.length,
carriersTextLength: carriersText.length,
encodePositionsLength: encodePositions.length,
outputLength: result.length,
result: result
});
return result;
} catch (error) {
console.error('[HiddenMsg] encodeToCarriers error:', error);
return null;
}
};
// Safe multi-carrier decoding for userscripts (avoids TypedArray/Xrays issues)
const decodeFromCarriers = (text) => {
console.log('[HiddenMsg] decodeFromCarriers called with text length:', text.length);
try {
const parts = text.split(/\s+/);
const bytes = [];
for (const part of parts) {
// Extract variation selectors from each carrier part
for (const cp of [...part].map((c) => c.codePointAt(0))) {
const b = fromVS(cp);
if (b !== null) {
bytes.push(b);
}
}
}
if (bytes.length === 0) {
console.log('[HiddenMsg] decodeFromCarriers: No variation selector bytes found');
return null;
}
// Use the same manual UTF-8 decoding as decodeMessage
let result = '';
let i = 0;
while (i < bytes.length) {
let byte1 = bytes[i++];
if (byte1 < 128) {
// Single byte character (ASCII)
result += String.fromCharCode(byte1);
} else if ((byte1 & 0xE0) === 0xC0) {
// Two byte character
if (i >= bytes.length) break;
let byte2 = bytes[i++];
result += String.fromCharCode(((byte1 & 0x1F) << 6) | (byte2 & 0x3F));
} else if ((byte1 & 0xF0) === 0xE0) {
// Three byte character
if (i + 1 >= bytes.length) break;
let byte2 = bytes[i++];
let byte3 = bytes[i++];
result += String.fromCharCode(((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F));
} else if ((byte1 & 0xF8) === 0xF0) {
// Four byte character (convert to surrogate pair)
if (i + 2 >= bytes.length) break;
let byte2 = bytes[i++];
let byte3 = bytes[i++];
let byte4 = bytes[i++];
let codePoint = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
if (codePoint > 0xFFFF) {
codePoint -= 0x10000;
result += String.fromCharCode(0xD800 + (codePoint >> 10));
result += String.fromCharCode(0xDC00 + (codePoint & 0x3FF));
} else {
result += String.fromCharCode(codePoint);
}
} else {
// Invalid byte, skip
continue;
}
}
console.log('[HiddenMsg] decodeFromCarriers result:', {
partsLength: parts.length,
bytesLength: bytes.length,
resultLength: result.length,
result: result
});
return result || null;
} catch (error) {
console.error('[HiddenMsg] decodeFromCarriers error:', error);
return null;
}
};
/* ---------- 3. Styles ---------- */
GM_addStyle(`
.hidden-msg-floating-btn{position:fixed;bottom:20px;right:20px;width:56px;height:56px;background:#e91e63;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:28px;color:#fff;box-shadow:0 4px 12px rgba(233,30,99,.45);transition:.3s;z-index:2147483647}
.hidden-msg-floating-btn:hover{transform:scale(1.1) rotate(5deg)}
.hidden-msg-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:2147483648}
.hidden-msg-container{background:white;border-radius:16px;padding:24px;width:90%;max-width:500px;max-height:90vh;overflow-y:auto;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#333}
.hidden-msg-header{text-align:center;margin-bottom:24px}
.hidden-msg-title{font-size:24px;font-weight:bold;margin:0 0 16px 0;color:#333}
.hidden-msg-description{color:#666;margin-bottom:20px;line-height:1.5}
.hidden-msg-toggle{position:relative;display:inline-flex;background:#f0f0f0;border-radius:25px;padding:4px;margin-bottom:24px}
.hidden-msg-toggle-option{padding:8px 20px;border-radius:20px;cursor:pointer;transition:all 0.3s;font-weight:500;color:#666}
.hidden-msg-toggle-option.active{background:#333;color:white}
.hidden-msg-input{width:100%;padding:12px;border:1px solid #ddd;border-radius:8px;font-size:14px;margin-bottom:16px;resize:vertical;min-height:80px;background:white;color:#333}
.hidden-msg-input:focus{outline:none;border-color:#e91e63}
.hidden-msg-paste-btn{display:inline-flex;align-items:center;gap:6px;background:#e91e63;color:white;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:14px;margin-bottom:16px}
.hidden-msg-paste-btn:hover{background:#d81b60}
.hidden-msg-paste-btn:disabled{background:#ccc;cursor:not-allowed}
.hidden-msg-section-title{font-weight:600;margin:20px 0 12px 0;color:#333}
.hidden-msg-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(40px,1fr));gap:8px;margin-bottom:20px}
.hidden-msg-grid-item{width:40px;height:40px;border:1px solid #ddd;border-radius:8px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all 0.2s;font-size:18px;background:white}
.hidden-msg-grid-item:hover{border-color:#e91e63;background:#f8f8f8}
.hidden-msg-grid-item.selected{border-color:#e91e63;background:#e91e63;color:white}
.hidden-msg-output-container{margin:16px 0}
.hidden-msg-output{background:#f8f9fa;border:1px solid #dee2e6;border-radius:8px;padding:12px;margin-bottom:8px;min-height:60px;font-family:monospace;white-space:pre-wrap;word-break:break-all}
.hidden-msg-output:empty:before{content:"Your encoded message will appear here...";color:#999;font-style:italic}
.hidden-msg-copy-btn{display:inline-flex;align-items:center;gap:6px;background:#28a745;color:white;border:none;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;transition:all 0.2s}
.hidden-msg-copy-btn:hover{background:#218838;transform:translateY(-1px)}
.hidden-msg-copy-btn:active{transform:translateY(0)}
.hidden-msg-footer{text-align:center;margin-top:20px;padding-top:16px;border-top:1px solid #eee}
.hidden-msg-link{color:#e91e63;text-decoration:none;font-size:14px}
.hidden-msg-link:hover{text-decoration:underline}
.hidden-msg-close{position:absolute;top:16px;right:16px;background:none;border:none;font-size:24px;cursor:pointer;color:#999;padding:4px}
.hidden-msg-close:hover{color:#333}
.hidden-msg-decode-result{background:#e8f5e8;border:1px solid #c3e6c3;border-radius:8px;padding:16px;margin:16px 0;color:#2d5a2d;word-wrap:break-word;word-break:break-word;white-space:pre-wrap;overflow-wrap:break-word}
.hidden-msg-decode-result.no-message{background:#fff3cd;border-color:#ffeaa7;color:#8d6e63;word-wrap:break-word;word-break:break-word;white-space:pre-wrap;overflow-wrap:break-word}
.hidden-msg-highlight{position:relative;background:rgba(255,235,59,0.3);border:2px dashed #ffc107;border-radius:4px;cursor:pointer;display:inline;padding:2px 4px;margin:0 2px}
/* Completely disable CSS tooltip - we'll use JavaScript only */
.hidden-msg-highlight::after{display:none !important}
.hidden-msg-hover-tooltip{position:fixed;background:#333;color:white;padding:12px 16px;border-radius:8px;font-size:14px;max-width:350px;word-wrap:break-word;z-index:2147483650;pointer-events:auto;opacity:0;transition:opacity 0.2s ease;box-shadow:0 8px 24px rgba(0,0,0,0.4);border:1px solid #555}
.hidden-msg-hover-tooltip.show{opacity:1}
.hidden-msg-hover-tooltip button{pointer-events:auto}
.hidden-msg-encode-tabs{margin-top:16px}
.hidden-msg-tab-nav{display:flex;background:#f0f0f0;border-radius:8px;padding:4px;margin-bottom:20px}
.hidden-msg-tab-option{flex:1;padding:8px 16px;text-align:center;border-radius:6px;cursor:pointer;transition:all 0.3s;font-weight:500;color:#666}
.hidden-msg-tab-option.active{background:#e91e63;color:white}
.hidden-msg-tab-option:hover:not(.active){background:#e8e8e8}
.hidden-msg-tab-content{display:block}
.hidden-msg-carrier-display{min-height:50px;max-height:120px;overflow-y:auto;background:#f8f9fa;border:1px solid #dee2e6;border-radius:8px;padding:12px;margin-bottom:16px;display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}
.hidden-msg-carrier-display:empty:before{content:"Selected carriers will appear here...";color:#999;font-style:italic}
.hidden-msg-carrier-item{background:#e91e63;color:white;padding:4px 8px;border-radius:4px;font-size:16px;display:flex;align-items:center;gap:4px}
.hidden-msg-carrier-remove{background:rgba(255,255,255,0.3);border:none;color:white;width:16px;height:16px;border-radius:50%;font-size:12px;cursor:pointer;display:flex;align-items:center;justify-content:center}
.hidden-msg-carrier-remove:hover{background:rgba(255,255,255,0.5)}
.hidden-msg-emoji-selector{max-height:200px;overflow-y:auto;background:#f8f9fa;border:1px solid #dee2e6;border-radius:8px;padding:16px;margin-bottom:16px}
.hidden-msg-emoji-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(40px,1fr));gap:8px}
.hidden-msg-emoji-item{width:40px;height:40px;border:1px solid #ddd;border-radius:8px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all 0.2s;font-size:18px;background:white}
.hidden-msg-emoji-item:hover{border-color:#e91e63;background:#f8f8f8;transform:scale(1.05)}
/* Skin tone variation support */
.hidden-msg-emoji-has-variations{position:relative}
.hidden-msg-emoji-has-variations::after{content:'';position:absolute;bottom:1px;right:0px;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:4px solid #666;transform:rotate(135deg);z-index:1}
.hidden-msg-emoji-has-variations:hover::after{border-bottom-color:#e91e63}
.hidden-msg-variation-picker{position:absolute;padding:5px;box-shadow:0px 2px 8px rgba(0,0,0,0.3);border-radius:8px;display:flex;align-items:center;justify-content:space-around;opacity:0;visibility:hidden;pointer-events:none;background:white;border:1px solid #ddd;z-index:2147483649;transform:scale(0.9);transition:all 0.15s ease-out;min-width:240px;height:50px}
.hidden-msg-variation-picker.visible{opacity:1;visibility:visible;pointer-events:all;transform:scale(1)}
.hidden-msg-variation-picker .hidden-msg-emoji-item{margin:0 2px;border:1px solid #ddd}
.hidden-msg-variation-picker .hidden-msg-emoji-item:hover{border-color:#e91e63;transform:scale(1.1)}
.hidden-msg-variation-pointer{position:absolute;width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid white;bottom:-8px;left:50%;transform:translateX(-50%);z-index:2147483650}
.hidden-msg-variation-pointer::before{content:'';position:absolute;width:0;height:0;border-left:9px solid transparent;border-right:9px solid transparent;border-top:9px solid #ddd;bottom:1px;left:-9px}
/* Floating Emoji Picker (Chat-App Style) */
.emoji-trigger-button{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:24px;height:24px;background:#f0f0f0;border:1px solid #ddd;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:16px;color:#666;transition:all 0.2s ease;z-index:10}
.emoji-trigger-button:hover{background:#e91e63;color:white;transform:translateY(-50%) scale(1.1)}
.floating-emoji-picker{position:fixed;width:420px;height:450px;background:white;border:1px solid #ddd;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,0.15);z-index:2147483649;display:flex;flex-direction:column;overflow:hidden}
.emoji-picker-header{padding:12px;border-bottom:1px solid #eee}
.emoji-search{width:100%;padding:8px 12px;border:1px solid #ddd;border-radius:20px;font-size:14px;outline:none;margin-bottom:8px}
.emoji-search:focus{border-color:#e91e63}
.emoji-skin-tones{display:flex;gap:4px;justify-content:center}
.skin-tone-btn{background:white;border:1px solid #ddd;border-radius:4px;padding:4px;cursor:pointer;font-size:16px;transition:all 0.2s}
.skin-tone-btn:hover{border-color:#e91e63;background:#f5f5f5}
.skin-tone-btn.active{border-color:#e91e63;background:#e91e63;color:white}
.emoji-categories{display:flex;padding:8px 12px;gap:4px;border-bottom:1px solid #eee;flex-wrap:wrap;justify-content:flex-start}
.emoji-category-tab{padding:6px 8px;border:1px solid #ddd;background:white;border-radius:6px;font-size:16px;cursor:pointer;transition:all 0.2s;white-space:nowrap;min-width:32px;text-align:center;flex-shrink:0;position:relative}
.emoji-category-tab:hover{background:#f5f5f5}
.emoji-category-tab.active{background:#e91e63;color:white;border-color:#e91e63}
.clear-recents-btn{position:absolute;top:-4px;right:-4px;background:#ff4757;color:white;border-radius:50%;width:16px;height:16px;font-size:10px;display:flex;align-items:center;justify-content:center;cursor:pointer;border:1px solid white;transition:all 0.2s}
.clear-recents-btn:hover{background:#ff3838;transform:scale(1.1)}
.emoji-grid{flex:1;padding:12px;overflow-y:auto;display:grid;grid-template-columns:repeat(8,1fr);gap:4px}
.emoji-item{width:32px;height:32px;border:none;background:none;font-size:20px;cursor:pointer;border-radius:6px;transition:all 0.2s;display:flex;align-items:center;justify-content:center}
.emoji-item:hover{background:#f0f0f0;transform:scale(1.2)}
.emoji-item.has-variations{position:relative}
.emoji-item.has-variations::after{content:'';position:absolute;bottom:1px;right:1px;width:0;height:0;border-left:3px solid transparent;border-right:3px solid transparent;border-bottom:3px solid #666;transform:rotate(135deg)}
/* Input field positioning for emoji buttons */
.hidden-msg-input-wrapper{position:relative;display:inline-block;width:100%}
.hidden-msg-input-wrapper .hidden-msg-input{padding-right:40px}
/* Visual screener status indicator */
.screener-status-indicator{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:rgba(255,193,7,0.9);color:#333;padding:8px 16px;border-radius:20px;font-size:14px;font-weight:500;z-index:2147483647;opacity:0;transition:all 0.3s ease;pointer-events:none;box-shadow:0 2px 8px rgba(0,0,0,0.2)}
.screener-status-indicator.show{opacity:1}
`);
/* ---------- 4. Enhanced Modal System ---------- */
function createModal(selectedText = '', preferredMode = 'decode') {
// Remove any existing modals
document.querySelector('.hidden-msg-modal')?.remove();
const modal = document.createElement('div');
modal.className = 'hidden-msg-modal';
modal.innerHTML = `
Hide a message in an emoji
This tool allows you to encode a hidden message into an emoji or alphabet letter. You can copy and paste text with a hidden message in it to decode the message.
Decode
Encode
1-Click
Free Form
Pick an emoji
Or pick a standard alphabet letter
Custom Carriers
`;
document.body.appendChild(modal);
setupModalEvents(modal, selectedText, preferredMode);
return modal;
}
function setupModalEvents(modal, selectedText, preferredMode = 'decode') {
const closeBtn = modal.querySelector('.hidden-msg-close');
const toggleOptions = modal.querySelectorAll('.hidden-msg-toggle-option');
const encodeMode = modal.querySelector('.hidden-msg-encode-mode');
const decodeMode = modal.querySelector('.hidden-msg-decode-mode');
const encodeInput = modal.querySelector('#encode-input');
const decodeInput = modal.querySelector('#decode-input');
const outputArea1Click = modal.querySelector('#output-area-1click');
const outputAreaFreeForm = modal.querySelector('#output-area-freeform');
const decodeResult = modal.querySelector('#decode-result');
const pasteBtn = modal.querySelector('#paste-btn');
const customCarriers = modal.querySelector('#custom-carriers');
const tabOptions = modal.querySelectorAll('.hidden-msg-tab-option');
const tab1Click = modal.querySelector('#tab-1-click');
const tabFreeForm = modal.querySelector('#tab-free-form');
let currentMode = preferredMode;
let currentTab = '1-click';
let selectedCarriers1Click = [];
// Set initial mode display based on preferred mode
if (preferredMode === 'encode') {
encodeMode.style.display = 'block';
decodeMode.style.display = 'none';
toggleOptions.forEach(opt => {
opt.classList.toggle('active', opt.dataset.mode === 'encode');
});
} else {
encodeMode.style.display = 'none';
decodeMode.style.display = 'block';
toggleOptions.forEach(opt => {
opt.classList.toggle('active', opt.dataset.mode === 'decode');
});
}
// Use the global tryDecodeText function (defined below)
// Pre-fill with selected text - RESPECT preferredMode
if (selectedText) {
if (preferredMode === 'decode') {
// User explicitly wants decode mode
decodeInput.value = selectedText;
const decoded = tryDecodeText(selectedText);
if (decoded) {
showDecodeResult(decoded);
}
} else {
// User explicitly wants encode mode
encodeInput.value = selectedText;
}
}
// Close modal
closeBtn.onclick = () => modal.remove();
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
// Mode toggle
toggleOptions.forEach(option => {
option.onclick = () => {
currentMode = option.dataset.mode;
toggleOptions.forEach(opt => {
opt.classList.toggle('active', opt === option);
});
if (currentMode === 'encode') {
encodeMode.style.display = 'block';
decodeMode.style.display = 'none';
} else {
encodeMode.style.display = 'none';
decodeMode.style.display = 'block';
}
};
});
// Setup tab switching
tabOptions.forEach(option => {
option.onclick = () => {
currentTab = option.dataset.tab;
tabOptions.forEach(opt => {
opt.classList.toggle('active', opt === option);
});
if (currentTab === '1-click') {
tab1Click.style.display = 'block';
tabFreeForm.style.display = 'none';
} else {
tab1Click.style.display = 'none';
tabFreeForm.style.display = 'block';
}
};
});
// Setup grids for 1-click tab
setupEmojiGrid(modal);
setupAlphabetGrid(modal);
// Free Form tab now only uses the floating emoji picker trigger button
// Setup encoding with enhanced carrier logic for both tabs
function getAllCarriers1Click() {
return [...selectedCarriers1Click];
}
function getAllCarriersFreeForm() {
// Return the entire custom carriers text as-is to preserve formatting
return customCarriers.value.trim();
}
function update1ClickOutput() {
const message = encodeInput.value;
const carriers = getAllCarriers1Click();
const copyBtn1Click = modal.querySelector('#copy-btn-1click');
if (!message || carriers.length === 0) {
outputArea1Click.textContent = '';
copyBtn1Click.style.display = 'none';
return;
}
// Use only the first selected carrier like the original demo
const carrier = carriers[0];
const encoded = encodeMessage(carrier, message);
outputArea1Click.textContent = encoded || '';
// Show/hide copy button based on encoded content
if (encoded) {
copyBtn1Click.style.display = 'inline-flex';
copyBtn1Click.onclick = () => {
navigator.clipboard.writeText(encoded).then(() => {
// Visual feedback
const origText = copyBtn1Click.innerHTML;
copyBtn1Click.innerHTML = 'โ Copied!';
copyBtn1Click.style.background = '#218838';
setTimeout(() => {
copyBtn1Click.innerHTML = origText;
copyBtn1Click.style.background = '#28a745';
}, 1000);
}).catch(() => {
copyBtn1Click.innerHTML = 'โ Failed';
setTimeout(() => copyBtn1Click.innerHTML = '๐ Copy to Clipboard', 1000);
});
};
} else {
copyBtn1Click.style.display = 'none';
}
}
// Real-time encoding for 1-click tab
encodeInput.oninput = () => {
if (currentTab === '1-click') {
update1ClickOutput();
}
};
// Encode button functionality for Free Form mode
const encodeBtn = modal.querySelector('#encode-btn');
encodeBtn.onclick = () => {
const message = encodeInput.value.trim();
if (!message) {
alert('Please enter text to encode');
return;
}
// Get carriers text (preserves format like "testcle :)")
const carriersText = getAllCarriersFreeForm();
if (!carriersText) {
alert('Please enter custom carrier text or select emojis');
return;
}
// Use format-preserving encoding for Free Form
const finalEncoded = encodeToCarriers(message, carriersText);
console.log('[HiddenMsg] Free Form encoding debug:', {
message,
carriersText,
finalEncoded,
encodedLength: finalEncoded ? finalEncoded.length : 0,
carriersTextLength: carriersText.length
});
if (finalEncoded) {
// Clear placeholder and show encoded text
outputAreaFreeForm.innerHTML = '';
outputAreaFreeForm.textContent = finalEncoded;
// Add visual indicator that encoding happened
const indicator = document.createElement('div');
indicator.style.cssText = 'font-size:12px;color:#666;margin-top:8px;font-style:italic;';
indicator.textContent = `โ Encoded ${message.length} character message into ${carriersText.length} carrier(s) (${finalEncoded.length} total chars with variation selectors)`;
outputAreaFreeForm.appendChild(indicator);
// Show and setup the copy button for Free Form
const copyBtnFreeForm = modal.querySelector('#copy-btn-freeform');
copyBtnFreeForm.style.display = 'inline-flex';
copyBtnFreeForm.onclick = () => {
navigator.clipboard.writeText(finalEncoded).then(() => {
// Visual feedback
const origText = copyBtnFreeForm.innerHTML;
copyBtnFreeForm.innerHTML = 'โ Copied!';
copyBtnFreeForm.style.background = '#218838';
setTimeout(() => {
copyBtnFreeForm.innerHTML = origText;
copyBtnFreeForm.style.background = '#28a745';
}, 1000);
}).catch(() => {
copyBtnFreeForm.innerHTML = 'โ Failed';
setTimeout(() => copyBtnFreeForm.innerHTML = '๐ Copy to Clipboard', 1000);
});
};
// Visual feedback for the encode button
const origText = encodeBtn.innerHTML;
encodeBtn.innerHTML = 'โ Encoded!';
setTimeout(() => encodeBtn.innerHTML = origText, 1000);
} else {
alert('Encoding failed - check console for details');
}
};
// Setup decoding with enhanced mapping display and smart link handling
function showDecodeResult(result) {
decodeResult.style.display = 'block';
decodeResult.className = 'hidden-msg-decode-result';
// Clear previous content
decodeResult.innerHTML = '';
// Handle both old string format and new object format for backward compatibility
if (typeof result === 'string') {
// Simple string result - apply link enhancement
const enhancedContent = enhanceContentWithLinks(result, 'unknown', '');
const messageContainer = document.createElement('div');
messageContainer.style.cssText = 'font-weight: bold; margin-bottom: 8px;';
messageContainer.textContent = 'Hidden message: ';
const contentDiv = document.createElement('div');
contentDiv.style.cssText = 'font-weight: normal; display: inline;';
contentDiv.appendChild(enhancedContent);
messageContainer.appendChild(contentDiv);
decodeResult.appendChild(messageContainer);
} else if (result && result.text) {
// Enhanced display with mapping information and smart link handling
const mappingInfo = result.mapping || 'unknown';
const details = result.details ? ` (${result.details})` : '';
// Create message header
const messageHeader = document.createElement('div');
messageHeader.style.cssText = 'font-weight: bold; margin-bottom: 8px;';
messageHeader.textContent = 'Hidden message: ';
// Apply smart link enhancement to the decoded text
const enhancedContent = enhanceContentWithLinks(result.text, mappingInfo, details);
// Style the enhanced content for modal context (different from tooltip)
enhancedContent.style.cssText = 'font-weight: normal; margin-bottom: 8px;';
// Adjust styling for modal context - remove mapping info since we show it separately
const contentText = enhancedContent.querySelector('div');
if (contentText) {
// Remove the mapping info that's added by enhanceContentWithLinks
const mappingElement = contentText.nextElementSibling;
if (mappingElement && mappingElement.textContent.includes('mapping:')) {
mappingElement.remove();
}
}
// Create encoding info footer
const encodingInfo = document.createElement('div');
encodingInfo.style.cssText = 'color: #666; font-size: 12px; margin-top: 8px;';
encodingInfo.textContent = `Encoding: ${mappingInfo}${details}`;
// Append all elements to the result container
decodeResult.appendChild(messageHeader);
decodeResult.appendChild(enhancedContent);
decodeResult.appendChild(encodingInfo);
}
}
function showNoMessage() {
decodeResult.style.display = 'block';
decodeResult.className = 'hidden-msg-decode-result no-message';
decodeResult.textContent = 'No hidden message detected.';
}
// Paste button functionality
pasteBtn.onclick = async () => {
try {
const text = await navigator.clipboard.readText();
decodeInput.value = text;
// Trigger input event to decode automatically
decodeInput.dispatchEvent(new Event('input'));
} catch (err) {
console.warn('Could not paste from clipboard:', err);
pasteBtn.textContent = 'โ Paste failed';
setTimeout(() => pasteBtn.innerHTML = '๐ Paste from Clipboard', 2000);
}
};
decodeInput.oninput = () => {
const text = decodeInput.value;
if (!text) {
decodeResult.style.display = 'none';
return;
}
const decoded = tryDecodeText(text);
if (decoded) {
showDecodeResult(decoded);
} else {
showNoMessage();
}
};
// Grid selection handlers for 1-click tab (single selection like original demo)
function handle1ClickGridSelection(item, value) {
// Clear all previous selections
modal.querySelectorAll('.hidden-msg-grid-item').forEach(gridItem => {
gridItem.classList.remove('selected');
});
selectedCarriers1Click = [];
// Select this item
item.classList.add('selected');
selectedCarriers1Click = [value];
update1ClickOutput();
}
// Beta scanner functionality
const betaScanner = modal.querySelector('#beta-scanner');
let currentTooltip = null;
// Set checkbox state based on cached value
betaScanner.checked = betaScannerEnabled;
betaScanner.onchange = () => {
betaScannerEnabled = betaScanner.checked;
if (betaScannerEnabled) {
startPageScanning();
} else {
stopPageScanning();
}
};
modal.handle1ClickGridSelection = handle1ClickGridSelection;
// Add emoji trigger buttons to input fields (but not decode input)
addEmojiTriggerButtons(modal);
}
/* ---------- 5. Emoji Trigger Button System ---------- */
function addEmojiTriggerButtons(modal) {
// Find all input fields that should have emoji buttons (exclude decode input)
const inputs = [
modal.querySelector('#encode-input'),
modal.querySelector('#custom-carriers')
].filter(input => input); // Remove null entries
inputs.forEach(input => {
// Skip if already wrapped
if (input.parentElement.classList.contains('hidden-msg-input-wrapper')) {
return;
}
// Create wrapper
const wrapper = document.createElement('div');
wrapper.className = 'hidden-msg-input-wrapper';
// Create emoji trigger button
const triggerButton = document.createElement('button');
triggerButton.className = 'emoji-trigger-button';
triggerButton.innerHTML = '๐';
triggerButton.type = 'button'; // Prevent form submission
triggerButton.title = 'Add emoji';
// Insert wrapper before input
input.parentNode.insertBefore(wrapper, input);
// Move input into wrapper and add button
wrapper.appendChild(input);
wrapper.appendChild(triggerButton);
// Add click handler for emoji picker
triggerButton.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
// Close any existing picker first (XOR logic handled in FloatingEmojiPicker)
FloatingEmojiPicker.createPicker(input, triggerButton);
};
});
}
/* ---------- 5. Enhanced Beta Page Scanner with Multi-Encoding Detection ---------- */
let scannerObserver = null;
let highlightedElements = new Set();
let betaScannerEnabled = false; // Cache beta scanner state
// Visual screener status indicator functions
function showScreenerStatusIndicator(isEnabled) {
// Remove existing indicator
const existing = document.querySelector('.screener-status-indicator');
if (existing) existing.remove();
// Create new indicator
const indicator = document.createElement('div');
indicator.className = 'screener-status-indicator';
indicator.textContent = `Screener ${isEnabled ? 'ON' : 'OFF'}`;
document.body.appendChild(indicator);
// Show with animation
setTimeout(() => indicator.classList.add('show'), 100);
// Auto-hide after 2 seconds
setTimeout(() => {
indicator.classList.remove('show');
setTimeout(() => {
if (indicator.parentNode) {
indicator.remove();
}
}, 300); // Wait for fade-out transition
}, 2000);
}
// Confidence checking function
function isLikelyText(str) {
if (!str) return false;
const printable = str.match(/[\p{L}\p{N}\p{P}\p{Zs}]/gu) || [];
return printable.length / str.length > 0.75;
}
// Helper function for UTF-8 validation
function tryUtf8(bytes) {
try {
let result = '';
let i = 0;
while (i < bytes.length) {
let byte1 = bytes[i++];
if (byte1 < 128) {
// Single byte character (ASCII)
result += String.fromCharCode(byte1);
} else if ((byte1 & 0xE0) === 0xC0) {
// Two byte character
if (i >= bytes.length) break;
let byte2 = bytes[i++];
result += String.fromCharCode(((byte1 & 0x1F) << 6) | (byte2 & 0x3F));
} else if ((byte1 & 0xF0) === 0xE0) {
// Three byte character
if (i + 1 >= bytes.length) break;
let byte2 = bytes[i++];
let byte3 = bytes[i++];
result += String.fromCharCode(((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F));
} else if ((byte1 & 0xF8) === 0xF0) {
// Four byte character (convert to surrogate pair)
if (i + 2 >= bytes.length) break;
let byte2 = bytes[i++];
let byte3 = bytes[i++];
let byte4 = bytes[i++];
let codePoint = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
if (codePoint > 0xFFFF) {
codePoint -= 0x10000;
result += String.fromCharCode(0xD800 + (codePoint >> 10));
result += String.fromCharCode(0xDC00 + (codePoint & 0x3FF));
} else {
result += String.fromCharCode(codePoint);
}
} else {
// Invalid byte, skip
continue;
}
}
return result;
} catch (error) {
return null;
}
}
// Known encoding mappings
const KNOWN_MAPS = [
{
name: '32-VS',
cpToByte: (cp) => {
if (cp >= 0xFE00 && cp <= 0xFE0F) return cp - 0xFE00; // 0-15
if (cp >= 0xE0100 && cp <= 0xE01EF) return cp - 0xE0100 + 16; // 16-31
return null;
}
},
{
name: '16-VS',
cpToByte: (cp) => {
if (cp >= 0xFE00 && cp <= 0xFE0F) return cp - 0xFE00; // 0-15 (4-bit nibbles)
return null;
}
},
{
name: 'ZW-SPACE',
cpToByte: (cp) => {
if (cp === 0x200B) return 1; // ZWSP = 1
if (cp === 0x200C) return 0; // ZWNJ = 0
return null;
}
},
{
name: 'ZWJ-BINARY',
cpToByte: (cp) => {
if (cp === 0x200D) return 1; // ZWJ = 1
if (cp === 0x200C) return 0; // ZWNJ = 0
return null;
}
}
];
// Cascade decoder with multiple encoding scheme detection
function bruteDecode(codePoints) {
if (!codePoints || codePoints.length === 0) return null;
// Try each known mapping
for (const mapping of KNOWN_MAPS) {
const bytes = [];
let validMapping = true;
for (const cp of codePoints) {
const byte = mapping.cpToByte(cp);
if (byte !== null) {
bytes.push(byte);
} else if (cp >= 0x200B) {
// This is a zero-width character we don't recognize for this mapping
validMapping = false;
break;
}
// Ignore non-zero-width characters for this mapping attempt
}
if (validMapping && bytes.length > 0) {
let decodedText = null;
if (mapping.name === '16-VS') {
// Special handling for 4-bit nibble encoding
if (bytes.length % 2 === 0) {
const fullBytes = [];
for (let i = 0; i < bytes.length; i += 2) {
fullBytes.push((bytes[i] << 4) | bytes[i + 1]);
}
decodedText = tryUtf8(fullBytes);
}
} else if (mapping.name.includes('BINARY') || mapping.name.includes('ZW')) {
// Binary encoding - convert bits to bytes
if (bytes.length >= 8) {
const fullBytes = [];
for (let i = 0; i < bytes.length; i += 8) {
let byte = 0;
for (let j = 0; j < 8 && i + j < bytes.length; j++) {
byte |= (bytes[i + j] << (7 - j));
}
fullBytes.push(byte);
}
decodedText = tryUtf8(fullBytes);
}
} else {
// Standard byte encoding (like 32-VS)
decodedText = tryUtf8(bytes);
}
if (decodedText && isLikelyText(decodedText)) {
return { text: decodedText, mapping: mapping.name };
}
}
}
// Try auto-learning for unknown mappings
const autoResult = autoLearnMapping(codePoints);
if (autoResult) {
return autoResult;
}
return null;
}
// Auto-learning heuristic for unknown mappings
function autoLearnMapping(cps) {
if (!cps || cps.length < 16) return null; // Need sufficient data
// Group consecutive zero-width characters
const zwGroups = [];
let currentGroup = [];
for (const cp of cps) {
if (cp >= 0x200B || (cp >= 0xFE00 && cp <= 0xFE0F) || (cp >= 0xE0100 && cp <= 0xE01EF)) {
currentGroup.push(cp);
} else if (currentGroup.length > 0) {
zwGroups.push([...currentGroup]);
currentGroup = [];
}
}
if (currentGroup.length > 0) {
zwGroups.push(currentGroup);
}
if (zwGroups.length === 0) return null;
// Try to detect patterns
const uniqueChars = [...new Set(cps.filter(cp =>
cp >= 0x200B || (cp >= 0xFE00 && cp <= 0xFE0F) || (cp >= 0xE0100 && cp <= 0xE01EF)
))];
// If we have 2-16 unique characters, try simple substitution
if (uniqueChars.length >= 2 && uniqueChars.length <= 16) {
// Try treating each unique character as a different value
const charToValue = {};
uniqueChars.forEach((char, index) => {
charToValue[char] = index;
});
const values = cps.map(cp => charToValue[cp]).filter(v => v !== undefined);
// Try different interpretations
const attempts = [];
// Attempt 1: Direct byte values
if (uniqueChars.length <= 256) {
attempts.push(values);
}
// Attempt 2: Binary if exactly 2 unique chars
if (uniqueChars.length === 2 && values.length >= 8) {
const bytes = [];
for (let i = 0; i < values.length; i += 8) {
let byte = 0;
for (let j = 0; j < 8 && i + j < values.length; j++) {
byte |= (values[i + j] << (7 - j));
}
bytes.push(byte);
}
attempts.push(bytes);
}
// Try each attempt
for (const attempt of attempts) {
const text = tryUtf8(attempt);
if (text && isLikelyText(text)) {
return {
text: text,
mapping: 'auto-learned',
details: `${uniqueChars.length} unique chars`
};
}
}
}
return null;
}
function startPageScanning() {
log('๐งช Enhanced beta scanner activated - scanning for multiple encoding schemes...');
// Initial scan
scanPageForEncodedText();
// Set up mutation observer for dynamic content
const MutationObs = window.MutationObserver || window.webkitMutationObserver;
if (MutationObs) {
scannerObserver = new MutationObs(() => {
// Debounce the scanning
clearTimeout(scannerObserver.timeout);
scannerObserver.timeout = setTimeout(scanPageForEncodedText, 500);
});
scannerObserver.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
}
}
function stopPageScanning() {
log('๐งช Enhanced beta scanner deactivated');
// Stop observer
if (scannerObserver) {
scannerObserver.disconnect();
scannerObserver = null;
}
// Remove all highlights
highlightedElements.forEach(element => {
if (element.parentNode) {
element.parentNode.replaceChild(document.createTextNode(element.textContent), element);
}
});
highlightedElements.clear();
// Remove any tooltips
document.querySelectorAll('.hidden-msg-hover-tooltip').forEach(el => el.remove());
}
function scanPageForEncodedText() {
// Get all text nodes in the document
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: function(node) {
// Skip script, style, modal content, tooltips, emoji picker, and already highlighted content
if (node.parentElement.tagName === 'SCRIPT' ||
node.parentElement.tagName === 'STYLE' ||
node.parentElement.closest('.hidden-msg-modal') ||
node.parentElement.closest('.hidden-msg-hover-tooltip') ||
node.parentElement.closest('.floating-emoji-picker') ||
node.parentElement.closest('.skin-tone-variation-popup') ||
node.parentElement.classList.contains('hidden-msg-highlight')) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
},
false
);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
textNodes.push(node);
}
// Check each text node for encoded content using the same logic as the modal
textNodes.forEach(textNode => {
const text = textNode.textContent;
if (!text || text.length < 2) return;
// Use the same decode function as the modal for consistency
const result = tryDecodeText(text);
if (result && result.text) {
console.log('[HiddenMsg] Beta scanner found encoded text:', {
originalText: text,
decodedText: result.text,
mapping: result.mapping,
details: result.details
});
const mapping = result.mapping || 'unknown';
const details = result.details || '';
highlightEncodedText(textNode, text, result.text, mapping, details);
}
});
}
function highlightEncodedText(textNode, originalText, decodedText, mapping = 'unknown', details = '') {
// Create highlight wrapper
const highlight = document.createElement('span');
highlight.className = 'hidden-msg-highlight';
highlight.textContent = originalText;
// Store decoding information for tooltip
highlight.dataset.decodedText = decodedText;
highlight.dataset.mapping = mapping;
highlight.dataset.details = details;
highlight.dataset.originalText = originalText;
// Add hover functionality
let hoverTimeout;
highlight.addEventListener('mouseenter', (e) => {
clearTimeout(hoverTimeout);
hoverTimeout = setTimeout(() => {
const target = e.target;
const storedDecodedText = target.dataset.decodedText;
const storedMapping = target.dataset.mapping;
const storedDetails = target.dataset.details;
const storedOriginalText = target.dataset.originalText;
showHoverTooltip(target, storedDecodedText, storedMapping, storedDetails, storedOriginalText);
}, 100);
});
highlight.addEventListener('mouseleave', () => {
clearTimeout(hoverTimeout);
// Don't call hideHoverTooltip() directly - let the persistence logic in showHoverTooltip() handle it
});
// Replace text node with highlighted version
if (textNode.parentNode) {
textNode.parentNode.replaceChild(highlight, textNode);
highlightedElements.add(highlight);
}
}
function showHoverTooltip(element, decodedText, mapping = 'unknown', details = '', originalText = '') {
// Remove any existing tooltip
hideHoverTooltip();
console.log('[HiddenMsg] showHoverTooltip called with:', {
decodedText,
mapping,
details,
originalText: originalText.substring(0, 50) + '...'
});
// Hide CSS tooltip by adding class to the element
element.classList.add('has-js-tooltip');
const tooltip = document.createElement('div');
tooltip.className = 'hidden-msg-hover-tooltip';
// Create tooltip content with smart link handling
const content = document.createElement('div');
// Enhanced content with link detection and GIF rendering using reusable function
const enhancedContent = enhanceContentWithLinks(decodedText, mapping, details);
content.appendChild(enhancedContent);
// Add "copy raw selectors" button for unknown mappings
if (mapping === 'unknown' || mapping === 'auto-learned') {
const copyBtn = document.createElement('button');
copyBtn.innerHTML = '๐ copy raw selectors';
copyBtn.style.cssText = `
background: #666;
color: white;
border: none;
padding: 2px 6px;
margin-top: 4px;
border-radius: 3px;
font-size: 10px;
cursor: pointer;
display: block;
`;
copyBtn.onclick = (e) => {
e.stopPropagation();
// Extract raw variation selectors and zero-width chars
const rawSelectors = [...originalText]
.map(c => c.codePointAt(0))
.filter(cp => cp >= 0x200B || (cp >= 0xFE00 && cp <= 0xFE0F) || (cp >= 0xE0100 && cp <= 0xE01EF))
.map(cp => `U+${cp.toString(16).toUpperCase()}`)
.join(' ');
navigator.clipboard.writeText(rawSelectors).then(() => {
copyBtn.innerHTML = 'โ copied!';
setTimeout(() => copyBtn.innerHTML = '๐ copy raw selectors', 1000);
}).catch(() => {
copyBtn.innerHTML = 'โ failed';
setTimeout(() => copyBtn.innerHTML = '๐ copy raw selectors', 1000);
});
};
content.appendChild(copyBtn);
}
tooltip.appendChild(content);
document.body.appendChild(tooltip);
// Position tooltip - ensure it's visible (adjusted for potentially larger content)
const rect = element.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Default positioning
let left = rect.left;
let top = rect.bottom + 5;
// Adjust if tooltip would go off-screen (increased width for GIF content)
const estimatedTooltipWidth = enhancedContent.querySelector('.gif-container') ? 450 : 350;
if (left + estimatedTooltipWidth > viewportWidth) {
left = viewportWidth - estimatedTooltipWidth - 10;
}
if (left < 10) {
left = 10;
}
// Estimated height adjustment for GIF content
const estimatedTooltipHeight = enhancedContent.querySelector('.gif-container') ? 250 : 100;
if (top + estimatedTooltipHeight > viewportHeight) {
top = rect.top - estimatedTooltipHeight - 5; // Show above element instead
}
tooltip.style.left = left + 'px';
tooltip.style.top = top + 'px';
// Show tooltip immediately (remove delay)
tooltip.classList.add('show');
// Enhanced persistence: tooltip stays visible when hovering over tooltip or element
let isOverElement = true; // Start as true since we're showing the tooltip
let isOverTooltip = false;
let hideTimer = null;
const checkAndHideTooltip = () => {
// Clear any existing timer
if (hideTimer) {
clearTimeout(hideTimer);
hideTimer = null;
}
// Set a longer delay to handle gaps between element and tooltip
hideTimer = setTimeout(() => {
if (!isOverElement && !isOverTooltip) {
hideHoverTooltip();
}
}, 500); // Increased delay even more for better persistence
};
// Element mouse events
const elementMouseEnter = () => {
isOverElement = true;
// Cancel any pending hide operation
if (hideTimer) {
clearTimeout(hideTimer);
hideTimer = null;
}
};
const elementMouseLeave = () => {
isOverElement = false;
// Use a short delay before checking to allow mouse to reach tooltip
setTimeout(() => {
if (!isOverTooltip) {
checkAndHideTooltip();
}
}, 50);
};
// Tooltip mouse events
const tooltipMouseEnter = () => {
isOverTooltip = true;
// Cancel any pending hide operation immediately
if (hideTimer) {
clearTimeout(hideTimer);
hideTimer = null;
}
};
const tooltipMouseLeave = () => {
isOverTooltip = false;
// Use a short delay before checking to allow mouse to return to element
setTimeout(() => {
if (!isOverElement) {
checkAndHideTooltip();
}
}, 50);
};
// Add event listeners
element.addEventListener('mouseenter', elementMouseEnter);
element.addEventListener('mouseleave', elementMouseLeave);
tooltip.addEventListener('mouseenter', tooltipMouseEnter);
tooltip.addEventListener('mouseleave', tooltipMouseLeave);
// Store references and cleanup functions
window.currentTooltip = tooltip;
window.currentTooltipElement = element;
window.currentTooltipCleanup = () => {
// Clear any pending hide timer
if (hideTimer) {
clearTimeout(hideTimer);
hideTimer = null;
}
element.removeEventListener('mouseenter', elementMouseEnter);
element.removeEventListener('mouseleave', elementMouseLeave);
tooltip.removeEventListener('mouseenter', tooltipMouseEnter);
tooltip.removeEventListener('mouseleave', tooltipMouseLeave);
};
}
// Reusable smart link enhancement function for both tooltips and decoder modal
function enhanceContentWithLinks(decodedText, mapping, details) {
const container = document.createElement('div');
// URL detection regex (comprehensive pattern)
const urlRegex = /(https?:\/\/(?:[-\w.])+(?::\d+)?(?:\/(?:[\w\/_.])*)?(?:\?[;&\w\/%=.]*)?)|(www\.(?:[-\w.])+(?::\d+)?(?:\/(?:[\w\/_.])*)?(?:\?[;&\w\/%=.]*)?)/gi;
// Check if the entire decoded text is a URL
const fullUrlMatch = decodedText.trim().match(/^(https?:\/\/\S+|www\.\S+)$/);
if (fullUrlMatch) {
const url = fullUrlMatch[1];
const fullUrl = url.startsWith('http') ? url : `https://${url}`;
// Check if it's a GIF link
if (isGifUrl(fullUrl)) {
// For Tenor URLs, try multiple approaches
if (fullUrl.includes('tenor.com')) {
container.innerHTML = `
`;
}
} else {
// Text might contain URLs mixed with other text
const hasUrls = urlRegex.test(decodedText);
if (hasUrls) {
// Process text to make URLs clickable
const processedText = decodedText.replace(urlRegex, (match) => {
const fullUrl = match.startsWith('http') ? match : `https://${match}`;
return `${match}`;
});
container.innerHTML = `