1753 lines
59 KiB
PHP
1753 lines
59 KiB
PHP
<?php include 'header.php'; ?>
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
<style>
|
|
/* CSS for Chat Interface */
|
|
.chat-wrapper {
|
|
display: flex;
|
|
height: 600px;
|
|
background: #fff;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
/* Custom Delete Modal */
|
|
.del-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.35); z-index:10000; align-items:center; justify-content:center; }
|
|
.del-overlay.show { display:flex; }
|
|
.del-box { background:#fff; border-radius:14px; width:260px; overflow:hidden; box-shadow:0 10px 40px rgba(0,0,0,0.18); animation: delPop 0.15s ease-out; }
|
|
@keyframes delPop { from { transform:scale(0.92); opacity:0; } to { transform:scale(1); opacity:1; } }
|
|
.del-title { padding:18px 20px 14px; text-align:center; font-size:14px; font-weight:700; color:#1a1a1a; border-bottom:1px solid #f0f0f0; }
|
|
.del-option { padding:12px 20px; text-align:center; font-size:13px; font-weight:500; cursor:pointer; transition:background 0.12s; border-bottom:1px solid #f0f0f0; }
|
|
.del-option:last-child { border-bottom:none; }
|
|
.del-option:hover { background:#f8f9fa; }
|
|
.del-option.del-red { color:#ef4444; }
|
|
.del-option.del-amber { color:#f59e0b; }
|
|
.del-option.del-cancel { color:#64748b; }
|
|
|
|
.chat-sidebar {
|
|
width: 340px;
|
|
border-right: 1px solid #f0f0f0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-shrink: 0;
|
|
}
|
|
.chat-main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: #fafbfe;
|
|
position: relative;
|
|
}
|
|
.chat-search-header {
|
|
padding: 18px 20px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
.chat-search-header img {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
}
|
|
.chat-search-box {
|
|
flex: 1;
|
|
position: relative;
|
|
}
|
|
.chat-search-box input {
|
|
width: 100%;
|
|
padding: 8px 16px 8px 36px;
|
|
border-radius: 20px;
|
|
border: 1px solid #e0e0e0;
|
|
font-size: 13px;
|
|
outline: none;
|
|
transition: 0.2s;
|
|
}
|
|
.chat-search-box input:focus {
|
|
border-color: #5856d6;
|
|
box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1);
|
|
}
|
|
.chat-search-box i {
|
|
position: absolute;
|
|
left: 12px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: #999;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.chat-list-wrapper {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
}
|
|
.chat-list-wrapper::-webkit-scrollbar { width: 6px; }
|
|
.chat-list-wrapper::-webkit-scrollbar-thumb { background: #ddd; border-radius: 4px; }
|
|
|
|
.section-title {
|
|
padding: 16px 20px 8px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #5856d6;
|
|
}
|
|
|
|
.contact-item {
|
|
display: flex;
|
|
padding: 12px 20px;
|
|
gap: 12px;
|
|
cursor: pointer;
|
|
transition: 0.2s;
|
|
align-items: center;
|
|
}
|
|
.contact-item:hover {
|
|
background: #f8faff;
|
|
}
|
|
.contact-item.active {
|
|
background: #5856d6;
|
|
}
|
|
.contact-item.active .ci-name { color: #fff; }
|
|
.contact-item.active .ci-msg { color: #e0e7ff; }
|
|
.contact-item.active .ci-time { color: #c7d2fe; }
|
|
|
|
.ci-avatar {
|
|
width: 42px;
|
|
height: 42px;
|
|
border-radius: 50%;
|
|
position: relative;
|
|
}
|
|
.ci-avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
}
|
|
.ci-status {
|
|
position: absolute;
|
|
bottom: 2px;
|
|
right: 2px;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: #10b981;
|
|
border: 2px solid #fff;
|
|
}
|
|
.contact-item.active .ci-status { border-color: #5856d6; }
|
|
|
|
.ci-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.ci-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
margin-bottom: 4px;
|
|
}
|
|
.ci-name {
|
|
font-size: 14px;
|
|
color: #333;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.ci-time {
|
|
font-size: 11px;
|
|
color: #999;
|
|
flex-shrink: 0;
|
|
}
|
|
.ci-msg {
|
|
font-size: 12px;
|
|
color: #666;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.ci-unread-badge {
|
|
background: #5856d6;
|
|
color: #fff;
|
|
font-size: 9px;
|
|
font-weight: bold;
|
|
padding: 1.5px 5px;
|
|
border-radius: 8px;
|
|
display: inline-block;
|
|
min-width: 14px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Right Blank View */
|
|
.chat-blank {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #666;
|
|
}
|
|
.chat-blank-icon {
|
|
width: 100px;
|
|
height: 100px;
|
|
border-radius: 50%;
|
|
background: #eff6ff;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
.chat-blank-icon i { font-size: 40px; color: #5856d6; }
|
|
|
|
/* Right Active View */
|
|
.chat-active-view {
|
|
display: none;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
.chat-header {
|
|
padding: 16px 24px;
|
|
background: #fff;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.ch-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
.ch-left img {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
}
|
|
.ch-name {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 2px;
|
|
}
|
|
.ch-status {
|
|
font-size: 12px;
|
|
color: #666;
|
|
}
|
|
.ch-right {
|
|
display: flex;
|
|
gap: 16px;
|
|
color: #666;
|
|
}
|
|
.ch-right i {
|
|
font-size: 18px;
|
|
cursor: pointer;
|
|
transition: 0.2s;
|
|
}
|
|
.ch-right i:hover { color: #5856d6; }
|
|
|
|
.chat-history {
|
|
flex: 1;
|
|
padding: 24px;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
.chat-history::-webkit-scrollbar { width: 6px; }
|
|
.chat-history::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px; }
|
|
|
|
.msg-row {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 8px;
|
|
max-width: 80%;
|
|
}
|
|
.msg-row img {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
flex-shrink: 0;
|
|
}
|
|
.msg-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.msg-bubble {
|
|
padding: 6px 7px 4px 9px;
|
|
border-radius: 7.5px;
|
|
font-size: 14.2px;
|
|
line-height: 19px;
|
|
box-shadow: 0 1px .5px rgba(11,20,26,.13);
|
|
position: relative;
|
|
color: #111b21;
|
|
word-break: break-all; /* Force long URLs/strings to break anywhere */
|
|
white-space: pre-wrap;
|
|
}
|
|
.msg-bubble p {
|
|
margin-bottom: 0;
|
|
margin-top: 0;
|
|
}
|
|
|
|
.msg-time {
|
|
font-size: 11px;
|
|
color: #667781;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
float: right;
|
|
margin-top: 4px;
|
|
margin-bottom: -4px;
|
|
margin-left: 12px;
|
|
}
|
|
|
|
/* WhatsApp-like message status */
|
|
.msg-status { display: inline-flex; align-items: center; margin-left: 3px; }
|
|
.msg-status .mdi { font-size: 15.5px; line-height: 1; margin-top: 1px;}
|
|
.msg-status-sending .mdi { color: #8696a0; animation: spinCheck 1s linear infinite; font-size: 13px;}
|
|
.msg-status-sent .mdi { color: #8696a0; }
|
|
.msg-status-delivered .mdi { color: #8696a0; }
|
|
.msg-status-read .mdi { color: #53bdeb; }
|
|
@keyframes spinCheck { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
|
|
/* Optimistic message (temporary) */
|
|
.msg-row.msg-sending { opacity: .7; }
|
|
.msg-row.msg-sending .msg-bubble { position: relative; }
|
|
|
|
/* Occupant Msg (Left) */
|
|
.msg-left {
|
|
align-self: flex-start;
|
|
}
|
|
.msg-left .msg-bubble {
|
|
background: #ffffff;
|
|
border-top-left-radius: 0;
|
|
}
|
|
.msg-left .msg-time { color: #667781; }
|
|
|
|
/* Admin Msg (Right) - Updated to Primary Color */
|
|
.msg-right {
|
|
align-self: flex-end;
|
|
flex-direction: row-reverse;
|
|
}
|
|
.msg-right .msg-bubble {
|
|
background: #5856d6;
|
|
color: #ffffff;
|
|
border-top-right-radius: 0;
|
|
}
|
|
.msg-right .msg-time { color: #667781; }
|
|
.msg-right .msg-status .mdi { color: #8696a0 !important; }
|
|
.msg-right .msg-status-read .mdi { color: #53bdeb !important; }
|
|
.edit-tag {
|
|
float: right !important;
|
|
margin-top: 2px !important;
|
|
margin-left: 8px !important;
|
|
font-size: 11.5px !important;
|
|
}
|
|
.msg-right .edit-tag { color: rgba(255,255,255,0.8) !important; }
|
|
.msg-left .edit-tag { color: #8696a0 !important; }
|
|
|
|
/* Chat Footer */
|
|
.chat-footer {
|
|
padding: 16px 24px;
|
|
background: #fff;
|
|
border-top: 1px solid #f0f0f0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.chat-footer-top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
width: 100%;
|
|
}
|
|
.reply-preview-box {
|
|
display: none;
|
|
width: 100%;
|
|
margin-bottom: 12px;
|
|
padding: 10px 14px;
|
|
background: #f8fafc;
|
|
border-left: 4px solid #5856d6;
|
|
border-radius: 6px;
|
|
position: relative;
|
|
font-size: 13px;
|
|
}
|
|
.rp-close {
|
|
position: absolute;
|
|
right: 12px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
cursor: pointer;
|
|
color: #94a3b8;
|
|
font-size: 18px;
|
|
}
|
|
.rp-close:hover { color: #ef4444; }
|
|
.rp-title { font-weight: bold; color: #5856d6; margin-bottom: 2px;}
|
|
.rp-text { color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 20px;}
|
|
|
|
.msg-reply-block {
|
|
background: rgba(0,0,0,0.05);
|
|
border-left: 3px solid #5856d6;
|
|
padding: 6px 10px;
|
|
border-radius: 4px;
|
|
margin-bottom: 6px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
}
|
|
.msg-reply-block .replier { font-weight: bold; color: #5856d6; }
|
|
.msg-right .msg-reply-block { background: rgba(255,255,255,0.2); border-color: #fff; }
|
|
.msg-right .msg-reply-block .replier { color: #fff; }
|
|
|
|
.chat-footer .input-wrapper {
|
|
flex: 1;
|
|
background: #f8fafc;
|
|
border-radius: 20px;
|
|
border: 1px solid #e2e8f0;
|
|
display: flex;
|
|
align-items: flex-end; /* Align to bottom for textarea growth */
|
|
padding: 8px 16px;
|
|
transition: 0.2s;
|
|
}
|
|
.chat-footer .input-wrapper:focus-within {
|
|
border-color: #5856d6;
|
|
background: #fff;
|
|
box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1);
|
|
}
|
|
.chat-footer textarea {
|
|
flex: 1;
|
|
border: none;
|
|
background: transparent;
|
|
outline: none;
|
|
padding: 8px;
|
|
font-size: 14px;
|
|
color: #333;
|
|
font-family: inherit;
|
|
resize: none;
|
|
max-height: 120px;
|
|
overflow-y: auto;
|
|
box-sizing: border-box;
|
|
/* Hide scrollbar for cleaner look */
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
}
|
|
.chat-footer textarea::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
#btnEmoji {
|
|
background: transparent;
|
|
border: none;
|
|
outline: none;
|
|
box-shadow: none;
|
|
color: #64748b;
|
|
font-size: 20px;
|
|
padding: 0;
|
|
margin: 0;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
#btnEmoji:hover {
|
|
color: #5856d6;
|
|
}
|
|
|
|
.btn-send {
|
|
background: #5856d6;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 8px;
|
|
padding: 10px 20px;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
transition: 0.2s;
|
|
box-shadow: none;
|
|
}
|
|
.btn-send:hover { background: #4b49ac; transform: translateY(-1px); }
|
|
|
|
/* Emoji Picker */
|
|
.emoji-picker {
|
|
display: none; position: absolute; bottom: 70px; right: 24px;
|
|
width: 320px; background: #fff; border-radius: 12px;
|
|
box-shadow: 0 8px 24px rgba(0,0,0,.12); border: 1px solid #edf2f7;
|
|
z-index: 100; overflow: hidden;
|
|
}
|
|
.emoji-picker.show { display: block; }
|
|
.emoji-tabs { display: flex; border-bottom: 1px solid #f1f5f9; padding: 0 4px; }
|
|
.emoji-tab {
|
|
flex: 1; padding: 8px 0; text-align: center; cursor: pointer;
|
|
font-size: 16px; opacity: .5; transition: .12s; border-bottom: 2px solid transparent;
|
|
}
|
|
.emoji-tab:hover { opacity: .8; }
|
|
.emoji-tab.active { opacity: 1; border-bottom-color: #5856d6; }
|
|
.emoji-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 2px; padding: 8px; max-height: 200px; overflow-y: auto; }
|
|
.emoji-grid::-webkit-scrollbar { width: 4px; }
|
|
.emoji-grid::-webkit-scrollbar-thumb { background: #ddd; border-radius: 4px; }
|
|
.emoji-item { font-size: 20px; text-align: center; padding: 6px; cursor: pointer; border-radius: 6px; transition: .1s; }
|
|
.emoji-item:hover { background: #f1f5f9; transform: scale(1.2); }
|
|
|
|
|
|
/* Custom Context Menu */
|
|
.chat-context-menu {
|
|
position: fixed; z-index: 9999; width: 180px; background: #fff;
|
|
border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.15);
|
|
padding: 6px 0; display: none; font-size: 13px;
|
|
border: 1px solid #edf2f7;
|
|
}
|
|
.chat-context-menu ul { list-style: none; padding: 0; margin: 0; }
|
|
.chat-context-menu li {
|
|
display: flex; align-items: center; gap: 10px; padding: 9px 16px;
|
|
cursor: pointer; transition: background .12s; color: #334155;
|
|
}
|
|
.chat-context-menu li:hover { background: #f1f5f9; }
|
|
.chat-context-menu li i { font-size: 16px; width: 18px; text-align: center; color: #64748b; }
|
|
.chat-context-menu li.ctx-danger { color: #ef4444; }
|
|
.chat-context-menu li.ctx-danger i { color: #ef4444; }
|
|
.chat-context-menu .ctx-sep { height: 1px; background: #f1f5f9; margin: 4px 0; }
|
|
|
|
/* Selection Mode */
|
|
.msg-checkbox {
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
font-size: 22px;
|
|
color: #94a3b8;
|
|
display: none;
|
|
cursor: pointer;
|
|
z-index: 10;
|
|
}
|
|
.msg-row { position: relative; transition: 0.2s; }
|
|
.msg-row.selectable {
|
|
cursor: pointer;
|
|
}
|
|
/* Admin Chat Checkbox */
|
|
.msg-right .msg-checkbox {
|
|
left: 15px; /* sedikit ditambah jaraknya */
|
|
}
|
|
.msg-right.selectable {
|
|
padding-left: 45px; /* memberi ruang */
|
|
}
|
|
/* Occupant Chat Checkbox */
|
|
.msg-left .msg-checkbox {
|
|
position: static;
|
|
transform: none;
|
|
margin-left: 15px;
|
|
align-self: center;
|
|
order: 3;
|
|
}
|
|
.msg-left.selectable {
|
|
padding-left: 0;
|
|
}
|
|
.msg-row.selectable .msg-checkbox {
|
|
display: block;
|
|
}
|
|
.msg-row.selected .msg-checkbox i::before {
|
|
content: "\F0134"; /* mdi-checkbox-marked-circle */
|
|
color: #5856d6;
|
|
}
|
|
.msg-row.selected .msg-bubble {
|
|
background: rgba(88, 86, 214, 0.15) !important;
|
|
color: #111b21 !important;
|
|
}
|
|
.msg-row.msg-right.selected .msg-bubble {
|
|
background: rgba(88, 86, 214, 0.8) !important;
|
|
color: #fff !important;
|
|
}
|
|
.ch-right-actions {
|
|
display: none;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
.ch-right-actions i {
|
|
font-size: 22px;
|
|
cursor: pointer;
|
|
color: #64748b;
|
|
}
|
|
.ch-right-actions i.action-del { color: #ef4444; }
|
|
.ch-right-actions i.action-close:hover { color: #333; }
|
|
|
|
/* Dropdown Menu */
|
|
.header-dropdown {
|
|
position: relative;
|
|
display: inline-block;
|
|
}
|
|
.header-dropdown-content {
|
|
display: none;
|
|
position: absolute;
|
|
right: 0;
|
|
top: 30px;
|
|
background-color: #fff;
|
|
min-width: 150px;
|
|
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
|
z-index: 1000;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
.header-dropdown-content a {
|
|
color: #333;
|
|
padding: 12px 16px;
|
|
text-decoration: none;
|
|
display: block;
|
|
font-size: 14px;
|
|
}
|
|
.header-dropdown-content a:hover {
|
|
background-color: #f1f5f9;
|
|
}
|
|
.show-dropdown {
|
|
display: block;
|
|
}
|
|
|
|
/* Profile View */
|
|
.profile-view {
|
|
display: none;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: #fff;
|
|
flex-direction: row;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
z-index: 50;
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
}
|
|
.pv-left {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #fff;
|
|
border-right: 1px solid #e2e8f0;
|
|
}
|
|
.pv-left img {
|
|
max-width: 80%;
|
|
max-height: 80%;
|
|
border-radius: 20px;
|
|
object-fit: contain;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
.pv-right {
|
|
flex: 1;
|
|
padding: 40px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
}
|
|
.pv-close {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
color: #64748b;
|
|
z-index: 51;
|
|
}
|
|
.pv-close:hover { color: #333; }
|
|
.pv-info-row {
|
|
margin-bottom: 20px;
|
|
}
|
|
.pv-label {
|
|
font-size: 11px;
|
|
color: #94a3b8;
|
|
margin-bottom: 4px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
font-weight: 600;
|
|
}
|
|
.pv-value {
|
|
font-size: 16px;
|
|
color: #0f172a;
|
|
font-weight: 500;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.profile-view {
|
|
flex-direction: column;
|
|
overflow-y: auto;
|
|
}
|
|
.pv-left {
|
|
border-right: none;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
padding: 40px 20px;
|
|
flex: none;
|
|
}
|
|
.pv-left img {
|
|
max-width: 200px;
|
|
max-height: 200px;
|
|
border-radius: 50%;
|
|
}
|
|
.pv-right {
|
|
padding: 20px;
|
|
flex: none;
|
|
}
|
|
}
|
|
|
|
/* ===== RESPONSIVE MOBILE (WhatsApp-like) ===== */
|
|
.chat-back-btn {
|
|
display: none;
|
|
background: none;
|
|
border: none;
|
|
font-size: 22px;
|
|
color: #5856d6;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
margin-right: 8px;
|
|
line-height: 1;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.chat-wrapper {
|
|
height: calc(100vh - 120px);
|
|
}
|
|
.chat-sidebar {
|
|
width: 100%;
|
|
border-right: none;
|
|
}
|
|
.chat-main {
|
|
display: none !important;
|
|
width: 100%;
|
|
}
|
|
.chat-wrapper.mobile-chat-open .chat-sidebar {
|
|
display: none !important;
|
|
}
|
|
.chat-wrapper.mobile-chat-open .chat-main {
|
|
display: flex !important;
|
|
width: 100%;
|
|
}
|
|
.chat-back-btn {
|
|
display: inline-flex;
|
|
}
|
|
.emoji-picker {
|
|
width: 280px;
|
|
right: 10px;
|
|
bottom: 60px;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="main-panel">
|
|
<div class="content-wrapper">
|
|
<div class="row">
|
|
<div class="col-md-12 grid-margin stretch-card mb-4 mt-3" style="display:none;">
|
|
</div>
|
|
|
|
<div class="col-md-12 grid-margin stretch-card">
|
|
<div class="card" style="border:1px solid #e5e9f0;box-shadow:none;border-radius:12px;">
|
|
<div class="card-body" style="padding:0;">
|
|
|
|
<div class="chat-wrapper">
|
|
<!-- Sidebar -->
|
|
<div class="chat-sidebar">
|
|
<div class="chat-search-header">
|
|
<img src="images/admin.png" alt="Admin">
|
|
<div class="chat-search-box">
|
|
<i class="mdi mdi-magnify"></i>
|
|
<input type="text" id="searchInput" placeholder="Search..." onkeyup="filterContacts()">
|
|
</div>
|
|
</div>
|
|
<div class="chat-list-wrapper">
|
|
<div id="chatsSection" style="display:none;">
|
|
<div class="section-title">Chats</div>
|
|
<div id="chatsList"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Chat Area -->
|
|
<div class="chat-main">
|
|
<!-- Blank State -->
|
|
<div class="chat-blank" id="chatBlank">
|
|
<div class="chat-blank-icon">
|
|
<i class="mdi mdi-message-text-outline"></i>
|
|
</div>
|
|
<p>Select a contact to start a conversation.</p>
|
|
</div>
|
|
|
|
<!-- Active Chat State -->
|
|
<div class="chat-active-view" id="chatActive">
|
|
<div class="chat-header">
|
|
<div class="ch-left">
|
|
<button class="chat-back-btn" id="btnBackToContacts" onclick="backToContacts()" title="Back">
|
|
<i class="mdi mdi-arrow-left"></i>
|
|
</button>
|
|
<img src="" id="activeAvatar" alt="Contact Avatar">
|
|
<div>
|
|
<div class="ch-name" id="activeName"></div>
|
|
<div class="ch-status" id="activeStatus">Occupant</div>
|
|
</div>
|
|
</div>
|
|
<div class="ch-right">
|
|
<div id="normalHeaderActions" class="header-dropdown">
|
|
<i class="mdi mdi-dots-vertical" onclick="toggleHeaderDropdown()"></i>
|
|
<div id="headerDropdownMenu" class="header-dropdown-content">
|
|
<a href="javascript:void(0)" onclick="openProfileView()">Profile</a>
|
|
<a href="javascript:void(0)" onclick="deleteAllChats()" style="color:#ef4444;">Delete all chats</a>
|
|
</div>
|
|
</div>
|
|
<div id="selectionHeaderActions" class="ch-right-actions">
|
|
<span id="selectionCount" style="font-size:15px; font-weight:bold; color:#5856d6;">0</span>
|
|
<i class="mdi mdi-delete action-del" title="Delete" onclick="triggerBulkDelete()"></i>
|
|
<i class="mdi mdi-close action-close" title="Cancel" onclick="exitSelectionMode()"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chat-history" id="chatHistory">
|
|
<!-- JS will populate -->
|
|
</div>
|
|
|
|
<div class="chat-footer">
|
|
<div class="reply-preview-box" id="replyPreview">
|
|
<div class="rp-title" id="rpTitle">Replying to User</div>
|
|
<div class="rp-text" id="rpText">...</div>
|
|
<i class="mdi mdi-close rp-close" onclick="closeReplyBox()"></i>
|
|
</div>
|
|
|
|
<div class="reply-preview-box" id="editPreview" style="border-left-color: #f59e0b;">
|
|
<div class="rp-title" style="color: #f59e0b;"><i class="mdi mdi-pencil" style="font-size:14px; margin-right:4px;"></i> Editing Message...</div>
|
|
<div class="rp-text" id="epText">...</div>
|
|
<i class="mdi mdi-close rp-close" onclick="cancelEdit()"></i>
|
|
</div>
|
|
|
|
<div class="chat-footer-top" id="footerNormal">
|
|
<div class="input-wrapper">
|
|
<textarea id="messageInput" rows="1" placeholder="Type a message..." onkeydown="handleEnter(event)"></textarea>
|
|
<button class="icon-btn" id="btnEmoji" onclick="toggleEmoji()" style="margin-bottom: 4px;"><i class="mdi mdi-emoticon-outline"></i></button>
|
|
</div>
|
|
<button class="btn-send" onclick="submitMessageOrEdit()" style="align-self: flex-end; margin-bottom: 4px;">
|
|
<i class="mdi mdi-send"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Profile View Overlay -->
|
|
<div class="profile-view" id="profileView">
|
|
<i class="mdi mdi-close pv-close" onclick="closeProfileView()" title="Close Profile"></i>
|
|
<div class="pv-left">
|
|
<img src="" id="pvImage" alt="Profile Image">
|
|
</div>
|
|
<div class="pv-right">
|
|
<h3 style="margin-bottom: 30px; color: #1e293b; font-weight: 600;">Occupant Profile</h3>
|
|
<div class="pv-info-row">
|
|
<div class="pv-label">Name</div>
|
|
<div class="pv-value" id="pvName"></div>
|
|
</div>
|
|
<div class="pv-info-row">
|
|
<div class="pv-label">Phone Number</div>
|
|
<div class="pv-value" id="pvPhone"></div>
|
|
</div>
|
|
<div class="pv-info-row">
|
|
<div class="pv-label">NIK / NIP</div>
|
|
<div class="pv-value" id="pvNik"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Emoji Picker -->
|
|
<div class="emoji-picker" id="emojiPicker">
|
|
<div class="emoji-tabs">
|
|
<div class="emoji-tab active" data-cat="smileys">😊</div>
|
|
<div class="emoji-tab" data-cat="gestures">👍</div>
|
|
<div class="emoji-tab" data-cat="hearts">❤️</div>
|
|
<div class="emoji-tab" data-cat="objects">🎉</div>
|
|
<div class="emoji-tab" data-cat="animals">🐱</div>
|
|
<div class="emoji-tab" data-cat="food">🍕</div>
|
|
</div>
|
|
<div class="emoji-grid" id="emojiGrid"></div>
|
|
</div>
|
|
|
|
<!-- Context Menu -->
|
|
<div id="msgContextMenu" class="chat-context-menu">
|
|
<ul>
|
|
<li onclick="prepareReply(event)"><i class="mdi mdi-reply"></i> Reply</li>
|
|
<li id="ctxMenuEdit" onclick="editSelectedMessage(event)" style="display:none;"><i class="mdi mdi-pencil-outline"></i> Edit</li>
|
|
<li onclick="enterSelectionMode(event)"><i class="mdi mdi-checkbox-marked-outline"></i> Select</li>
|
|
<div class="ctx-sep"></div>
|
|
<li class="ctx-danger" onclick="deleteSelectedMessage(event)"><i class="mdi mdi-delete-outline"></i> Delete</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Custom Delete Modal -->
|
|
<div class="del-overlay" id="delOverlay">
|
|
<div class="del-box">
|
|
<div class="del-title" id="delTitle"></div>
|
|
<div id="delOptions"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== MODAL: DELETE ALL CHATS ==================== -->
|
|
<div class="modal fade" id="modalDeleteAllChats" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content mdl-card" style="border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);">
|
|
<form id="formDeleteAllChats" onsubmit="executeDeleteAllChats(event)">
|
|
<div class="modal-body p-4" style="text-align: left;">
|
|
<h5 class="font-weight-bold mb-3">Delete All Chats</h5>
|
|
<p style="font-size:14px;color:#666;margin-bottom:20px;">Are you sure you want to delete all chats with <strong id="deleteAll_occupant_name">this occupant</strong>?</p>
|
|
|
|
<div class="d-flex justify-content-end align-items-center" style="gap:20px;">
|
|
<span style="font-size:14px; font-weight:500; color:#1e293b; cursor:pointer;" data-dismiss="modal">Cancel</span>
|
|
<span style="font-size:14px; font-weight:500; color:#ef4444; cursor:pointer;" onclick="executeDeleteAllChats(event)">Delete</span>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.delete-confirm-cb:checked ~ .custom-control-label::before {
|
|
background-color: #ff4747 !important;
|
|
border-color: #ff4747 !important;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
function showDeleteModal(title, options) {
|
|
document.getElementById('delTitle').textContent = title;
|
|
let optHtml = '';
|
|
options.forEach(function(o) {
|
|
optHtml += '<div class="del-option ' + o.cls + '" id="del-opt-' + Math.random().toString(36).substr(2,5) + '">' + o.text + '</div>';
|
|
});
|
|
optHtml += '<div class="del-option del-cancel">Cancel</div>';
|
|
document.getElementById('delOptions').innerHTML = optHtml;
|
|
|
|
// Bind events
|
|
let optEls = document.getElementById('delOptions').querySelectorAll('.del-option');
|
|
let idx = 0;
|
|
options.forEach(function(o) {
|
|
optEls[idx].onclick = o.action;
|
|
idx++;
|
|
});
|
|
// Cancel = last one
|
|
optEls[optEls.length - 1].onclick = closeDeleteModal;
|
|
|
|
document.getElementById('delOverlay').classList.add('show');
|
|
}
|
|
function closeDeleteModal() {
|
|
document.getElementById('delOverlay').classList.remove('show');
|
|
}
|
|
// Close on overlay background click
|
|
document.getElementById('delOverlay').addEventListener('click', function(e) {
|
|
if (e.target === this) closeDeleteModal();
|
|
});
|
|
</script>
|
|
|
|
<?php include 'footer.php'; ?>
|
|
|
|
<script>
|
|
let contacts = [];
|
|
let activeOccupantId = null;
|
|
let activeAvatarUrl = '';
|
|
let activeOccupantData = null;
|
|
|
|
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 isToday = date.toDateString() === now.toDateString();
|
|
const yesterday = new Date(now);
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
const isYesterday = date.toDateString() === yesterday.toDateString();
|
|
|
|
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;
|
|
const timeStr = hours + ':' + minutes + ' ' + ampm;
|
|
|
|
if (isToday) return timeStr;
|
|
if (isYesterday) return 'Yesterday';
|
|
|
|
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
|
|
if (diffDays < 7) {
|
|
const days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
|
|
return days[date.getDay()];
|
|
}
|
|
|
|
const dd = String(date.getDate()).padStart(2, '0');
|
|
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
const yyyy = date.getFullYear();
|
|
return dd + '/' + mm + '/' + yyyy;
|
|
}
|
|
|
|
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 = '📷 Photo';
|
|
if(!strippedMsg && rawLastMsg.includes('mdi-file-document')) strippedMsg = '📄 Document';
|
|
if(!strippedMsg && rawLastMsg.includes('<video')) strippedMsg = '🎥 Video';
|
|
if(!strippedMsg && rawLastMsg.includes('<audio')) strippedMsg = '🎵 Audio';
|
|
|
|
let lastMsg = strippedMsg || (rawLastMsg ? 'Attachment' : '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: #1e2223;';
|
|
msgStyle = 'font-weight: 700; color: #222;';
|
|
}
|
|
|
|
let verifiedColor = (c.id_occupant == activeOccupantId) ? '#fff' : '#1da1f2';
|
|
let isVerified = c.name && c.nik_nip && c.no_hp && c.telegram_id && c.pin && c.foto && c.finger_id && c.code_register && c.progress === 'used' && c.status === 'approved' && c.access === 'active';
|
|
let verifiedIcon = isVerified ? `<i class="mdi mdi-check-decagram" style="color: ${verifiedColor}; font-size: 14px; margin-left: 4px; vertical-align: middle;" title="Verified Approved"></i>` : '';
|
|
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}; display:inline-flex; align-items:center; width:100%;">${c.name} ${verifiedIcon}</div>
|
|
<div class="ci-time">${timeStr}</div>
|
|
</div>
|
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:4px;">
|
|
<div class="ci-msg" style="${msgStyle}; flex:1;">${lastMsg}</div>
|
|
${unreadBadge}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
chatsHtml += html;
|
|
});
|
|
|
|
if(chatsHtml) {
|
|
document.getElementById('chatsList').innerHTML = chatsHtml;
|
|
document.getElementById('chatsSection').style.display = 'block';
|
|
} else {
|
|
document.getElementById('chatsList').innerHTML = '<div style="text-align:center; padding:20px; color:#999; font-size:13px;">No active chats.</div>';
|
|
document.getElementById('chatsSection').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function filterContacts() {
|
|
let q = document.getElementById('searchInput').value.toLowerCase();
|
|
let filtered = contacts.filter(c => c.name.toLowerCase().includes(q));
|
|
renderContacts(filtered);
|
|
}
|
|
|
|
function getOccupantStatusString(occupant) {
|
|
let occStatStr = 'Status unknown';
|
|
if (occupant && occupant.last_active) {
|
|
let lastActStr = 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 = `Last Online ${jam}.${menit} ${tgl}-${bln}-${thn}`;
|
|
} else {
|
|
const hariArr = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
let namaHari = hariArr[actDate.getDay()];
|
|
let jam = actDate.getHours().toString().padStart(2, '0');
|
|
let menit = actDate.getMinutes().toString().padStart(2, '0');
|
|
occStatStr = `Last Online at ${jam}:${menit} on ${namaHari}`;
|
|
}
|
|
}
|
|
return occStatStr;
|
|
}
|
|
|
|
function openChat(id) {
|
|
activeOccupantId = id;
|
|
renderContacts(contacts);
|
|
|
|
document.getElementById('chatBlank').style.display = 'none';
|
|
document.getElementById('chatActive').style.display = 'flex';
|
|
|
|
// Mobile: switch to chat view
|
|
document.querySelector('.chat-wrapper').classList.add('mobile-chat-open');
|
|
|
|
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') {
|
|
let occ = res.occupant;
|
|
activeOccupantData = occ;
|
|
let isVerifiedActive = occ.name && occ.nik_nip && occ.no_hp && occ.telegram_id && occ.pin && occ.foto && occ.finger_id && occ.code_register && occ.progress === 'used' && occ.status === 'approved' && occ.access === 'active';
|
|
let activeVerifiedIcon = isVerifiedActive ? `<i class="mdi mdi-check-decagram" style="color: #1da1f2; font-size: 16px; margin-left: 4px; vertical-align: middle;" title="Verified Approved"></i>` : '';
|
|
document.getElementById('activeName').innerHTML = res.occupant.name + activeVerifiedIcon;
|
|
|
|
document.getElementById('activeStatus').innerText = getOccupantStatusString(res.occupant);
|
|
document.getElementById('activeAvatar').src = res.occupant.foto;
|
|
activeAvatarUrl = res.occupant.foto;
|
|
|
|
renderMessages(res.data);
|
|
loadContacts();
|
|
}
|
|
}, 'json');
|
|
}
|
|
|
|
function backToContacts() {
|
|
document.querySelector('.chat-wrapper').classList.remove('mobile-chat-open');
|
|
activeOccupantId = null;
|
|
document.getElementById('chatBlank').style.display = 'flex';
|
|
document.getElementById('chatActive').style.display = 'none';
|
|
renderContacts(contacts);
|
|
}
|
|
|
|
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') ? 'You' : document.getElementById('activeName').innerText;
|
|
let strippedRpMsg = msg.reply_message.replace(/<[^>]*>?/gm, '');
|
|
if(!strippedRpMsg) strippedRpMsg = 'Attachment/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">Sending... ${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="Failed to send"></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="Failed to send"></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') ? 'Replying to You' : ('Replying to ' + 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);
|
|
let msgId = selectedMsgId;
|
|
|
|
if (selectedMsgSender === 'account') {
|
|
showDeleteModal('Delete Message?', [
|
|
{ text: 'Delete for Everyone', cls: 'del-red', action: function(){ closeDeleteModal(); executeDelete(msgId, msgElement, 'everyone'); } },
|
|
{ text: 'Delete for Me', cls: 'del-amber', action: function(){ closeDeleteModal(); executeDelete(msgId, msgElement, 'me'); } }
|
|
]);
|
|
} else {
|
|
showDeleteModal('Delete Message?', [
|
|
{ text: 'Delete for Me', cls: 'del-red', action: function(){ closeDeleteModal(); executeDelete(msgId, 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('Failed', res.message || 'Message could not be deleted', '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;
|
|
|
|
let onlyAdminMessages = selectedMessages.every(m => m.sender === 'account');
|
|
let count = selectedMessages.length;
|
|
|
|
if (onlyAdminMessages) {
|
|
showDeleteModal('Delete ' + count + ' Messages?', [
|
|
{ text: 'Delete for Everyone', cls: 'del-red', action: function(){ closeDeleteModal(); executeBulkDelete('everyone'); } },
|
|
{ text: 'Delete for Me', cls: 'del-amber', action: function(){ closeDeleteModal(); executeBulkDelete('me'); } }
|
|
]);
|
|
} else {
|
|
showDeleteModal('Delete ' + count + ' Messages?', [
|
|
{ text: 'Delete for Me', cls: 'del-red', action: function(){ closeDeleteModal(); 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();
|
|
} else {
|
|
// Revert opacity
|
|
idsToDelete.forEach(id => {
|
|
let el = document.getElementById('msg-' + id);
|
|
if(el) el.style.opacity = '1';
|
|
});
|
|
Swal.fire('Failed', res.message || 'Failed to delete some messages', '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('Failed', res.message || 'Message could not be edited', 'error');
|
|
}
|
|
}, 'json');
|
|
}
|
|
|
|
// Profile and Dropdown Logic
|
|
document.addEventListener('click', function(e) {
|
|
let dropIcon = document.querySelector('#normalHeaderActions i');
|
|
let dropMenu = document.getElementById('headerDropdownMenu');
|
|
if (dropIcon && dropMenu && !dropIcon.contains(e.target) && !dropMenu.contains(e.target)) {
|
|
dropMenu.classList.remove('show-dropdown');
|
|
}
|
|
});
|
|
|
|
function toggleHeaderDropdown() {
|
|
document.getElementById('headerDropdownMenu').classList.toggle('show-dropdown');
|
|
}
|
|
|
|
function openProfileView() {
|
|
document.getElementById('headerDropdownMenu').classList.remove('show-dropdown');
|
|
if(activeOccupantData) {
|
|
document.getElementById('pvImage').src = activeOccupantData.foto || 'images/admin.png';
|
|
document.getElementById('pvName').innerText = activeOccupantData.name || '-';
|
|
document.getElementById('pvPhone').innerText = activeOccupantData.no_hp || '-';
|
|
document.getElementById('pvNik').innerText = activeOccupantData.nik_nip || '-';
|
|
}
|
|
document.getElementById('profileView').style.display = 'flex';
|
|
}
|
|
|
|
function closeProfileView() {
|
|
document.getElementById('profileView').style.display = 'none';
|
|
}
|
|
|
|
function deleteAllChats() {
|
|
document.getElementById('headerDropdownMenu').classList.remove('show-dropdown');
|
|
|
|
if(activeOccupantData && activeOccupantData.name) {
|
|
document.getElementById('deleteAll_occupant_name').innerText = activeOccupantData.name;
|
|
} else {
|
|
document.getElementById('deleteAll_occupant_name').innerText = "this occupant";
|
|
}
|
|
|
|
$('#modalDeleteAllChats').modal('show');
|
|
}
|
|
|
|
function executeDeleteAllChats(event) {
|
|
if (event) event.preventDefault();
|
|
$('#modalDeleteAllChats').modal('hide');
|
|
|
|
isSelectionMode = true;
|
|
selectedMessages = [];
|
|
let rows = document.querySelectorAll('.msg-row');
|
|
rows.forEach(row => {
|
|
let id = parseInt(row.getAttribute('data-id'));
|
|
let sender = row.classList.contains('msg-right') ? 'account' : 'occupant';
|
|
selectedMessages.push({id: id, sender: sender});
|
|
});
|
|
if (selectedMessages.length > 0) {
|
|
executeBulkDelete('me');
|
|
} else {
|
|
Swal.fire('Empty', 'No messages to delete', 'info');
|
|
isSelectionMode = false;
|
|
}
|
|
}
|
|
|
|
// Initial load
|
|
$(document).ready(function() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const chatId = urlParams.get('id');
|
|
|
|
if (chatId) {
|
|
openChat(chatId);
|
|
} else {
|
|
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') {
|
|
if(res2.occupant) {
|
|
document.getElementById('activeStatus').innerText = getOccupantStatusString(res2.occupant);
|
|
}
|
|
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);
|
|
});
|
|
</script>
|