782 lines
29 KiB
JavaScript
782 lines
29 KiB
JavaScript
|
|
let contacts = [];
|
|
let activeOccupantId = null;
|
|
let activeAvatarUrl = '';
|
|
|
|
function formatTime(dateStr) {
|
|
if(!dateStr) return '';
|
|
const date = new Date(dateStr);
|
|
let hours = date.getHours();
|
|
let minutes = date.getMinutes();
|
|
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
hours = hours % 12;
|
|
hours = hours ? hours : 12;
|
|
minutes = minutes < 10 ? '0'+minutes : minutes;
|
|
return hours + ':' + minutes + ' ' + ampm;
|
|
}
|
|
|
|
function formatRelative(dateStr) {
|
|
if(!dateStr) return '';
|
|
const date = new Date(dateStr);
|
|
const now = new Date();
|
|
const diffMs = now - date;
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
|
|
if(diffMins < 60) return diffMins + ' Min';
|
|
const diffHours = Math.floor(diffMins / 60);
|
|
if(diffHours < 24) return diffHours + ' Hour';
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
return diffDays + ' Day';
|
|
}
|
|
|
|
function loadContacts() {
|
|
$.post('chat_api.php', { action: 'get_contacts' }, function(res) {
|
|
if(res.status === 'success') {
|
|
contacts = res.data;
|
|
renderContacts(contacts);
|
|
}
|
|
}, 'json');
|
|
}
|
|
|
|
function renderContacts(data) {
|
|
let chatsHtml = '';
|
|
let contactsHtml = '';
|
|
|
|
const nowServer = new Date();
|
|
|
|
data.forEach(c => {
|
|
let activeClass = (c.id_occupant == activeOccupantId) ? 'active' : '';
|
|
|
|
// Cek jika pesan berisi media sebelumnya dicadangkan sbg teks dummy
|
|
let rawLastMsg = c.last_message || '';
|
|
let strippedMsg = rawLastMsg.replace(/<[^>]*>?/gm, '').trim();
|
|
if(!strippedMsg && rawLastMsg.includes('<img')) strippedMsg = '📷 Foto';
|
|
if(!strippedMsg && rawLastMsg.includes('mdi-file-document')) strippedMsg = '📄 Dokumen';
|
|
if(!strippedMsg && rawLastMsg.includes('<video')) strippedMsg = '🎥 Video';
|
|
if(!strippedMsg && rawLastMsg.includes('<audio')) strippedMsg = '🎵 Audio';
|
|
|
|
let lastMsg = strippedMsg || (rawLastMsg ? 'Lampiran' : 'Start a conversation');
|
|
|
|
let timeStr = c.last_time ? formatRelative(c.last_time) : '';
|
|
|
|
let isOnline = false;
|
|
if (c.last_active) {
|
|
let lastActStr = c.last_active.replace(/-/g, '/');
|
|
let actDate = new Date(lastActStr);
|
|
let diffSec = (nowServer - actDate) / 1000;
|
|
if (diffSec < 20) {
|
|
isOnline = true;
|
|
}
|
|
}
|
|
let statusColor = isOnline ? '#10b981' : '#f59e0b';
|
|
|
|
let unreadBadge = '';
|
|
let nameStyle = 'font-weight: 600;';
|
|
let msgStyle = '';
|
|
if (c.unread_count > 0 && c.id_occupant != activeOccupantId) {
|
|
unreadBadge = `<span class="ci-unread-badge">${c.unread_count}</span>`;
|
|
nameStyle = 'font-weight: 800; color: #000;';
|
|
msgStyle = 'font-weight: 700; color: #222;';
|
|
}
|
|
|
|
let html = `
|
|
<div class="contact-item ${activeClass}" onclick="openChat(${c.id_occupant})">
|
|
<div class="ci-avatar">
|
|
<img src="${c.foto}" alt="${c.name}">
|
|
<div class="ci-status" style="background:${statusColor}"></div>
|
|
</div>
|
|
<div class="ci-info">
|
|
<div class="ci-header">
|
|
<div class="ci-name" style="${nameStyle}">${c.name}${unreadBadge}</div>
|
|
<div class="ci-time">${timeStr}</div>
|
|
</div>
|
|
<div class="ci-msg" style="${msgStyle}">${lastMsg}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
if(c.last_time) {
|
|
chatsHtml += html;
|
|
} else {
|
|
contactsHtml += html;
|
|
}
|
|
});
|
|
|
|
if(chatsHtml) {
|
|
document.getElementById('chatsList').innerHTML = chatsHtml;
|
|
document.getElementById('chatsSection').style.display = 'block';
|
|
} else {
|
|
document.getElementById('chatsSection').style.display = 'none';
|
|
}
|
|
|
|
if(contactsHtml) {
|
|
document.getElementById('contactsList').innerHTML = contactsHtml;
|
|
document.getElementById('contactsSection').style.display = 'block';
|
|
} else {
|
|
document.getElementById('contactsSection').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function filterContacts() {
|
|
let q = document.getElementById('searchInput').value.toLowerCase();
|
|
let filtered = contacts.filter(c => c.name.toLowerCase().includes(q));
|
|
renderContacts(filtered);
|
|
}
|
|
|
|
function openChat(id) {
|
|
activeOccupantId = id;
|
|
renderContacts(contacts);
|
|
|
|
document.getElementById('chatBlank').style.display = 'none';
|
|
document.getElementById('chatActive').style.display = 'flex';
|
|
document.getElementById('chatHistory').innerHTML = '<div style="text-align:center; padding:20px; color:#999;">Loading...</div>';
|
|
|
|
$.post('chat_api.php', { action: 'get_messages', id_occupant: id }, function(res) {
|
|
if(res.status === 'success') {
|
|
document.getElementById('activeName').innerText = res.occupant.name;
|
|
let occStatStr = 'Status tidak diketahui';
|
|
if (res.occupant.last_active) {
|
|
let lastActStr = res.occupant.last_active.replace(/-/g, '/');
|
|
let actDate = new Date(lastActStr);
|
|
let diffSec = (new Date() - actDate) / 1000;
|
|
let diffDays = diffSec / (60 * 60 * 24);
|
|
|
|
if (diffSec < 20) {
|
|
occStatStr = 'Online';
|
|
} else if (diffDays > 7) {
|
|
let jam = actDate.getHours().toString().padStart(2, '0');
|
|
let menit = actDate.getMinutes().toString().padStart(2, '0');
|
|
let tgl = actDate.getDate().toString().padStart(2, '0');
|
|
let bln = (actDate.getMonth() + 1).toString().padStart(2, '0');
|
|
let thn = actDate.getFullYear();
|
|
occStatStr = `Terakhir Online ${jam}.${menit} ${tgl}-${bln}-${thn}`;
|
|
} else {
|
|
const hariArr = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
|
|
let namaHari = hariArr[actDate.getDay()];
|
|
let jam = actDate.getHours().toString().padStart(2, '0');
|
|
let menit = actDate.getMinutes().toString().padStart(2, '0');
|
|
occStatStr = `Terakhir Online Jam ${jam}:${menit} Hari ${namaHari}`;
|
|
}
|
|
}
|
|
document.getElementById('activeStatus').innerText = occStatStr;
|
|
document.getElementById('activeAvatar').src = res.occupant.foto;
|
|
activeAvatarUrl = res.occupant.foto;
|
|
|
|
renderMessages(res.data);
|
|
loadContacts();
|
|
}
|
|
}, 'json');
|
|
}
|
|
|
|
function buildMessagesHtml(messages) {
|
|
let html = '';
|
|
const adminAvatar = 'images/admin.png';
|
|
|
|
messages.forEach(msg => {
|
|
let isRight = (msg.sender === 'account');
|
|
let cls = isRight ? 'msg-right' : 'msg-left';
|
|
|
|
let statusHtml = '';
|
|
if (isRight) {
|
|
if (msg.is_read == 1) {
|
|
statusHtml = '<span class="msg-status msg-status-read"><i class="mdi mdi-check-all"></i></span>';
|
|
} else if (msg.telegram_msg_id) {
|
|
statusHtml = '<span class="msg-status msg-status-delivered"><i class="mdi mdi-check-all"></i></span>';
|
|
} else {
|
|
statusHtml = '<span class="msg-status msg-status-sent"><i class="mdi mdi-check"></i></span>';
|
|
}
|
|
}
|
|
let avatarToUse = isRight ? adminAvatar : activeAvatarUrl;
|
|
let onContextMenu = `oncontextmenu="showContextMenu(event, ${msg.id_chat}, '${msg.sender}', this); return false;"`;
|
|
|
|
let replyBlock = '';
|
|
if (msg.reply_to_chat_id && msg.reply_message) {
|
|
let rpSender = (msg.reply_sender === 'account') ? 'Anda' : document.getElementById('activeName').innerText;
|
|
let strippedRpMsg = msg.reply_message.replace(/<[^>]*>?/gm, '');
|
|
if(!strippedRpMsg) strippedRpMsg = 'Lampiran/File';
|
|
replyBlock = `<div class="msg-reply-block" onclick="scrollToMsg(${msg.reply_to_chat_id})">` +
|
|
`<div class="replier">${rpSender}</div>` +
|
|
`<div style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:200px;">${strippedRpMsg}</div>` +
|
|
`</div>`;
|
|
}
|
|
|
|
let msgOutput = msg.message;
|
|
let timeOutput = `<div class="msg-time">${formatTime(msg.created_at)} ${statusHtml}</div>`;
|
|
let bubbleClass = 'msg-bubble';
|
|
|
|
// Additional Logic for Selection Mode
|
|
let selectionClasses = '';
|
|
if (isSelectionMode) {
|
|
selectionClasses = 'selectable';
|
|
if (selectedMessages.some(sm => sm.id === msg.id_chat)) selectionClasses += ' selected';
|
|
}
|
|
let checkboxHtml = `<div class="msg-checkbox" onclick="toggleSelectMessage(event, ${msg.id_chat}, '${msg.sender}')"><i class="mdi mdi-checkbox-blank-circle-outline"></i></div>`;
|
|
let msgRowClick = isSelectionMode ? `onclick="toggleSelectMessage(event, ${msg.id_chat}, '${msg.sender}')"` : '';
|
|
|
|
html += `
|
|
<div class="msg-row ${cls} ${selectionClasses}" id="msg-${msg.id_chat}" data-id="${msg.id_chat}" ${msgRowClick}>
|
|
${checkboxHtml}
|
|
<img src="${avatarToUse}">
|
|
<div class="msg-content">
|
|
<div class="${bubbleClass}" ${onContextMenu}>${replyBlock}${msgOutput}</div>
|
|
${timeOutput}
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
return html;
|
|
}
|
|
|
|
function renderMessages(messages) {
|
|
let hist = document.getElementById('chatHistory');
|
|
let newHtml = buildMessagesHtml(messages);
|
|
|
|
if (hist.innerHTML !== newHtml) {
|
|
let isScrolledBottom = hist.scrollHeight - hist.clientHeight <= hist.scrollTop + 10;
|
|
hist.innerHTML = newHtml;
|
|
if (isScrolledBottom || hist.scrollTop === 0) {
|
|
scrollToBottom();
|
|
}
|
|
}
|
|
}
|
|
|
|
function scrollToBottom() {
|
|
let hist = document.getElementById('chatHistory');
|
|
hist.scrollTop = hist.scrollHeight;
|
|
}
|
|
|
|
function scrollToMsg(id) {
|
|
let el = document.getElementById('msg-' + id);
|
|
if(el) {
|
|
el.scrollIntoView({behavior: "smooth", block: "center"});
|
|
el.style.backgroundColor = 'rgba(88, 86, 214, 0.2)';
|
|
setTimeout(() => { el.style.backgroundColor = 'transparent'; }, 1000);
|
|
}
|
|
}
|
|
|
|
function handleEnter(e) {
|
|
if(e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault(); // Mencegah pindah baris
|
|
submitMessageOrEdit();
|
|
}
|
|
}
|
|
|
|
// Auto-resize textarea
|
|
document.addEventListener('input', function (e) {
|
|
if (e.target.id === 'messageInput') {
|
|
e.target.style.height = 'auto'; // Reset height
|
|
e.target.style.height = (e.target.scrollHeight) + 'px'; // Set to scroll height
|
|
}
|
|
});
|
|
|
|
function submitMessageOrEdit() {
|
|
if (editingMsgId) {
|
|
saveEdit();
|
|
} else {
|
|
sendMessage();
|
|
}
|
|
}
|
|
|
|
function sendMessage() {
|
|
let input = document.getElementById('messageInput');
|
|
let text = input.value.trim();
|
|
if(!text || !activeOccupantId) return;
|
|
|
|
let formData = new FormData();
|
|
formData.append('action', 'send_message');
|
|
formData.append('id_occupant', activeOccupantId);
|
|
formData.append('message', text);
|
|
formData.append('sender', 'account');
|
|
|
|
if (replyingToId) formData.append('reply_to_chat_id', replyingToId);
|
|
|
|
let tempId = 'temp-' + Date.now();
|
|
let optStatus = '<span class="msg-status msg-status-sending"><i class="mdi mdi-clock-outline"></i></span>';
|
|
let timeOutput = `<div class="msg-time">Mengirim... ${optStatus}</div>`;
|
|
|
|
let hist = document.getElementById('chatHistory');
|
|
let tempRow = document.createElement('div');
|
|
tempRow.className = 'msg-row msg-right msg-sending';
|
|
tempRow.id = tempId;
|
|
let escapedText = text.replace(/</g, '<').replace(/>/g, '>');
|
|
tempRow.innerHTML = `
|
|
<img src="images/admin.png">
|
|
<div class="msg-content">
|
|
<div class="msg-bubble">${escapedText}</div>
|
|
${timeOutput}
|
|
</div>
|
|
`;
|
|
hist.appendChild(tempRow);
|
|
scrollToBottom();
|
|
|
|
input.value = '';
|
|
input.style.height = 'auto'; // Reset textarea height
|
|
closeReplyBox();
|
|
|
|
$.ajax({
|
|
url: 'chat_api.php',
|
|
type: 'POST',
|
|
data: formData,
|
|
contentType: false,
|
|
processData: false,
|
|
dataType: 'json',
|
|
success: function(res) {
|
|
if(res.status === 'success') {
|
|
loadContacts();
|
|
$.post('chat_api.php', { action: 'get_messages', id_occupant: activeOccupantId }, function(res2) {
|
|
if(res2.status === 'success') {
|
|
renderMessages(res2.data);
|
|
}
|
|
}, 'json');
|
|
} else {
|
|
let el = document.getElementById(tempId);
|
|
if (el) {
|
|
el.querySelector('.msg-status').innerHTML = '<i class="mdi mdi-alert-circle" style="color:#ef4444;font-size:14px;" title="Gagal terkirim"></i>';
|
|
el.classList.remove('msg-sending');
|
|
}
|
|
}
|
|
},
|
|
error: function() {
|
|
let el = document.getElementById(tempId);
|
|
if (el) {
|
|
el.querySelector('.msg-status').innerHTML = '<i class="mdi mdi-alert-circle" style="color:#ef4444;font-size:14px;" title="Gagal terkirim"></i>';
|
|
el.classList.remove('msg-sending');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// ═══ EMOJI PICKER ═══
|
|
const emojiData = {
|
|
smileys: ['😀','😃','😄','😁','😆','😅','🤣','😂','🙂','😊','😇','🥰','😍','🤩','😘','😋','😛','😜','🤪','😝','🤑','🤗','🤭','🤫','🤔','🤐','🤨','😐','😑','😶','😏','😒','🙄','😬','😮💨','🤥','😌','😔','😪','🤤','😴','😷','🤒','🤕','🤢','🤮','🥵','🥶','😵','🤯','🥳','🥸','😎','🤓','🧐'],
|
|
gestures: ['👍','👎','👌','🤌','🤏','✌️','🤞','🤟','🤘','🤙','👈','👉','👆','👇','☝️','✋','🤚','🖐️','🖖','👋','🤝','👏','✍️','🙏','💪','🦾','🫶','🫱','🫲'],
|
|
hearts: ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','🤎','💔','❤️🔥','❤️🩹','💕','💞','💓','💗','💖','💘','💝','💟','♥️','🫀'],
|
|
objects: ['🎉','🎊','🎈','🎁','🎀','🏆','🥇','🎵','🎶','🎸','🎹','🎤','🔔','⭐','🌟','💫','✨','⚡','🔥','💥','🎯','💡','📌','📎','✅','❌','⚠️','💬','💭','🗨️'],
|
|
animals: ['🐶','🐱','🐭','🐹','🐰','🦊','🐻','🐼','🐨','🐯','🦁','🐮','🐷','🐸','🐵','🐔','🐧','🐦','🦅','🦉','🐴','🦄','🐝','🐛','🦋','🐌','🐞','🐙','🐠','🐳'],
|
|
food: ['🍕','🍔','🍟','🌭','🍿','🧀','🥚','🍳','🥞','🧇','🥓','🥩','🍗','🍖','🌮','🌯','🥗','🍜','🍝','🍣','🍤','🍩','🍪','🎂','🍰','🧁','🍫','🍬','🍭','☕','🍵','🥤','🧃','🍷','🍺']
|
|
};
|
|
|
|
function toggleEmoji() {
|
|
let picker = document.getElementById('emojiPicker');
|
|
picker.classList.toggle('show');
|
|
if (picker.classList.contains('show')) renderEmojis('smileys');
|
|
}
|
|
function renderEmojis(cat) {
|
|
let grid = document.getElementById('emojiGrid');
|
|
grid.innerHTML = emojiData[cat].map(e => `<div class="emoji-item" onclick="insertEmoji('${e}')">${e}</div>`).join('');
|
|
}
|
|
function insertEmoji(e) {
|
|
let input = document.getElementById('messageInput');
|
|
input.value += e;
|
|
input.style.height = 'auto';
|
|
input.style.height = (input.scrollHeight) + 'px';
|
|
input.focus();
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if(e.target.classList.contains('emoji-tab')) {
|
|
document.querySelectorAll('.emoji-tab').forEach(t => t.classList.remove('active'));
|
|
e.target.classList.add('active');
|
|
renderEmojis(e.target.dataset.cat);
|
|
}
|
|
|
|
let picker = document.getElementById('emojiPicker');
|
|
let btn = document.getElementById('btnEmoji');
|
|
if (picker && btn && picker.classList.contains('show') && !picker.contains(e.target) && !btn.contains(e.target)) {
|
|
picker.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
|
|
// Context Menu Logic
|
|
let selectedMsgId = null;
|
|
let selectedMsgSender = null;
|
|
let selectedMsgText = null;
|
|
let replyingToId = null;
|
|
let editingMsgId = null;
|
|
let originalEditMsgText = null;
|
|
|
|
// Selection Mode State
|
|
let isSelectionMode = false;
|
|
let selectedMessages = []; // Array of objects: {id, sender}
|
|
|
|
function showContextMenu(e, idChat, sender, bubbleEl) {
|
|
e.preventDefault();
|
|
selectedMsgId = idChat;
|
|
selectedMsgSender = sender;
|
|
|
|
// Extract text ignoring the reply block and the (Diedit) tag
|
|
let clone = bubbleEl.cloneNode(true);
|
|
let replyBlocks = clone.querySelectorAll('.msg-reply-block');
|
|
replyBlocks.forEach(b => b.remove());
|
|
let editTags = clone.querySelectorAll('i'); // Removes the (Diedit) tag along with other icons
|
|
editTags.forEach(t => t.remove());
|
|
selectedMsgText = (clone.textContent || '').trim();
|
|
|
|
let menu = document.getElementById('msgContextMenu');
|
|
|
|
// Show/hide Edit button based on sender
|
|
let btnEdit = document.getElementById('ctxMenuEdit');
|
|
if (sender === 'account') {
|
|
btnEdit.style.display = 'flex';
|
|
} else {
|
|
btnEdit.style.display = 'none';
|
|
}
|
|
|
|
let x = e.clientX;
|
|
let y = e.clientY;
|
|
if (x + 180 > window.innerWidth) x = window.innerWidth - 190;
|
|
if (y + 130 > window.innerHeight) y = window.innerHeight - 130;
|
|
menu.style.left = x + 'px';
|
|
menu.style.top = y + 'px';
|
|
menu.style.display = 'block';
|
|
}
|
|
|
|
function prepareReply(event) {
|
|
if(event) event.stopPropagation();
|
|
replyingToId = selectedMsgId;
|
|
let title = (selectedMsgSender === 'account') ? 'Membalas Anda' : ('Membalas ' + document.getElementById('activeName').innerText);
|
|
document.getElementById('rpTitle').innerText = title;
|
|
document.getElementById('rpText').innerText = selectedMsgText;
|
|
document.getElementById('replyPreview').style.display = 'block';
|
|
document.getElementById('messageInput').focus();
|
|
hideContextMenu();
|
|
}
|
|
|
|
function closeReplyBox() {
|
|
replyingToId = null;
|
|
document.getElementById('replyPreview').style.display = 'none';
|
|
}
|
|
|
|
function hideContextMenu() {
|
|
document.getElementById('msgContextMenu').style.display = 'none';
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
hideContextMenu();
|
|
});
|
|
|
|
function deleteSelectedMessage(event) {
|
|
if(event) event.stopPropagation();
|
|
hideContextMenu();
|
|
if (!selectedMsgId) return;
|
|
|
|
let msgElement = document.getElementById('msg-' + selectedMsgId);
|
|
|
|
if (selectedMsgSender === 'account') {
|
|
// Pesan Admin: Ada pilihan Hapus untuk Semua atau Hanya Saya
|
|
Swal.fire({
|
|
title: 'Hapus Pesan?',
|
|
text: "Pilih metode penghapusan.",
|
|
icon: 'warning',
|
|
showDenyButton: true,
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Hapus utk Semua',
|
|
denyButtonText: 'Hapus utk Saya',
|
|
cancelButtonText: 'Batal',
|
|
customClass: {
|
|
popup: 'swal2-custom-small',
|
|
confirmButton: 'swal-btn-delete-all',
|
|
denyButton: 'swal-btn-delete-me',
|
|
cancelButton: 'swal-btn-cancel'
|
|
},
|
|
buttonsStyling: false
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
// Semua
|
|
executeDelete(selectedMsgId, msgElement, 'everyone');
|
|
} else if (result.isDenied) {
|
|
// Saya
|
|
executeDelete(selectedMsgId, msgElement, 'me');
|
|
}
|
|
});
|
|
} else {
|
|
// Pesan Occupant: Hanya bisa Hapus untuk Saya
|
|
Swal.fire({
|
|
title: 'Hapus Pesan?',
|
|
text: "Pesan ini hanya terhapus pada layar Anda.",
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Hapus utk Saya',
|
|
cancelButtonText: 'Batal',
|
|
customClass: {
|
|
popup: 'swal2-custom-small',
|
|
confirmButton: 'swal-btn-delete-me',
|
|
cancelButton: 'swal-btn-cancel'
|
|
},
|
|
buttonsStyling: false
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
executeDelete(selectedMsgId, msgElement, 'me');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function executeDelete(id, msgElement, type) {
|
|
if (msgElement) msgElement.style.opacity = '0.5';
|
|
$.post('chat_api.php', { action: 'delete_message', id_chat: id, delete_type: type }, function(res) {
|
|
if(res.status === 'success') {
|
|
$('#msg-' + id).fadeOut(300, function() { $(this).remove(); });
|
|
} else {
|
|
if (msgElement) msgElement.style.opacity = '1';
|
|
Swal.fire('Gagal', res.message || 'Pesan tidak dapat dihapus', 'error');
|
|
}
|
|
}, 'json');
|
|
}
|
|
|
|
// ========================
|
|
// BULK DELETE & SELECTION
|
|
// ========================
|
|
|
|
function enterSelectionMode(event) {
|
|
if(event) event.stopPropagation();
|
|
hideContextMenu();
|
|
|
|
isSelectionMode = true;
|
|
selectedMessages = [{ id: selectedMsgId, sender: selectedMsgSender }];
|
|
|
|
// Stop editing/replying if active
|
|
cancelEdit();
|
|
closeReplyBox();
|
|
|
|
// Switch header actions
|
|
document.getElementById('normalHeaderActions').style.display = 'none';
|
|
document.getElementById('selectionHeaderActions').style.display = 'flex';
|
|
document.getElementById('selectionCount').innerText = selectedMessages.length;
|
|
|
|
// Add selectable classes to all rows and selected to the first one
|
|
let rows = document.querySelectorAll('.msg-row');
|
|
rows.forEach(row => {
|
|
row.classList.add('selectable');
|
|
// Add onclick to rows that are currently loaded
|
|
row.setAttribute('onclick', `toggleSelectMessage(event, ${row.getAttribute('data-id')}, '${row.classList.contains('msg-right') ? 'account' : 'occupant'}')`);
|
|
});
|
|
document.getElementById('msg-' + selectedMsgId).classList.add('selected');
|
|
}
|
|
|
|
function exitSelectionMode() {
|
|
isSelectionMode = false;
|
|
selectedMessages = [];
|
|
|
|
// Switch header actions back
|
|
document.getElementById('normalHeaderActions').style.display = 'block';
|
|
document.getElementById('selectionHeaderActions').style.display = 'none';
|
|
|
|
// Remove selectable classes
|
|
let rows = document.querySelectorAll('.msg-row');
|
|
rows.forEach(row => {
|
|
row.classList.remove('selectable', 'selected');
|
|
row.removeAttribute('onclick'); // Important: remove the bulk select onclick
|
|
});
|
|
}
|
|
|
|
function toggleSelectMessage(event, idChat, sender) {
|
|
if(event) event.stopPropagation();
|
|
if(!isSelectionMode) return;
|
|
|
|
let index = selectedMessages.findIndex(m => m.id === idChat);
|
|
let row = document.getElementById('msg-' + idChat);
|
|
|
|
if (index > -1) {
|
|
// Deselect
|
|
selectedMessages.splice(index, 1);
|
|
if(row) row.classList.remove('selected');
|
|
} else {
|
|
// Select
|
|
selectedMessages.push({ id: idChat, sender: sender });
|
|
if(row) row.classList.add('selected');
|
|
}
|
|
|
|
document.getElementById('selectionCount').innerText = selectedMessages.length;
|
|
|
|
// If no messages selected, automatically exit selection mode
|
|
if (selectedMessages.length === 0) {
|
|
exitSelectionMode();
|
|
}
|
|
}
|
|
|
|
function triggerBulkDelete() {
|
|
if (selectedMessages.length === 0) return;
|
|
|
|
// Check if ALL selected messages are from admin
|
|
let onlyAdminMessages = selectedMessages.every(m => m.sender === 'account');
|
|
|
|
if (onlyAdminMessages) {
|
|
// Admin only: Can choose 'Untuk Semua' or 'Hanya Saya'
|
|
Swal.fire({
|
|
title: 'Hapus ' + selectedMessages.length + ' Pesan?',
|
|
text: "Pilih metode penghapusan.",
|
|
icon: 'warning',
|
|
showDenyButton: true,
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Hapus utk Semua',
|
|
denyButtonText: 'Hapus utk Saya',
|
|
cancelButtonText: 'Batal',
|
|
customClass: {
|
|
popup: 'swal2-custom-small',
|
|
confirmButton: 'swal-btn-delete-all',
|
|
denyButton: 'swal-btn-delete-me',
|
|
cancelButton: 'swal-btn-cancel'
|
|
},
|
|
buttonsStyling: false
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
executeBulkDelete('everyone');
|
|
} else if (result.isDenied) {
|
|
executeBulkDelete('me');
|
|
}
|
|
});
|
|
} else {
|
|
// Mix of Admin + Occupant OR Occupant only: Only 'Hanya Saya'
|
|
Swal.fire({
|
|
title: 'Hapus ' + selectedMessages.length + ' Pesan?',
|
|
text: "Pesan-pesan ini hanya terhapus pada layar Anda.",
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Hapus',
|
|
cancelButtonText: 'Batal',
|
|
customClass: {
|
|
popup: 'swal2-custom-small',
|
|
confirmButton: 'swal-btn-delete-me',
|
|
cancelButton: 'swal-btn-cancel'
|
|
},
|
|
buttonsStyling: false
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
executeBulkDelete('me');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function executeBulkDelete(type) {
|
|
let idsToDelete = selectedMessages.map(m => m.id);
|
|
|
|
// Dim the UI immediately
|
|
idsToDelete.forEach(id => {
|
|
let el = document.getElementById('msg-' + id);
|
|
if(el) el.style.opacity = '0.5';
|
|
});
|
|
|
|
$.post('chat_api.php', {
|
|
action: 'bulk_delete_messages',
|
|
id_chats: idsToDelete,
|
|
delete_type: type
|
|
}, function(res) {
|
|
if(res.status === 'success') {
|
|
idsToDelete.forEach(id => {
|
|
$('#msg-' + id).fadeOut(300, function() { $(this).remove(); });
|
|
});
|
|
exitSelectionMode();
|
|
Swal.fire({
|
|
title: 'Terhapus!',
|
|
text: res.message,
|
|
icon: 'success',
|
|
timer: 1500,
|
|
showConfirmButton: false
|
|
});
|
|
} else {
|
|
// Revert opacity
|
|
idsToDelete.forEach(id => {
|
|
let el = document.getElementById('msg-' + id);
|
|
if(el) el.style.opacity = '1';
|
|
});
|
|
Swal.fire('Gagal', res.message || 'Gagal menghapus beberapa pesan', 'error');
|
|
}
|
|
}, 'json');
|
|
}
|
|
|
|
function editSelectedMessage(event) {
|
|
if(event) event.stopPropagation();
|
|
hideContextMenu();
|
|
if (!selectedMsgId || selectedMsgSender !== 'account') return;
|
|
|
|
editingMsgId = selectedMsgId;
|
|
originalEditMsgText = selectedMsgText;
|
|
|
|
// Set UI to Edit Mode
|
|
document.getElementById('epText').innerText = originalEditMsgText;
|
|
document.getElementById('editPreview').style.display = 'block';
|
|
|
|
// If user was replying, cancel it so they don't do both
|
|
closeReplyBox();
|
|
|
|
let inputEl = document.getElementById('messageInput');
|
|
inputEl.value = originalEditMsgText;
|
|
inputEl.style.height = 'auto';
|
|
inputEl.style.height = (inputEl.scrollHeight) + 'px';
|
|
inputEl.focus();
|
|
}
|
|
|
|
function cancelEdit() {
|
|
editingMsgId = null;
|
|
originalEditMsgText = null;
|
|
document.getElementById('editPreview').style.display = 'none';
|
|
let inputEl = document.getElementById('messageInput');
|
|
inputEl.value = '';
|
|
inputEl.style.height = 'auto';
|
|
}
|
|
|
|
function saveEdit() {
|
|
let input = document.getElementById('messageInput');
|
|
let newText = input.value.trim();
|
|
|
|
if (!newText || !editingMsgId) return;
|
|
if (newText === originalEditMsgText) {
|
|
cancelEdit();
|
|
return;
|
|
}
|
|
|
|
let msgElement = document.getElementById('msg-' + editingMsgId);
|
|
if (msgElement) msgElement.style.opacity = '0.5';
|
|
|
|
let idToEdit = editingMsgId;
|
|
cancelEdit(); // Reset UI immediately
|
|
|
|
$.post('chat_api.php', { action: 'edit_message', id_chat: idToEdit, new_message: newText }, function(res) {
|
|
if (msgElement) msgElement.style.opacity = '1';
|
|
|
|
if (res.status === 'success') {
|
|
// Reload messages to ensure formatting consistency
|
|
if (activeOccupantId) {
|
|
$.post('chat_api.php', { action: 'get_messages', id_occupant: activeOccupantId }, function(res2) {
|
|
if(res2.status === 'success') {
|
|
renderMessages(res2.data);
|
|
}
|
|
}, 'json');
|
|
}
|
|
} else {
|
|
Swal.fire('Gagal', res.message || 'Pesan tidak dapat diedit', 'error');
|
|
}
|
|
}, 'json');
|
|
}
|
|
|
|
// Initial load
|
|
$(document).ready(function() {
|
|
loadContacts();
|
|
renderEmojis('smileys');
|
|
setInterval(function(){
|
|
if(isSelectionMode) return; // DON'T REFRESH HTML WHILE USER IS SELECTING
|
|
if(!document.getElementById('searchInput').value) {
|
|
loadContacts();
|
|
if(activeOccupantId) {
|
|
$.post('chat_api.php', { action: 'get_messages', id_occupant: activeOccupantId }, function(res2) {
|
|
if(res2.status === 'success') {
|
|
let hist = document.getElementById('chatHistory');
|
|
let newHtml = buildMessagesHtml(res2.data);
|
|
if(hist.innerHTML !== newHtml) {
|
|
let isScrolledBottom = hist.scrollHeight - hist.clientHeight <= hist.scrollTop + 10;
|
|
// Update innerHTML instead of appending
|
|
hist.innerHTML = newHtml;
|
|
if(isScrolledBottom) scrollToBottom();
|
|
}
|
|
}
|
|
}, 'json');
|
|
}
|
|
}
|
|
}, 5000);
|
|
});
|
|
|