1// js/social.js 2import { 3 db, 4 doc, 5 getDoc, 6 updateDoc, 7 collection, 8 addDoc, 9 query, 10 orderBy, 11 limit, 12 startAfter, 13 getDocs, 14 arrayUnion, 15 arrayRemove, 16 where, 17 onSnapshot, 18 auth, 19 // --- THÃM RTDB --- 20 rtdb, 21 rtdbRef, 22 onValue, 23 // --- THÃM STORAGE Äá» UPLOAD ẢNH CHAT --- 24 storage, 25 ref, 26 uploadString, 27 getDownloadURL, 28} from "./firebase-config.js"; 29import { ITEM_DB } from "./data.js"; 30 31// Äảm bảo 100% ngưá»i chÆ¡i nà o cÅ©ng nhìn thấy Äá» cá»§a ngưá»i khác 32window.XIANXIA_ITEMS = ITEM_DB; 33 34let lastCommentDoc = null; 35const COMMENTS_PER_PAGE = 10; 36let foundUserUid = null; 37 38// [GUARD] Cá» chá»ng spam click Tìm Bạn â chặn user bấm liên tục gây tá»n Firestore Read 39let _isSearching = false; 40 41window.openSocialModal = () => { 42 const modal = document.getElementById("socialModal"); 43 if (modal) modal.style.display = "flex"; 44}; 45 46// [FIX BUG] quickSearch nháºn cả UID string (từ comment avatar) lẫn shortId (sá» 6 chữ sá») 47// Phân biá»t: shortId là sá» 6 chữ sá», UID là string dà i >10 ký tá»± 48window.quickSearch = (id, openChatMode = false) => { 49 if (!id || id === "Äang cáºp nháºt..." || id === "???" || id === "") 50 return alert("Äạo hữu nà y chưa có ID sá»!"); 51 if (openChatMode) { 52 // [TRUYá»N ÃM MODE]: Gá»i trá»±c tiếp searchUserById vá»i UID 53 const input = document.getElementById("search-id-input"); 54 if (input) input.value = id; 55 window.searchUserById(true); 56 } else {
57 // [TÃM KIẾM MODE]: Luá»ng cÅ© 58 const input = document.getElementById("search-id-input"); 59 if (input) { 60 input.value = id; 61 window.searchUserById(false); 62 } 63 } 64}; 65 66// [REWRITE] searchUserById â Dual-mode: UID trá»±c tiếp hoặc shortId 67window.searchUserById = async (openChatMode = false) => { 68 const user = auth.currentUser; 69 if (!user) return alert("Vui lòng ÄÄng nháºp!"); 70 71 // [GUARD] Chá»ng spam click â nếu Äang search thì chặn 72 if (_isSearching) return; 73 _isSearching = true; 74 75 const rawInput = document.getElementById("search-id-input").value.trim(); 76 if (!rawInput) { 77 _isSearching = false; 78 return alert("Vui lòng nháºp ID sá» hoặc UID!"); 79 } 80 81 // [LOADING STATE] Hiá»n thá» trạng thái Äang tìm (CHá» KHI KIá»M TRA TRONG Là NGƯá»I CHÆ I BÃNH THƯá»NG) 82 const resultEl = document.getElementById("search-result"); 83 if (!openChatMode && resultEl) resultEl.style.display = "none"; 84 85 try { 86 let docSnapResult = null; 87 let targetData = null; 88 let targetUid = null; 89 90 // [DUAL-MODE] Nháºn diá»n: nếu input là sá» thuần (6 chữ sá») â shortId, ngược lại â UID 91 const isShortId = /^\d{4,6}$/.test(rawInput); 92 93 if (isShortId) { 94 // MODE 1: Tìm theo shortId â 1 Firestore Read (auto-indexed single field) 95 const qS = query(collection(db, "users"), where("shortId", "==", parseInt(rawInput))); 96 const querySnapshot = await getDocs(qS); 97 if (!querySnapshot.empty) { 98 docSnapResult = querySnapshot.docs[0]; 99 targetData = docSnapResult.data(); 100 targetUid = docSnapResult.id; 101 } 102 } else { 103 // MODE 2: Tìm trá»±c tiếp bằng UID â 1 Firestore Read tuyá»t Äá»i 104 const userDocRef = doc(db, "users", rawInput); 105 const userDocSnap = await getDoc(userDocRef); 106 if (userDocSnap.exists()) { 107 targetData = userDocSnap.data(); 108 targetUid = userDocSnap.id; 109 } 110 } 111 112 // Không tìm thấy â thông báo UI rõ rà ng 113 if (!targetData || !targetUid) { 114 if (!openChatMode && resultEl) resultEl.style.display = "none"; 115 alert("Không tìm thấy Äạo hữu! Kiá»m tra lại ID."); 116 _isSearching = false; 117 return; 118 } 119 120 // Chặn tá»± tìm chÃnh mình 121 if (targetUid === user.uid) { 122 _isSearching = false; 123 return alert("Không thá» tá»± truyá»n âm cho chÃnh mình!"); 124 } 125 126 // Ghi nháºn UID Äã tìm thấy và o biến toà n cục 127 foundUserUid = targetUid; 128 129 // [KIá»M TRA CHẶN]: Äá»c dữ liá»u chặn từ RAM 130 const myBlockedList = window.CURRENT_USER_DATA 131 ? window.CURRENT_USER_DATA.blockedUsers || [] 132 : []; 133 const theirBlockedList = targetData.blockedUsers || []; 134 const iAmBlockingThem = myBlockedList.includes(foundUserUid); 135 const theyAreBlockingMe = theirBlockedList.includes(user.uid); 136 137 if (openChatMode) { 138 // ========================================== 139 // Ká»CH BẢN A: Gá»I TỪ NÃT "TRUYá»N ÃM" 140 // ========================================== 141 if (theyAreBlockingMe) { 142 return alert("Äạo hữu nà y Äang bế quan, cá»± tuyá»t má»i truyá»n âm!"); 143 } 144 if (iAmBlockingThem) { 145 return alert("Bạn Äã ÄÆ°a ngưá»i nà y và o sá» Äen. Không thá» giao tiếp."); 146 } 147 148 // [SOCIAL V3 Phase 3] Rewired: Má» Chat Dock thay vì chatbox cÅ© 149 if (window._chatDock && window._chatDock.openChat) { 150 window._chatDock.openChat(targetUid, targetData.name, targetData.photoURL); 151 } else { 152 if (window.showToast) window.showToast('Há» thá»ng chat Äang khá»i tạo...', 'info'); 153 } 154 } else { 155 // ========================================== 156 // Ká»CH BẢN B: Gá»I TỪ MODAL TÃM ÄẠO Há»®U (LUá»NG CŨ) 157 // ========================================== 158 // Render kết quả tìm kiếm 159 const rankInfo = window.tinhToanCanhGioi(targetData.tu_vi || 0, targetData.role); 160 if (resultEl) resultEl.style.display = "block"; 161 document.getElementById("s-avatar").src = 162 targetData.photoURL ||
163 "https://ui-avatars.com/api/?name=User&background=random"; 164 document.getElementById("s-name").innerText = targetData.name || "Äạo Hữu"; 165 document.getElementById("s-rank").innerText = rankInfo.name; 166 document.getElementById("s-rank").style.color = rankInfo.color; 167 168 // TẠO NÃT CHẶN Äá»NG (innerHTML an toà n vì foundUserUid là Firebase UID) 169 const blockBtnHtml = `<button onclick="toggleBlockUser('${foundUserUid}')" style="margin-top: 10px; background:${iAmBlockingThem ? "#4caf50" : "#d32f2f"}; color:white; border:none; padding:5px 15px; border-radius:15px; cursor:pointer; font-size:0.8rem; transition: 0.3s;"><i class="fas ${iAmBlockingThem ? "fa-unlock" : "fa-ban"}"></i> ${iAmBlockingThem ? "Bá» Chặn Sá» Äen" : "ÄÆ°a Và o Sá» Äen"}</button>`; 170 const bioEl = document.getElementById("s-bio"); 171 const chatArea = document.getElementById("chat-input-area"); 172 173 // XỬ Là GIAO DIá»N THEO TRẠNG THÃI CHẶN 174 if (theyAreBlockingMe) { 175 bioEl.innerHTML = `<span style="color:#e91e63; font-weight:bold;"><i class="fas fa-lock"></i> Äạo hữu nà y Äang bế quan, cá»± tuyá»t má»i truyá»n âm!</span> <br> ${blockBtnHtml}`; 176 if (chatArea) chatArea.style.display = "none"; 177 } else if (iAmBlockingThem) { 178 bioEl.innerHTML = `<span style="color:#888; font-style:italic;"><i class="fas fa-user-slash"></i> Bạn Äã ÄÆ°a ngưá»i nà y và o sá» Äen. Không thá» giao tiếp.</span> <br> ${blockBtnHtml}`; 179 if (chatArea) chatArea.style.display = "none"; 180 } else { 181 bioEl.innerHTML = 182 (targetData.bio ? `"${targetData.bio}"` : '"Ngưá»i nà y chưa có châm ngôn..."') + 183 `<br> ${blockBtnHtml}`; 184 if (chatArea) chatArea.style.display = "none"; // Mặc Äá»nh ẩn, bấm "Hiá»n ô nháºp" má»i ra 185 } 186 } 187 188 } catch (error) { 189 console.warn("Lá»i tìm kiếm Äạo hữu:", error.message || error); 190 alert("Lá»i kết ná»i! Vui lòng thá» lại sau."); 191 if (resultEl) resultEl.style.display = "none"; 192 } finally { 193 // [GUARD] Luôn giải phóng cá», ká» cả khi lá»i 194 _isSearching = false; 195 } 196}; 197// --- LOGIC CHẶN / Bá» CHẶN ÄẠO Há»®U --- 198window.toggleBlockUser = async (targetUid) => { 199 // SECURITY [SEC-HIGH-01 2026-04-26]: Legacy client-side block write DISABLED. 200 // This function previously called updateDoc({ blockedUsers: arrayUnion/arrayRemove }), 201 // which: 202 // 1. Bypassed the blockUser CF (no atomic friends[] cleanup or relationship doc delete) 203 // 2. Left the friendship state inconsistent (target still had blocker in their friends[]) 204 // Defense-in-depth: Firestore rules deny-list (SEC-CRIT-01) now ALSO blocks this write 205 // at the DB layer. This stub is a second layer of protection at the application layer. 206 // All block operations MUST go through the blockUser Cloud Function. 207 console.warn('[user_inbox.js] toggleBlockUser() disabled. Use blockUser CF via ho-so.js instead.'); 208 if (window.showToast) { 209 window.showToast('TÃnh nÄng nà y Äã ÄÆ°á»£c nâng cấp. Vui lòng truy cáºp Há» SÆ¡ cá»§a Äạo hữu Äá» thá»±c hiá»n.', 'info'); 210 } 211}; 212 213window.showChatInput = () => 214 (document.getElementById("chat-input-area").style.display = "block"); 215 216window.sendMessageToUser = async () => { 217 // [PHASE 3 â 2026-04-26] Legacy DM DISABLED. 218 // Luá»ng cÅ© ghi trá»±c tiếp và o /users/{uid}/inbox â Äã bá» lá»i bảo máºt (SEC-05). 219 // Rule allow create: if request.auth.uid == userId chặn ghi và o inbox cá»§a NGƯá»I KHÃC. 220 // Thay bằng chat-dock.js openChat() (Firestore /chats/{chatId} + realtime). 221 console.warn('[user_inbox.js] sendMessageToUser() Äã bá» vô hiá»u hóa. Dùng window._chatDock.openChat() thay thế.'); 222 223 const user = auth.currentUser; 224 if (!user || !foundUserUid) return; 225 226 // Äóng modal social cÅ© nếu Äang má» 227 const modal = document.getElementById('socialModal'); 228 if (modal) modal.style.display = 'none'; 229 230 // Redirect sang Chat Dock (openChat tá»± xá» lý block check) 231 if (window._chatDock && window._chatDock.openChat) { 232 const name = window._friendsDataCache?.[foundUserUid]?.name || ''; 233 const avatar = window._friendsDataCache?.[foundUserUid]?.avatar || ''; 234 window._chatDock.openChat(foundUserUid, name, avatar); 235 } else { 236 if (window.showToast) window.showToast('Há» thá»ng chat chưa khá»i tạo. Thá» lại sau và i giây.', 'info'); 237 } 238}; 239 240// [Äà DI CHUYá»N]: Logic Äếm tin nhắn badge ÄÆ°á»£c gá»p và o initInboxBadge trong main.js 241// Äá» luôn chạy khi load trang, không phụ thuá»c và o lazy-load cá»§a module nà y. 242 243window.toggleInboxPopup = async () => { 244 const popup = document.getElementById("inbox-popup"); 245 const authUser = window.YTT_currentUser || auth.currentUser; 246 247 if (!popup) return; 248 if (!authUser) { 249 if (window.showToast) window.showToast("Bạn cần ÄÄng nháºp Äá» xem há»p thư!", "error"); 250 return; 251 } 252 253 // Nếu Äang má» thì Äóng 254 if (popup.classList.contains("show")) { 255 popup.classList.remove("show"); 256 return; 257 } 258 259 // Äóng chéo các panel khác 260 if (window.closeAllPanels) window.closeAllPanels('inbox'); 261 popup.classList.add("show"); 262 263 const listDiv = document.getElementById("inbox-list"); 264 listDiv.innerHTML = ` 265 <div style="padding: 20px; text-align: center;"> 266 <div style="height:40px; border-radius:8px; margin-bottom:10px; background:#333;"></div> 267 <div style="height:40px; border-radius:8px; margin-bottom:10px; background:#333;"></div> 268 <div style="height:40px; border-radius:8px; display:inline-block; width:100%; background:#333;"></div> 269 </div>`; 270 271 try { 272 const q = query( 273 collection(db, "users", authUser.uid, "inbox"), 274 orderBy("timestamp", "desc"), 275 limit(20) 276 ); 277 const snapshot = await getDocs(q); 278 279 // Lá»c thá»§ công (do Firestore không há» trợ OR trên 1 field khác field orderBy tá»t) 280 const validDocs = []; 281 snapshot.forEach((docSnap) => { 282 const data = docSnap.data(); 283 // [BUG #3 FIX]: CHá» hiá»n thá» tin nhắn private_message, bá» qua doc cÅ© không có type 284 if (data.type === 'private_message') { 285 validDocs.push({ id: docSnap.id, ...data }); 286 } 287 });
288 289 if (validDocs.length === 0) { 290 listDiv.innerHTML = '<div style="padding: 20px; text-align: center; color: #888;">Chưa có tin nhắn nà o...</div>'; 291 return; 292 } 293 294 let html = ""; 295 for (const docData of validDocs) { 296 const time = new Date(docData.timestamp).toLocaleString("vi-VN"); 297 const isUnread = docData.read === false; 298 const safeContent = window.DOMPurify ? window.DOMPurify.sanitize(docData.content) : docData.content; 299 const safeName = window.DOMPurify ? window.DOMPurify.sanitize(docData.fromName) : docData.fromName; 300 const safeAvatar = window.DOMPurify ? window.DOMPurify.sanitize(docData.fromAvatar) : docData.fromAvatar; 301 302 if (isUnread) { 303 updateDoc(doc(db, "users", authUser.uid, "inbox", docData.id), { read: true }).catch(e => console.warn(e)); 304 } 305 306 html += ` 307 <div style="background:#1e1e1e; padding:15px; border-radius:10px; margin-bottom:10px; display:flex; gap:15px; border-left: 4px solid ${isUnread ? "#e91e63" : "#444"};"> 308 <img src="${safeAvatar}" style="width:45px; height:45px; border-radius:50%; object-fit:cover;"> 309 <div style="flex:1;"> 310 <div style="display:flex; justify-content:space-between;"> 311 <strong style="color:#4fc3f7; font-size:0.9rem;">${safeName}</strong> 312 <span style="font-size:0.7rem; color:#888;">${time}</span> 313 </div> 314 <div style="color:#ddd; margin-top:5px; font-size:0.95rem;">${safeContent}</div> 315 <button class="btn-reply" data-reply-uid="${docData.fromId}" data-reply-name="${safeName}" data-reply-avatar="${safeAvatar}"><i class="fas fa-reply"></i> Phản há»i</button> 316 </div> 317 </div>`; 318 } 319 listDiv.innerHTML = html; 320 } catch (error) { 321 console.warn("Lá»i tải há»p thư:", error); 322 listDiv.innerHTML = '<div style="color:#e91e63; text-align:center; padding:20px;"><i class="fas fa-exclamation-triangle"></i> Lá»i kết ná»i! Vui lòng thá» lại.</div>'; 323 } 324}; 325 326window.replyToUser = (targetUid, targetName, targetAvatar) => { 327 // Äóng Inbox Popup 328 const popup = document.getElementById("inbox-popup"); 329 if (popup) popup.classList.remove("show"); 330 331 foundUserUid = targetUid; 332 window.openSocialModal(); 333 document.getElementById("search-result").style.display = "block"; 334 document.getElementById("chat-input-area").style.display = "block"; 335 // Dùng textContent thay innerText (an toà n XSS â DOMPurify Äã sanitize trưá»c) 336 document.getElementById("s-name").textContent = targetName; 337 document.getElementById("s-avatar").src = targetAvatar; 338 document.getElementById("s-rank").textContent = "Äang phản há»i..."; 339 document.getElementById("s-bio").textContent = "Nháºp tin nhắn phản há»i bên dưá»i."; 340}; 341 342window.markAllInboxRead = async () => { 343 const user = window.YTT_currentUser || auth.currentUser; 344 if (!user) return; 345 346 try { 347 const q = query( 348 collection(db, "users", user.uid, "inbox"), 349 where("read", "==", false) 350 ); 351 const snapshot = await getDocs(q); 352 const batchUpdates = []; 353 snapshot.forEach(docSnap => { 354 batchUpdates.push(updateDoc(doc(db, "users", user.uid, "inbox", docSnap.id), { read: true })); 355 }); 356 await Promise.all(batchUpdates); 357 358 // Cáºp nháºt giao diá»n bên trong inbox popup 359 const inboxes = document.querySelectorAll('#inbox-list > div[style*="border-left"]'); 360 inboxes.forEach(div => { 361 div.style.borderLeftColor = "#444"; 362 }); 363 if (window.showToast) window.showToast("Äã Äánh dấu Äã Äá»c tất cả", "success"); 364 } catch (err) { 365 console.warn('Lá»i Äánh dấu Äã Äá»c:', err); 366 } 367}; 368 369 370// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 371// EVENT DELEGATION (INBOX-SPECIFIC) â Chá» xá» lý nút Reply
372// Lưu ý: [data-uid] và [data-action="openInbox"] Äã ÄÆ°á»£c 373// chuyá»n sang main.js Äá» chạy TRƯá»C khi module nà y lazy-load. 374// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 375document.addEventListener("click", (e) => { 376 // Nút Reply trong Inbox (data-reply-uid) 377 const replyBtn = e.target.closest('[data-reply-uid]'); 378 if (replyBtn) { 379 e.preventDefault(); 380 const uid = replyBtn.dataset.replyUid; 381 const name = replyBtn.dataset.replyName || "Äạo Hữu"; 382 const avatar = replyBtn.dataset.replyAvatar || "https://ui-avatars.com/api/?name=User&background=random"; 383 window.replyToUser(uid, name, avatar); 384 } 385}); 386 387// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 388// [SOCIAL V3] Các hà m chat cÅ© (Äá»nh nghÄ©a) Äã ÄÆ°á»£c THÃO Bá» á» Phase 3. 389// Logic chat má»i nằm trong js/chat-dock.js (window._chatDock.openChat). 390�
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.