PageSourceSearch

https://yeu-tiengtrung.com/js/user_inbox.js?v=23.59

js yeu-tiengtrung.com collected 2026-09-24 14:58:22 UTC 17,547 bytes, 390 lines download raw bytes

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.