Khám Phá Tính Năng Ẩn Trong Zalo: Tùy Biến Reaction Độc Đáo

Vào một ngày rảnh rỗi, mình lướt Facebook và tình cờ thấy bài viết của bạn Anh VoHoai (hay còn gọi là anhwaivo) trên group J2TEAM, chia sẻ về một tính năng ẩn đầy thú vị trong Zalo Web liên quan đến cách xử lý reaction. Sau khi mày mò source code và thử nghiệm, mình phát hiện có thể dùng emoji hoặc text tùy chỉnh để react, vượt xa các biểu tượng mặc định! Trong bài này, mình sẽ giải thích cách Zalo handle reaction, cách mình khám phá tính năng này, và hướng dẫn sử dụng một userscript để bạn tự thử. Hãy cùng khám phá nào!

Zalo Xử Lý Reaction Như Thế Nào?

Dựa trên source code từ Gist của anhwaivo, mình nhận thấy Zalo quản lý reaction rất lỏng lẻo:

rIcon: Biểu tượng reaction, có thể là emoji hoặc text tùy chỉnh.

rType: Định danh loại reaction, giúp hệ thống phân loại. Hàm sendReaction: Được gọi khi bạn react, với dữ liệu dạng e.sendReaction({ rType: t, rIcon: a }).

Sau khi gửi, Zalo dùng các hàm như d(t) để xử lý sự kiện và o(e) để cập nhật giao diện chat ngay lập tức. Điều hay ho là hệ thống không giới hạn chặt chẽ rIcon, mở ra cơ hội thử những reaction độc lạ.

Phát Hiện Tính Năng Ẩn: Reaction Tùy Chỉnh

Nhờ bài viết của Anh VoHoai trên J2TEAM, mình nảy ra ý tưởng: “Sao không thử react bằng thứ gì đó ngoài emoji mặc định?” Thế là mình bắt tay thử nghiệm với text và emoji lạ. Kết quả bất ngờ: Nó hoạt động! Bạn có thể gửi reaction như “😎🔥” hoặc “Hello World” mà vẫn hiển thị trong chat. Đây là một tính năng ẩn không được Zalo công bố, như một “món quà” cho dân tò mò. Mình đã mất cả buổi để thử đủ kiểu reaction, và bạn bè trong nhóm chat cứ hỏi: “Làm sao thế?!”

Hướng Dẫn Tùy Biến Reaction Với Userscript

Để mọi người dễ dàng thử, anhwaivo đã viết một userscript đơn giản (dựa trên proof of concept anh chia sẻ). Dưới đây là code đầy đủ của userscript "Zola React" mà anh ấy đăng trên GitHub issue và group J2TEAM. Bạn có thể copy và dùng ngay với Tampermonkey hoặc Violentmonkey.

Userscript Code: Zola React (Proof of Concept)

// ==UserScript==
// @name         Zalo Custom Reactions
// @version      1.8.1
// @description  Zalo web custom reaction
// @author       Anhwaivo
// @maintainer   Kennex666 (UI/UX), Meohunter (TextBox), dacsang97 (Emoji Picker)
// @match        https://*.zalo.me/*
// @match        https://chat.zalo.me/*
// @grant        none
// @run-at       document-idle
// @downloadURL  https://github.com/anhwaivo/zalo-custom-reaction-userscript/raw/refs/heads/main/zalorcustomemoji.user.js
// ==/UserScript==

(function() {
	"use strict";
	
		const settings = {
			isRecently: false
		}
	
		const reactions = [
			{
				type: 100,
				icon: "👏",
				name: "clap",
				class: "emoji-sizer emoji-outer",
				bgPos: "80% 12.5%",
			},
			{
				type: 101,
				icon: "🎉",
				name: "huh",
				class: "emoji-sizer emoji-outer",
				bgPos: "74% 62.5%",
			},
			{
				type: 102,
				icon: "🎨",
				name: "send_custom",
				class: "emoji-sizer emoji-outer",
				bgPos: "84% 82.5%",
			},
			// Thêm emoji: Copy dòng dưới, sửa icon để thêm reaction tuỳ ý (gồm text hoặc emoji),
			// {type: "", icon: "", name: "", class: "emoji-sizer emoji-outer", bgPos: "74% 62.5%"},
		];
	
		const RecentlyReaction = {
			add: function (reaction) {
				const emojiCustom = {
					type: simpleHash(reaction),
					icon: reaction,
					name: reaction,
					class: "emoji-sizer emoji-outer",
					bgPos: "0% 0%",
				};
				if (settings.isRecently){
					reactions[reactions.length - 1] = emojiCustom;
				}
				else {
					reactions.push(emojiCustom);
				}
				settings.isRecently = true;
				localStorage.setItem("recentlyCustomReaction", JSON.stringify(emojiCustom));
			},
	
			get: function () {
				const reaction = localStorage.getItem("recentlyCustomReaction");
				if (reaction) {
					return JSON.parse(reaction);
				}
				return null;
			},
	
			load: function () {
				const reaction = this.get();
				if (reaction) {
					settings.isRecently = true;
					reactions.push(reaction);
				}
			}
			
		}
	
		// Đồng bộ id cho người dùng script (Emoji sẽ hiện được khi người dùng xài cùng emoji)
		function simpleHash(str) {
			let hash = 0;
			for (let i = 0; i < str.length; i++) {
				hash = (hash << 5) - hash + str.charCodeAt(i);
				hash |= 0; // convert to 32-bit int
			}
			return Math.abs(hash); // chỉ là số, không có chữ
		}
	
		// Common emojis for the emoji picker
		const emojiCategories = {
			"Smileys": ["😀", "😃", "😄", "😁", "😆", "😅", "😂", "🤣", "🥲", "😊", "😇", "🙂", "🙃", "😉", "😌", "😍", "🥰", "😘", "😗", "😙", "😚", "😋", "😛", "😝", "😜", "🤪", "🤨", "🧐", "🤓", "😎", "🥸", "🤩", "🥳", "😏", "😒", "😞", "😔", "😟", "😕", "🙁", "☹️", "😣", "😖", "😫", "😩", "🥺", "😢", "😭", "😮‍💨", "😤", "😠", "😡", "🤬", "🤯", "😳", "🥵", "🥶", "😱", "😨", "😰", "😥"],
			"Gestures": ["👋", "🤚", "✋", "🖖", "👌", "🤌", "🤏", "✌️", "🤞", "🤟", "🤘", "🤙", "👈", "👉", "👆", "🖕", "👇", "👍", "👎", "✊", "👊", "🤛", "🤜", "👏", "🙌", "👐", "🤲", "🤝", "🙏"],
			"People": ["👶", "👧", "🧒", "👦", "👩", "🧑", "👨", "👩‍🦱", "🧑‍🦱", "👨‍🦱", "👩‍🦰", "🧑‍🦰", "👨‍🦰", "👱‍♀️", "👱", "👱‍♂️", "👩‍🦳", "🧑‍🦳", "👨‍🦳", "👩‍🦲", "🧑‍🦲", "👨‍🦲", "🧔‍♀️", "🧔", "🧔‍♂️"],
			"Animals": ["🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐻‍❄️", "🐨", "🐯", "🦁", "🐮", "🐷", "🐽", "🐸", "🐵", "🙈", "🙉", "🙊", "🐒", "🐔", "🐧", "🐦", "🐤", "🐣", "🐥", "🦆", "🦅", "🦉", "🦇", "🐺", "🐗", "🐴", "🦄", "🐝", "🪱", "🐛", "🦋", "🐌", "🐞"],
			"Food": ["🍎", "🍐", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🫐", "🍈", "🍒", "🍑", "🥭", "🍍", "🥥", "🥝", "🍅", "🍆", "🥑", "🥦", "🥬", "🥒", "🌶", "🫑", "🌽", "🥕", "🥔", "🍠", "🥐", "🥯", "🍞", "🥖", "🥨", "🧀", "🥚", "🍳", "🧈", "🥞", "🧇", "🥓", "🍔", "🍟", "🍕", "🌭", "🥪", "🌮", "🌯", "🫔", "🥙"],
			"Activities": ["⚽️", "🏀", "🏈", "⚾️", "🥎", "🎾", "🏐", "🏉", "🥏", "🎱", "🪀", "🏓", "🏸", "🏒", "🏑", "🥍", "🏏", "🪃", "🥅", "⛳️", "🪁", "🏹", "🎣", "🤿", "🥊", "🥋", "🎽", "🛹", "🛼", "🛷", "⛸", "🥌", "🎿"],
			"Objects": ["⌚️", "📱", "💻", "⌨️", "🖥", "🖱", "🖨", "🕹", "🗜", "💾", "💿", "📀", "📼", "📷", "📸", "📹", "🎥", "📽", "🎞", "📞", "☎️", "📟", "📠", "📺", "📻", "🎙", "🎚", "🎛", "🧭", "⏱", "⏲", "⏰", "🕰"],
			"Symbols": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔", "❣️", "💕", "💞", "💓", "💗", "💖", "💘", "💝", "💟", "☮️", "✝️", "☪️", "🕉", "☸️", "✡️", "🔯", "🕎", "☯️", "☦️", "🛐", "⛎", "♈️", "♉️", "♊️", "♋️", "♌️", "♍️", "♎️", "♏️", "♐️", "♑️", "♒️", "♓️", "🆔", "⚛️"]
		};
	
		const createEmojiPicker = () => {
			const picker = document.createElement("div");
			picker.id = "emoji-picker";
			picker.style.cssText = `
				position: absolute;
				bottom: calc(100% + 10px);
				right: 0;
				background: white;
				border-radius: 12px;
				box-shadow: 0 4px 16px rgba(0,0,0,0.2);
				padding: 8px;
				z-index: 10000;
				animation: fadeIn 0.2s ease-out;
				width: 280px;
				max-height: 350px;
				overflow: hidden;
				display: flex;
				flex-direction: column;
			`;
	
			// Create tabs for categories
			const tabsContainer = document.createElement("div");
			tabsContainer.style.cssText = `
				display: flex;
				overflow-x: auto;
				padding-bottom: 5px;
				margin-bottom: 5px;
				border-bottom: 1px solid #eee;
				gap: 4px;
				scrollbar-width: none;
				-ms-overflow-style: none;
				height: 36px;
				min-height: 36px;
				align-items: center;
			`;
			
			// Hide scrollbar
			tabsContainer.addEventListener('mousewheel', function(e) {
				this.scrollLeft += e.deltaY;
				e.preventDefault();
			});
			
			// Add CSS to hide scrollbar
			const style = document.createElement('style');
			style.textContent = `
				#emoji-picker div::-webkit-scrollbar {
					display: none;
				}
				.emoji-category-tab {
					display: flex;
					align-items: center;
					justify-content: center;
					height: 28px;
					width: 28px;
				}
			`;
			document.head.appendChild(style);
	
			// Create emoji content container
			const emojiContent = document.createElement("div");
			emojiContent.style.cssText = `
				overflow-y: auto;
				display: grid;
				grid-template-columns: repeat(8, 1fr);
				gap: 4px;
				padding-right: 4px;
				max-height: 240px;
			`;
	
			// Create tabs for each category
			Object.keys(emojiCategories).forEach((category, idx) => {
				const tab = document.createElement("button");
				tab.className = "emoji-category-tab";
				tab.dataset.category = category;
				
				// Use an icon or first emoji to represent each category
				const categoryIcons = {
					"Smileys": "😀",
					"Gestures": "👍",
					"People": "👨",
					"Animals": "🐱",
					"Food": "🍔",
					"Activities": "⚽️",
					"Objects": "📱",
					"Symbols": "❤️"
				};
				
				tab.textContent = categoryIcons[category] || category.slice(0, 1);
				tab.title = category;
				
				tab.style.cssText = `
					background: ${idx === 0 ? '#e3f2fd' : 'transparent'};
					border: none;
					border-radius: 6px;
					padding: 0;
					cursor: pointer;
					font-size: 16px;
					min-width: 28px;
					min-height: 28px;
					text-align: center;
					transition: background-color 0.2s;
					flex-shrink: 0;
					display: flex;
					align-items: center;
					justify-content: center;
				`;
				
				tab.addEventListener("click", () => {
					// Deselect all tabs
					document.querySelectorAll(".emoji-category-tab").forEach(t => {
						t.style.background = "transparent";
					});
					
					// Select this tab
					tab.style.background = "#e3f2fd";
					
					// Clear and populate emoji content
					emojiContent.innerHTML = "";
					
					// Add emojis for this category
					emojiCategories[category].forEach(emoji => {
						const emojiButton = document.createElement("button");
						emojiButton.className = "emoji-button";
						emojiButton.textContent = emoji;
						emojiButton.style.cssText = `
							background: none;
							border: none;
							cursor: pointer;
							font-size: 18px;
							padding: 4px;
							border-radius: 4px;
							transition: background-color 0.2s, transform 0.2s;
						`;
						emojiButton.onmouseover = () => {
							emojiButton.style.backgroundColor = "#f0f0f0";
							emojiButton.style.transform = "scale(1.1)";
						};
						emojiButton.onmouseout = () => {
							emojiButton.style.backgroundColor = "transparent";
							emojiButton.style.transform = "scale(1)";
						};
						
						emojiContent.appendChild(emojiButton);
					});
				});
				
				tabsContainer.appendChild(tab);
			});
	
			// Add tabs and content to picker
			picker.appendChild(tabsContainer);
			picker.appendChild(emojiContent);
			
			// Initialize with the first category
			setTimeout(() => {
				const firstTab = picker.querySelector(".emoji-category-tab");
				if (firstTab) firstTab.click();
			}, 0);
	
			document.addEventListener("click", (e) => {
				if (picker.style.display === "flex" && !picker.contains(e.target) && e.target.id !== "emoji-button") {
					picker.style.display = "none";
				}
			});
	
			picker.style.display = "none";
			return picker;
		};
	
		const createTextInputPopup = () => {
			const popup = document.createElement("div");
			popup.id = "custom-text-reaction-popup";
			popup.style.cssText = `
				position: fixed;
				top: 50%;
				left: 50%;
				transform: translate(-50%, -50%);
				background: white;
				border-radius: 12px;
				box-shadow: 0 4px 20px rgba(0,0,0,0.25);
				padding: 20px;
				z-index: 9999;
				display: none;
				flex-direction: column;
				gap: 15px;
				min-width: 300px;
				font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', sans-serif;
				animation: fadeIn 0.2s ease-out;
			`;
			
			const title = document.createElement("div");
			title.textContent = "Tùy chỉnh reaction";
			title.style.cssText = "font-weight: bold; font-size: 16px; color: #333; margin-bottom: 5px;";
			
			const inputContainer = document.createElement("div");
			inputContainer.style.cssText = "position: relative;";
			
			const input = document.createElement("input");
			input.type = "text";
			input.id = "custom-text-reaction-input";
			input.placeholder = "Nhập nội dung reaction...";
			input.maxLength = 20; 
			input.style.cssText = `
				padding: 10px 12px;
				padding-right: 40px;
				border: 2px solid #e0e0e0;
				border-radius: 8px;
				width: 100%;
				box-sizing: border-box;
				font-size: 14px;
				transition: border-color 0.2s;
				outline: none;
			`;
			input.addEventListener("focus", () => {
				input.style.borderColor = "#2196F3";
			});
			input.addEventListener("blur", () => {
				input.style.borderColor = "#e0e0e0";
			});
			
			// Emoji button
			const emojiButton = document.createElement("button");
			emojiButton.id = "emoji-button";
			emojiButton.textContent = "😊";
			emojiButton.style.cssText = `
				position: absolute;
				right: 10px;
				top: 50%;
				transform: translateY(-50%);
				background: none;
				border: none;
				font-size: 18px;
				cursor: pointer;
				padding: 0;
				opacity: 0.7;
				transition: opacity 0.2s, transform 0.2s;
			`;
			emojiButton.onmouseover = () => {
				emojiButton.style.opacity = "1";
				emojiButton.style.transform = "translateY(-50%) scale(1.1)";
			};
			emojiButton.onmouseout = () => {
				emojiButton.style.opacity = "0.7";
				emojiButton.style.transform = "translateY(-50%) scale(1)";
			};
	
			// Create emoji picker
			const emojiPicker = createEmojiPicker();
			
			// Toggle emoji picker
			emojiButton.addEventListener("click", (e) => {
				e.preventDefault();
				e.stopPropagation();
				if (emojiPicker.style.display === "none" || emojiPicker.style.display === "") {
					emojiPicker.style.display = "flex";
				} else {
					emojiPicker.style.display = "none";
				}
			});
			
			// Add emoji to input when clicked
			emojiPicker.addEventListener("click", (e) => {
				if (e.target.classList.contains("emoji-button")) {
					input.value += e.target.textContent;
					charCounter.textContent = `${input.value.length}/20`;
					emojiPicker.style.display = "none";
					input.focus();
				}
			});
			
			const charCounter = document.createElement("div");
			charCounter.style.cssText = "position: absolute; right: 10px; bottom: -18px; font-size: 11px; color: #999;";
			charCounter.textContent = "0/20";
			
			input.addEventListener("input", () => {
				charCounter.textContent = `${input.value.length}/20`;
			});
			
			inputContainer.appendChild(input);
			inputContainer.appendChild(emojiButton);
			inputContainer.appendChild(charCounter);
			inputContainer.appendChild(emojiPicker);
			
			const buttonContainer = document.createElement("div");
			buttonContainer.style.cssText = "display: flex; justify-content: flex-end; gap: 12px; margin-top: 10px;";
			
			const cancelButton = document.createElement("button");
			cancelButton.textContent = "Hủy";
			cancelButton.style.cssText = `
				padding: 8px 16px;
				border: none;
				border-radius: 6px;
				background-color: #f5f5f5;
				color: #333;
				font-weight: 500;
				cursor: pointer;
				transition: background-color 0.2s;
			`;
			cancelButton.onmouseover = () => {
				cancelButton.style.backgroundColor = "#e0e0e0";
			};
			cancelButton.onmouseout = () => {
				cancelButton.style.backgroundColor = "#f5f5f5";
			};
			cancelButton.onclick = () => {
				hidePopup();
			};
			
			const confirmButton = document.createElement("button");
			confirmButton.textContent = "Gửi";
			confirmButton.style.cssText = `
				padding: 8px 16px;
				border: none;
				border-radius: 6px;
				background-color: #2196F3;
				color: white;
				font-weight: 500;
				cursor: pointer;
				transition: background-color 0.2s;
			`;
			confirmButton.onmouseover = () => {
				confirmButton.style.backgroundColor = "#1976D2";
			};
			confirmButton.onmouseout = () => {
				confirmButton.style.backgroundColor = "#2196F3";
			};
			
			buttonContainer.appendChild(cancelButton);
			buttonContainer.appendChild(confirmButton);
	
			
			// key down event enter for input
			input.addEventListener("keydown", (e) => {
				if (e.key === "Enter") {
					confirmButton.click()
				}
			});
			
			popup.appendChild(title);
			popup.appendChild(inputContainer);
			popup.appendChild(buttonContainer);
			
			// Add overlay
			const overlay = document.createElement("div");
			overlay.id = "custom-reaction-overlay";
			overlay.style.cssText = `
				position: fixed;
				top: 0;
				left: 0;
				right: 0;
				bottom: 0;
				background: rgba(0,0,0,0.4);
				z-index: 9998;
				display: none;
				animation: fadeIn 0.2s ease-out;
			`;
			overlay.addEventListener("click", (e) => {
				if (e.target === overlay) {
					hidePopup();
				}
			});
			
			const hidePopup = () => {
				popup.style.display = "none";
				overlay.style.display = "none";
				emojiPicker.style.display = "none";
			};
			
			document.body.appendChild(popup);
			document.body.appendChild(overlay);
			
			return {
				popup, 
				input, 
				confirmButton, 
				show: () => {
					popup.style.display = "flex";
					overlay.style.display = "block";
					input.value = "";
					charCounter.textContent = "0/20";
					input.focus();
				},
				hide: hidePopup,
				overlay
			};
		};
	
		const enhanceReactionPanel = () => {
			const style = document.createElement("style");
			style.textContent = `
				.reaction-emoji-list {
					display: flex !important;
					width: fit-content !important; /* Wider emoji list */
					gap: 2px !important;
					border-radius: 28px !important;
					background-color: white !important;
					box-shadow: 0 2px 12px rgba(0,0,0,0.15) !important;
				}
				
				.reaction-emoji-icon {
					display: flex !important;
					align-items: center !important;
					justify-content: center !important;
					font-size: 20px !important;
					border-radius: 50% !important;
					cursor: pointer !important;
					background-color: rgba(240, 240, 240, 0.5) !important;
					transition: transform 0.2s, background-color 0.2s !important;
				}
				
				.reaction-emoji-text {
					white-space: nowrap !important;
					overflow: hidden !important;
					text-overflow: ellipsis !important;
					max-width: 3ch !important;
				}
		
				.reaction-emoji-icon:hover {
					transform: scale(1.1) !important;
					background-color: #e3f2fd !important;
				}
				
				.emoji-list-wrapper {
					padding: 0.07rem !important;
				}
				
				@keyframes fadeIn {
					from { opacity: 0; }
					to { opacity: 1; }
				}
				
				@keyframes popIn {
					0% { transform: scale(0.8); opacity: 0; }
					70% { transform: scale(1.05); opacity: 1; }
					100% { transform: scale(1); opacity: 1; }
				}
			`;
			document.head.appendChild(style);
		};
	
		const observer = new MutationObserver(mutations => mutations.forEach(m => {
			if (m.type === "childList" && m.addedNodes.length > 0 && Array.from(m.addedNodes).some(n => n.querySelector?.(".reaction-emoji-list"))) {
				setTimeout(() => {
					document.querySelectorAll(".reaction-emoji-list").forEach(list => {
						if (list.getAttribute("data-extended") !== "true") {
							list.setAttribute("data-extended", "true");
							const wrapper = list.closest(".emoji-list-wrapper");
							if (wrapper) {
								const btn = wrapper.querySelector('[id^="reaction-btn-"]');
								const id = btn?.id.replace("reaction-btn-", "");
								
								list.style.animation = "popIn 0.3s ease-out forwards";
								
								reactions.forEach((react, idx) => {
									const div = document.createElement("div");
									const divEmoji = document.createElement("span");
									div.className = "reaction-emoji-icon";
									if (react.icon.length > 2) {
										div.className += " reaction-emoji-text";
									}
	
									div.setAttribute("data-custom", "true");
									div.style.animationDelay = `${50 * (idx + 7)}ms`;
									
									if (react.name === "send_custom") {
										divEmoji.innerText = react.icon;
										div.title = "Gửi reaction tùy chỉnh";
									} else {
										if (react.icon.length > 2) {
											div.title = react.icon;
										}
										divEmoji.innerText = react.icon;
									}
									
									div.appendChild(divEmoji);
									list.appendChild(div);
									div.addEventListener("click", e => {
										e.preventDefault();
										e.stopPropagation();
										
										if (react.name === "send_custom") {
											if (!window.textInputPopup) {
												window.textInputPopup = createTextInputPopup();
											}
											
											window.textInputPopup.show();
											
											window.currentReactionContext = { wrapper, id };
											
											window.textInputPopup.confirmButton.onclick = () => {
												const customText = window.textInputPopup.input.value.trim();
												if (customText) {
													const customReaction = {
														...react,
														icon: customText,
														type: simpleHash(customText)
													};
													RecentlyReaction.add(customText);
													sendReaction(wrapper, id, customReaction);
													window.textInputPopup.hide();
												}
											};
											
											return;
										}
										
										sendReaction(wrapper, id, react);
									});
								});
							}
						}
					});
				}, 50);
			}
		}));
		
		function sendReaction(wrapper, id, react) {
			const getReactFiber = el => { 
				for (const k in el) if (k.startsWith("__react")) return el[k]; 
				return null 
			};
			
			let fiber = getReactFiber(wrapper);
			if (fiber) {
				while (fiber) {
					if (fiber.memoizedProps?.sendReaction) {
						fiber.memoizedProps.sendReaction({rType: react.type, rIcon: react.icon});
						id && updateBtn(id, react);
						break;
					}
					fiber = fiber.return;
				}
			}
			
			if (window.S?.default?.reactionMsgInfo) {
				const msg = wrapper.closest(".msg-item");
				const msgFiber = msg && getReactFiber(msg);
				msgFiber?.memoizedProps?.sendReaction({rType: react.type, rIcon: react.icon});
				id && updateBtn(id, react);
				wrapper.classList.add("hide-elist");
				wrapper.classList.remove("show-elist");
			}
		}
	
		function updateBtn(id, react) {
			const span = document.querySelector(`#reaction-btn-${id} span`);
			if (span) {
				span.innerHTML = "";
				
				if (react.name === "text" || typeof react.icon === "string" && react.icon.length > 2) {
					const textContainer = document.createElement("div");
					textContainer.className = "text-reaction";
					textContainer.textContent = react.icon;
					span.appendChild(textContainer);
				} else {
					const emoji = document.createElement("span");
					if (react.class) {
						emoji.className = react.class;
						emoji.style.cssText = `background: url("assets/emoji.1e7786c93c8a0c1773f165e2de2fd129.png?v=20180604") ${react.bgPos} / 5100% no-repeat; margin: -1px; position: relative; top: 2px`;
					} else {
						emoji.textContent = react.icon;
						emoji.style.fontSize = "20px";
					}
					span.appendChild(emoji);
				}
			}
		}
	
		function initReactions() {
			if (window.S?.default) {
				if (!window.S.default.reactionMsgInfo.some(r => r.rType >= 100)) {
					window.S.default.reactionMsgInfo = [...window.S.default.reactionMsgInfo, ...reactions.map(r => ({rType: r.type, rIcon: r.icon, name: r.name}))];
				}
			} else setTimeout(initReactions, 1000);
		}
	
		const style = document.createElement("style");
		style.textContent = `
			[data-custom="true"] { position: relative; }
			[data-custom="true"]::after { 
				content: ''; 
				position: absolute; 
				bottom: -2px; 
				right: -2px; 
				width: 6px; 
				height: 6px; 
				background: #2196F3; 
				border-radius: 50%; 
			}
			.msg-reaction-icon span { 
				display: flex; 
				align-items: center; 
				justify-content: center; 
			}
			
			/* Text reaction styles */
			.text-reaction {
				background-color: #e3f2fd;
				border-radius: 12px;
				padding: 3px 10px;
				font-size: 12px;
				font-weight: 600;
				color: #1976d2;
				max-width: 120px;
				overflow: hidden;
				text-overflow: ellipsis;
				white-space: nowrap;
				box-shadow: 0 1px 3px rgba(0,0,0,0.1);
			}
			
			/* Hover tooltips */
			[data-custom="true"] {
				position: relative;
			}
			
			[data-custom="true"]:hover::before {
				content: attr(title);
				position: absolute;
				top: -30px;
				left: 50%;
				transform: translateX(-50%);
				background-color: rgba(0,0,0,0.7);
				color: white;
				padding: 4px 8px;
				border-radius: 4px;
				font-size: 12px;
				white-space: nowrap;
				pointer-events: none;
				opacity: 0;
				animation: fadeIn 0.2s forwards;
				z-index: 9999;
			}
		`;
		document.head.appendChild(style);
	
		const init = () => {
			observer.observe(document.body, {childList: true, subtree: true});
			initReactions();
			enhanceReactionPanel();
			RecentlyReaction.load();
		};
		"loading" === document.readyState ? document.addEventListener("DOMContentLoaded", init) : init();
	})();
  1. Bước 1: Cài Đặt Tampermonkey Hoặc Violentmonkey

    Tải extension Tampermonkey (cho Chrome) hoặc Violentmonkey (cho Firefox) từ cửa hàng trình duyệt.

    Đây là công cụ để chạy userscript, cho phép can thiệp vào giao diện Zalo Web. Cài xong, bật extension và chuẩn bị thêm script.

  2. Bước 2: Thêm Userscript

    Copy code trên vào Tampermonkey/Violentmonkey, tạo script mới, paste và lưu

    Script sẽ inject vào hàm sendReaction của Zalo, cho phép custom rIcon. Reload Zalo Web để kích hoạt.

  3. Bước 3: Thử Reaction Tùy Chỉnh

    Truy cập Zalo Web, mở khung chat và nhấp vào nút reaction (hoặc nút custom nếu script thêm).

    Nhập emoji hoặc text tùy chỉnh (ví dụ: “🚀 Cool”).

    Nhấn gửi và xem reaction hiển thị trong chat.

    Mình đã test trên Chrome, hoạt động mượt mà. Nếu thấy hay, cho anhwaivo một star trên GitHub issue của Zalo Custom Reactions nhé! (Link issue: Zalo Custom Reactions).

Lợi Ích Của Reaction Tùy Chỉnh

Tính năng này mang lại nhiều điểm thú vị: Sáng tạo không giới hạn: Tạo reaction độc đáo, từ emoji hiếm đến text ngắn, làm cuộc trò chuyện sinh động hơn.

Tăng tương tác: Bạn bè sẽ bất ngờ và thích thú với những reaction “không đụng hàng”.

Học hỏi kỹ thuật: Nếu bạn là dev, mày mò source code Zalo giúp hiểu cách xử lý API.

Cộng đồng chia sẻ: Script dễ fork, bạn có thể thêm tính năng và chia sẻ lại.

Lưu Ý Khi Sử Dụng Userscript

Dù thú vị, bạn cần cẩn thận khi dùng: Tính ổn định: Zalo có thể cập nhật và chặn reaction tùy chỉnh.

Bảo mật: Chỉ dùng script từ nguồn đáng tin cậy như GitHub của anhwaivo.

Hạn chế lạm dụng: Reaction quá dài có thể làm chậm chat.

Test cẩn thận: Thử trên tài khoản phụ để tránh lỗi.

Mình từng gặp lag khi gửi text dài, nên khuyên bạn giữ reaction dưới 10 ký tự.

Kết Luận

Nhờ bài viết của Anh VoHoai trên J2TEAM, mình đã khám phá được tính năng tùy chỉnh reaction siêu thú vị trong Zalo. Với userscript trên, bạn có thể tạo reaction độc đáo để “khoe” với bạn bè. Copy code và thử ngay nhé! Nếu thấy bài viết này hay, để lại một star trên GitHub hoặc góp ý giúp mình nha. Cùng đào sâu thêm những bí mật của Zalo nào!

Copyright (c):
blog.inlove.eu.org

About the author

.Thinhem
Có những thứ luôn quẩn quanh cuộc đời bạn và đôi lúc không có lời giải thích, liệu rằng đó có phải là duyên số?!

2 nhận xét

  1. Ngô Vi Minh Hiếu
    Ngô Vi Minh Hiếu
    Nhận xét này đã bị tác giả xóa.
  2. Nặc danh
    Nhận xét này đã bị quản trị viên blog xóa.
Hãy viết ý kiến của bạn về bài viết này. Đừng quên kiểm tra Notify Me để nhận thông báo trả lời.

Tham gia cuộc trò chuyện