![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/soundstudiopro.com/public_html/ |
<?php
session_start();
require_once 'config/database.php';
require_once 'includes/security.php';
require_once 'includes/translations.php';
if (!isset($_SESSION['user_id'])) {
header('Location: /auth/login.php');
exit;
}
$current_page = 'messages';
$page_title = t('messages.page_title');
$selected_user_id = $_GET['user_id'] ?? null;
?>
<?php include 'includes/header.php'; ?>
<div class="container">
<div class="messages-container">
<div class="messages-header">
<h1><i class="fas fa-comments"></i> <?= t('messages.title') ?></h1>
<div class="header-actions">
<button class="btn btn-icon-mobile" id="showSidebarBtn" aria-label="Show conversations">
<i class="fas fa-bars"></i>
</button>
<button class="btn btn-primary" id="newMessageBtn">
<i class="fas fa-plus"></i>
<span class="btn-text"><?= t('messages.new_message') ?></span>
</button>
</div>
</div>
<div class="messages-content" id="messagesLayout">
<!-- Sidebar: Conversations List -->
<div class="messages-sidebar" id="messagesSidebar">
<div class="sidebar-header-mobile">
<h2><?= t('messages.title') ?></h2>
<button class="close-sidebar-btn" id="closeSidebarBtn" aria-label="Close sidebar">
<i class="fas fa-times"></i>
</button>
</div>
<div class="search-box">
<input type="text" id="searchUsers" placeholder="<?= t('messages.search_users') ?>" autocomplete="off">
<i class="fas fa-search"></i>
<div id="searchResults" class="search-results"></div>
</div>
<div id="conversationsList" class="conversations-list">
<div class="loading"><?= t('messages.loading_conversations') ?></div>
</div>
</div>
<!-- Main: Message Thread -->
<div class="messages-main">
<div class="conversation-header-mobile">
<button class="back-to-conversations-btn" id="backToConversationsBtn" aria-label="Back to conversations">
<i class="fas fa-arrow-left"></i>
</button>
<h2 id="mobileConversationTitle"><?= t('messages.title') ?></h2>
</div>
<div id="noConversation" class="no-conversation">
<i class="fas fa-comments"></i>
<h2><?= t('messages.select_conversation') ?></h2>
<p><?= t('messages.select_conversation_desc') ?></p>
</div>
<div id="conversationView" class="conversation-view" style="display: none;">
<div class="conversation-header">
<div class="conversation-user-info">
<div class="user-avatar" id="conversationAvatar"></div>
<div class="user-details">
<div class="user-name" id="conversationUserName"></div>
<div class="user-status" id="conversationUserStatus"><?= t('messages.offline') ?></div>
</div>
</div>
<div class="conversation-actions">
<button class="btn-icon mark-unseen" id="markUnseenBtn" title="<?= t('messages.mark_unread') ?>">
<i class="fas fa-envelope"></i>
</button>
<button class="btn-icon" id="deleteConversationBtn" title="<?= t('messages.delete_conversation') ?>">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
<div id="messagesList" class="messages-list">
<div class="loading"><?= t('messages.loading_messages') ?></div>
</div>
<div class="message-input-container">
<form id="messageForm" class="message-form">
<input type="hidden" id="receiverId" name="receiver_id">
<div class="input-wrapper">
<input
type="text"
id="messageInput"
name="message"
placeholder="<?= t('messages.type_message') ?>"
autocomplete="off"
maxlength="5000"
>
<button type="submit" class="send-btn" id="sendBtn" aria-label="Send message">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</form>
</div>
</div>
</div>
<div class="messages-overlay" id="messagesOverlay" aria-hidden="true"></div>
</div>
</div>
</div>
<!-- New Message Modal -->
<div id="newMessageModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2><?= t('messages.new_message') ?></h2>
<button class="modal-close" id="closeModal" aria-label="Close modal">×</button>
</div>
<div class="modal-body">
<div class="search-box">
<input type="text" id="newMessageSearch" placeholder="<?= t('messages.search_users_message') ?>" autocomplete="off">
<i class="fas fa-search"></i>
</div>
<div id="newMessageUsers" class="users-list"></div>
</div>
</div>
</div>
<!-- Forward Message Modal -->
<div id="forwardMessageModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2><?= t('messages.forward_message_title') ?></h2>
<button class="modal-close" id="closeForwardModal" aria-label="Close modal">×</button>
</div>
<div class="modal-body">
<div class="forward-message-preview" id="forwardMessagePreview"></div>
<div class="search-box">
<input type="text" id="forwardMessageSearch" placeholder="<?= t('messages.search_artists_forward') ?>" autocomplete="off">
<i class="fas fa-search"></i>
</div>
<div id="forwardMessageUsers" class="users-list"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancelForwardBtn"><?= t('common.cancel') ?></button>
</div>
</div>
</div>
<style>
.messages-container {
max-width: 1400px;
margin: 0 auto;
padding: 1.5rem;
min-height: calc(100vh - 180px);
padding-bottom: 1rem;
margin-bottom: 1rem;
}
.messages-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.messages-header h1 {
color: white;
font-size: 2rem;
display: flex;
align-items: center;
gap: 1rem;
margin: 0;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.messages-content {
display: grid;
grid-template-columns: 380px 1fr;
gap: 1.5rem;
height: calc(100vh - 200px);
max-height: calc(100vh - 200px);
min-height: 500px;
position: relative;
overflow: hidden;
}
.messages-overlay {
position: absolute;
inset: 0;
border-radius: 16px;
background: rgba(8, 10, 24, 0.65);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 110;
}
.messages-sidebar {
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
padding: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.search-box {
position: relative;
margin-bottom: 1rem;
}
.search-box input {
width: 100%;
padding: 0.75rem 2.5rem 0.75rem 1rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: white;
font-size: 0.95rem;
}
.search-box input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.search-box i {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
color: rgba(255, 255, 255, 0.5);
}
.search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: rgba(30, 30, 30, 0.98);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
margin-top: 0.5rem;
max-height: 300px;
overflow-y: auto;
z-index: 100;
display: none;
}
.search-results.active {
display: block;
}
.search-result-item {
padding: 1rem;
cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
gap: 1rem;
transition: background 0.2s;
}
.search-result-item:hover {
background: rgba(102, 126, 234, 0.1);
}
.conversations-list {
flex: 1;
overflow-y: auto;
padding-right: 0.5rem;
}
.conversation-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
margin-bottom: 0.5rem;
}
.conversation-item:hover,
.conversation-item.active {
background: rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.2);
}
.thread-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 1.1rem;
flex-shrink: 0;
}
.thread-avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.thread-avatar-link,
.user-avatar-link,
.message-avatar-link {
display: inline-block;
text-decoration: none;
color: inherit;
transition: transform 0.2s ease;
}
.thread-avatar-link:hover,
.user-avatar-link:hover,
.message-avatar-link:hover {
transform: scale(1.05);
}
.thread-avatar-link:active,
.user-avatar-link:active,
.message-avatar-link:active {
transform: scale(0.95);
}
.thread-info {
flex: 1;
min-width: 0;
}
.thread-name {
color: white;
font-weight: 600;
margin-bottom: 0.2rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.thread-preview {
color: #a0aec0;
font-size: 0.9rem;
margin-bottom: 0.2rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.thread-time {
color: #718096;
font-size: 0.8rem;
}
.unread-badge {
background: linear-gradient(135deg, #ff4757, #ff3742);
color: white;
font-size: 0.7rem;
font-weight: 700;
min-width: 20px;
height: 20px;
padding: 0 5px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
line-height: 1;
}
.messages-main {
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
height: 100%;
position: relative;
}
.no-conversation {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
color: white;
padding: 2rem;
}
.no-conversation i {
font-size: 4rem;
color: #667eea;
margin-bottom: 1rem;
}
.no-conversation h2 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.no-conversation p {
color: #a0aec0;
}
.conversation-view {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
flex: 1;
position: relative;
overflow: hidden;
}
.conversation-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.02);
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.conversation-user-info {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
}
.conversation-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn-icon {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #a0aec0;
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-icon:hover {
background: rgba(255, 77, 87, 0.2);
border-color: rgba(255, 77, 87, 0.4);
color: #ff4757;
}
.btn-icon.mark-unseen:hover {
background: rgba(102, 126, 234, 0.2);
border-color: rgba(102, 126, 234, 0.4);
color: #667eea;
}
.btn-icon i {
font-size: 0.9rem;
}
.user-avatar {
width: 45px;
height: 45px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 1rem;
flex-shrink: 0;
}
.user-avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.user-name {
color: white;
font-weight: 600;
font-size: 1.1rem;
}
.user-status {
color: #a0aec0;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 0.5rem;
transition: color 0.3s ease;
}
.user-status.online {
color: #48bb78;
font-weight: 500;
}
.user-status.online::before {
content: '';
width: 8px;
height: 8px;
background: #48bb78;
border-radius: 50%;
display: inline-block;
animation: pulse-green 2s infinite;
box-shadow: 0 0 0 0 rgba(72, 187, 120, 0.7);
}
@keyframes pulse-green {
0% {
box-shadow: 0 0 0 0 rgba(72, 187, 120, 0.7);
transform: scale(1);
}
50% {
box-shadow: 0 0 0 4px rgba(72, 187, 120, 0);
transform: scale(1.1);
}
100% {
box-shadow: 0 0 0 0 rgba(72, 187, 120, 0);
transform: scale(1);
}
}
.messages-list {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 1.5rem;
padding-bottom: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
/* Ensure messages list doesn't overlap with input */
margin-bottom: 80px;
}
.message-item {
display: flex;
gap: 0.75rem;
animation: fadeIn 0.3s ease;
}
.message-item.sent {
flex-direction: row-reverse;
}
.message-avatar {
width: 35px;
height: 35px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 0.85rem;
flex-shrink: 0;
}
.message-avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.message-content {
max-width: 70%;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.message-bubble {
padding: 0.75rem 1rem;
border-radius: 12px;
word-wrap: break-word;
line-height: 1.5;
position: relative;
}
.message-link {
color: inherit;
text-decoration: underline;
opacity: 0.9;
transition: opacity 0.2s;
}
.message-link:hover {
opacity: 1;
}
.message-forward-btn,
.message-unsend-btn {
position: absolute;
top: 0.5rem;
background: rgba(0, 0, 0, 0.3);
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: all 0.2s ease;
color: rgba(255, 255, 255, 0.8);
font-size: 0.75rem;
z-index: 10;
padding: 0;
min-width: 24px;
}
.message-forward-btn {
right: 2.5rem;
}
.message-unsend-btn {
right: 0.5rem;
}
.message-item.sent .message-bubble:hover .message-forward-btn,
.message-item.sent .message-bubble:hover .message-unsend-btn,
.message-item.sent .message-bubble:focus-within .message-forward-btn,
.message-item.sent .message-bubble:focus-within .message-unsend-btn {
opacity: 1;
}
/* Show unsend and forward buttons on mobile/touch devices */
@media (hover: none) and (pointer: coarse) {
.message-item.sent .message-forward-btn,
.message-item.sent .message-unsend-btn {
opacity: 0.6;
}
.message-item.sent .message-forward-btn:active,
.message-item.sent .message-unsend-btn:active {
opacity: 1;
}
}
.message-forward-btn:hover {
background: rgba(102, 126, 234, 0.8);
color: white;
transform: scale(1.1);
}
.message-forward-btn:active {
transform: scale(0.95);
}
.message-unsend-btn:hover {
background: rgba(255, 77, 87, 0.8);
color: white;
transform: scale(1.1);
}
.message-unsend-btn:active {
transform: scale(0.95);
}
.forward-message-preview {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
line-height: 1.5;
}
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 0.5rem 1rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.2);
}
.link-preview-container {
margin-top: 0.75rem;
}
.link-preview {
position: relative;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
overflow: hidden;
background: rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
max-width: 100%;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
color: inherit;
margin-top: 0.5rem;
}
.message-item.sent .link-preview {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.3);
}
.message-item.received .link-preview {
background: rgba(0, 0, 0, 0.2);
border-color: rgba(255, 255, 255, 0.15);
}
.link-preview:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border-color: rgba(102, 126, 234, 0.5);
}
.link-preview-image {
width: 100%;
height: 200px;
overflow: hidden;
background: rgba(0, 0, 0, 0.2);
position: relative;
}
.link-preview-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.link-preview-image .link-preview-icon {
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
.link-preview-content {
padding: 0.75rem;
flex: 1;
}
.link-preview-site {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.6);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.25rem;
font-weight: 600;
}
.link-preview-title {
font-size: 0.95rem;
font-weight: 600;
color: white;
margin-bottom: 0.25rem;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.link-preview-description {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-top: 0.25rem;
}
.link-preview-icon {
position: absolute;
top: 0.5rem;
right: 0.5rem;
font-size: 1.2rem;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(5px);
}
/* Link preview without image */
.link-preview:not(:has(.link-preview-image)) {
flex-direction: row;
align-items: flex-start;
}
.link-preview:not(:has(.link-preview-image)) .link-preview-content {
flex: 1;
}
.link-preview:not(:has(.link-preview-image)) .link-preview-icon {
position: static;
margin: 0.75rem;
margin-left: 0;
flex-shrink: 0;
}
.message-item.sent .message-bubble {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-bottom-right-radius: 4px;
}
.message-item.received .message-bubble {
background: rgba(255, 255, 255, 0.1);
color: white;
border-bottom-left-radius: 4px;
}
.message-meta {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: #718096;
padding: 0 0.5rem;
}
.message-item.sent .message-meta {
justify-content: flex-end;
}
.message-time {
font-size: 0.75rem;
color: inherit;
}
.message-read-status {
display: inline-flex;
align-items: center;
gap: 0.25rem;
color: #48bb78;
font-weight: 600;
text-transform: none;
}
.message-read-status i {
font-size: 0.7rem;
}
.message-input-container {
padding: 1rem 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(10, 10, 20, 0.98);
position: absolute;
bottom: 0;
left: 0;
right: 0;
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
backdrop-filter: blur(10px);
z-index: 10;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
}
.message-form {
width: 100%;
}
.input-wrapper {
display: flex;
gap: 0.75rem;
align-items: center;
}
.input-wrapper input {
flex: 1;
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: white;
font-size: 0.95rem;
}
.input-wrapper input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.send-btn {
width: 45px;
height: 45px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
border: none;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
flex-shrink: 0;
}
.send-btn:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.loading {
text-align: center;
color: #a0aec0;
padding: 2rem;
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: rgba(30, 30, 30, 0.98);
border-radius: 16px;
width: 90%;
max-width: 500px;
max-height: 80vh;
display: flex;
flex-direction: column;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.modal-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
color: white;
margin: 0;
}
.modal-close {
background: none;
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.2s;
}
.modal-close:hover {
background: rgba(255, 255, 255, 0.1);
}
.modal-body {
padding: 1.5rem;
overflow-y: auto;
}
.users-list {
margin-top: 1rem;
}
.user-item {
padding: 1rem;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 1rem;
transition: background 0.2s;
border: 1px solid transparent;
}
.user-item:hover {
background: rgba(102, 126, 234, 0.1);
border-color: rgba(102, 126, 234, 0.2);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
/* Scrollbar Styling */
.messages-sidebar::-webkit-scrollbar,
.messages-list::-webkit-scrollbar,
.search-results::-webkit-scrollbar {
width: 6px;
}
.messages-sidebar::-webkit-scrollbar-track,
.messages-list::-webkit-scrollbar-track,
.search-results::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
}
.messages-sidebar::-webkit-scrollbar-thumb,
.messages-list::-webkit-scrollbar-thumb,
.search-results::-webkit-scrollbar-thumb {
background: rgba(102, 126, 234, 0.5);
border-radius: 3px;
}
.messages-sidebar::-webkit-scrollbar-thumb:hover,
.messages-list::-webkit-scrollbar-thumb:hover,
.search-results::-webkit-scrollbar-thumb:hover {
background: rgba(102, 126, 234, 0.7);
}
/* Mobile Header for Sidebar */
.sidebar-header-mobile {
display: none;
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.sidebar-header-mobile h2 {
color: white;
font-size: 1.25rem;
margin: 0;
}
.close-sidebar-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
.close-sidebar-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.btn-icon-mobile {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
width: 40px;
height: 40px;
border-radius: 8px;
display: none;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
}
.btn-icon-mobile:hover {
background: rgba(102, 126, 234, 0.2);
border-color: rgba(102, 126, 234, 0.4);
}
/* Mobile Header for Conversation */
.conversation-header-mobile {
display: none;
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.02);
align-items: center;
gap: 1rem;
}
.back-to-conversations-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
}
.back-to-conversations-btn:hover {
background: rgba(102, 126, 234, 0.2);
border-color: rgba(102, 126, 234, 0.4);
}
.conversation-header-mobile h2 {
color: white;
font-size: 1.1rem;
margin: 0;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Mobile Responsive */
@media (max-width: 768px) {
.btn-icon-mobile {
display: flex;
}
.messages-container {
padding: 1rem;
min-height: calc(100vh - 150px);
padding-bottom: 6rem;
margin-bottom: 4rem;
}
.messages-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1rem;
}
.messages-header h1 {
font-size: 1.75rem;
}
.btn-primary {
width: 100%;
justify-content: center;
}
.header-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.btn-primary .btn-text {
display: inline;
}
#messagesLayout {
grid-template-columns: 1fr;
height: calc(100vh - 250px);
min-height: 500px;
gap: 0;
position: relative;
}
#messagesLayout .messages-sidebar {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 130;
transform: translateX(-100%);
transition: transform 0.3s ease;
max-height: none;
height: 100%;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.3);
}
#messagesLayout.sidebar-open .messages-sidebar {
transform: translateX(0);
}
#messagesLayout .messages-overlay {
display: block;
}
#messagesLayout.sidebar-open .messages-overlay {
opacity: 1;
pointer-events: auto;
}
#messagesLayout.conversation-active .messages-sidebar {
transform: translateX(-100%);
}
.sidebar-header-mobile {
display: flex;
}
.messages-main {
min-height: 100%;
position: relative;
z-index: 100;
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 2rem);
}
.conversation-header-mobile {
display: flex;
}
.conversation-header {
display: none;
}
.message-content {
max-width: 85%;
}
.message-bubble {
padding: 0.625rem 0.875rem;
font-size: 0.9rem;
}
.message-input-container {
padding: 0.85rem 1rem calc(0.85rem + env(safe-area-inset-bottom, 0px)) 1rem;
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
.messages-list {
margin-bottom: 80px;
}
.input-wrapper input {
font-size: 0.9rem;
padding: 0.625rem 0.875rem;
}
.send-btn {
width: 40px;
height: 40px;
}
.modal-content {
width: 95%;
max-height: 90vh;
margin: 1rem;
}
.modal-header {
padding: 1rem;
}
.modal-body {
padding: 1rem;
}
.thread-avatar {
width: 45px;
height: 45px;
font-size: 1rem;
}
.message-avatar {
width: 30px;
height: 30px;
font-size: 0.75rem;
}
.user-avatar {
width: 40px;
height: 40px;
font-size: 0.9rem;
}
}
@media (max-width: 480px) {
.messages-container {
padding: 0.75rem;
}
.messages-header h1 {
font-size: 1.5rem;
}
.message-content {
max-width: 90%;
}
.conversation-item {
padding: 0.75rem;
}
.thread-info {
font-size: 0.9rem;
}
.thread-preview {
font-size: 0.85rem;
}
}
</style>
<script>
let currentUserId = <?= json_encode($_SESSION['user_id']) ?>;
let selectedUserId = null;
let refreshInterval = null;
let conversationRefreshInterval = null;
let searchTimeout = null;
let previousUnreadCount = 0;
let previousConversationCounts = {}; // Track unread counts per conversation
let lastMessageId = null; // Track last message ID to detect new messages
let lastReadMessageId = null; // Track last sent message that has been seen
let currentMessages = []; // Cache current conversation messages
let messagesLayout = null;
let messagesOverlay = null;
let resizeTimeout = null;
// Translation strings for JavaScript
const translations = {
loadingConversations: <?= json_encode(t('messages.loading_conversations')) ?>,
noConversations: <?= json_encode(t('messages.no_conversations')) ?>,
loadingMessages: <?= json_encode(t('messages.loading_messages')) ?>,
noMessages: <?= json_encode(t('messages.no_messages')) ?>,
online: <?= json_encode(t('messages.online')) ?>,
offline: <?= json_encode(t('messages.offline')) ?>,
noUsersFound: <?= json_encode(t('messages.no_users_found')) ?>,
conversationMarkedUnread: <?= json_encode(t('messages.conversation_marked_unread')) ?>,
conversationDeleted: <?= json_encode(t('messages.conversation_deleted')) ?>,
errorLoadingConversations: <?= json_encode(t('messages.error_loading_conversations')) ?>,
errorLoadingMessages: <?= json_encode(t('messages.error_loading_messages')) ?>,
errorSendingMessage: <?= json_encode(t('messages.error_sending_message')) ?>,
errorMarkingUnread: <?= json_encode(t('messages.error_marking_unread')) ?>,
errorDeleting: <?= json_encode(t('messages.error_deleting')) ?>,
deleteConfirm: <?= json_encode(t('messages.delete_confirm')) ?>,
justNow: <?= json_encode(t('messages.just_now')) ?>,
minutesAgo: <?= json_encode(t('messages.minutes_ago')) ?>,
hoursAgo: <?= json_encode(t('messages.hours_ago')) ?>,
daysAgo: <?= json_encode(t('messages.days_ago')) ?>,
viewProfileTemplate: <?= json_encode(t('messages.view_profile_of', ['name' => '___NAME___'])) ?>,
messageSeen: <?= json_encode(t('messages.seen')) ?>,
messageSeenAt: <?= json_encode(t('messages.seen_at')) ?>,
unsendMessage: <?= json_encode(t('messages.unsend_message')) ?>,
forwardMessage: <?= json_encode(t('messages.forward_message')) ?>,
forwardMessageTitle: <?= json_encode(t('messages.forward_message_title')) ?>,
forwarding: <?= json_encode(t('messages.forwarding')) ?>,
forwardConfirm: <?= json_encode(t('messages.forward_confirm')) ?>,
forwardSuccess: <?= json_encode(t('messages.forward_success')) ?>,
forwardError: <?= json_encode(t('messages.forward_error')) ?>,
searchArtistsForward: <?= json_encode(t('messages.search_artists_forward')) ?>,
noAvailableUsers: <?= json_encode(t('messages.no_available_users')) ?>,
openConversation: <?= json_encode(t('messages.open_conversation')) ?>,
unsendConfirm: <?= json_encode(t('messages.unsend_confirm')) ?>
};
function isMobileView() {
return window.innerWidth <= 768;
}
function openMobileSidebar() {
if (!messagesLayout || !isMobileView()) return;
messagesLayout.classList.add('sidebar-open');
messagesLayout.classList.remove('conversation-active');
const sidebar = document.getElementById('messagesSidebar');
if (sidebar) {
sidebar.classList.add('active');
}
}
function closeMobileSidebar() {
if (!messagesLayout) return;
messagesLayout.classList.remove('sidebar-open');
const sidebar = document.getElementById('messagesSidebar');
if (sidebar) {
sidebar.classList.remove('active');
}
}
function showMobileConversationView() {
if (!messagesLayout || !isMobileView()) return;
messagesLayout.classList.add('conversation-active');
closeMobileSidebar();
}
function resetMobileLayoutState() {
if (!messagesLayout) return;
if (!isMobileView()) {
messagesLayout.classList.remove('sidebar-open', 'conversation-active');
const sidebar = document.getElementById('messagesSidebar');
if (sidebar) {
sidebar.classList.remove('active');
}
return;
}
if (selectedUserId) {
showMobileConversationView();
} else {
openMobileSidebar();
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
messagesLayout = document.getElementById('messagesLayout');
messagesOverlay = document.getElementById('messagesOverlay');
if (messagesOverlay) {
messagesOverlay.addEventListener('click', function() {
closeMobileSidebar();
});
}
loadConversations();
setupEventListeners();
setupMobileNavigation();
resetMobileLayoutState();
window.addEventListener('resize', handleResize);
// Auto-refresh conversations every 20 seconds (optimized for performance)
// Only run when page is visible
refreshInterval = setInterval(function() {
if (!document.hidden) {
loadConversations();
}
}, 20000);
// Auto-refresh active conversation every 5 seconds (optimized from 3 seconds)
// Only run when conversation is open and page is visible
conversationRefreshInterval = setInterval(function() {
if (selectedUserId && !document.hidden) {
refreshActiveConversation();
}
}, 5000);
// DISABLED: Automatic badge updates on messages page to prevent performance issues
// Badge is updated when conversations are loaded, no need for separate interval
// The loadConversations() function already updates the badge via updateHeaderBadgeFromCount()
// Load specific conversation if user_id is in URL
<?php if ($selected_user_id): ?>
selectConversation(<?= (int)$selected_user_id ?>);
<?php endif; ?>
});
function handleResize() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(resetMobileLayoutState, 150);
}
// Mobile navigation functions
function setupMobileNavigation() {
const sidebar = document.getElementById('messagesSidebar');
const closeSidebarBtn = document.getElementById('closeSidebarBtn');
const backToConversationsBtn = document.getElementById('backToConversationsBtn');
const conversationView = document.getElementById('conversationView');
// Close sidebar button
if (closeSidebarBtn) {
closeSidebarBtn.addEventListener('click', function() {
closeMobileSidebar();
});
}
// Back to conversations button
if (backToConversationsBtn) {
backToConversationsBtn.addEventListener('click', function() {
if (window.innerWidth <= 768) {
// On mobile, show sidebar and hide conversation
openMobileSidebar();
conversationView.style.display = 'none';
document.getElementById('noConversation').style.display = 'flex';
selectedUserId = null;
loadConversations();
}
});
}
// Show sidebar when clicking conversation items on mobile
document.addEventListener('click', function(e) {
if (window.innerWidth <= 768) {
if (e.target.closest('.conversation-item')) {
// Sidebar will be hidden by CSS when conversation is shown
setTimeout(() => {
closeMobileSidebar();
}, 300);
}
}
});
}
function setupEventListeners() {
// New message button
document.getElementById('newMessageBtn').addEventListener('click', openNewMessageModal);
document.getElementById('closeModal').addEventListener('click', closeNewMessageModal);
// Forward message modal
const closeForwardModal = document.getElementById('closeForwardModal');
const cancelForwardBtn = document.getElementById('cancelForwardBtn');
if (closeForwardModal) {
closeForwardModal.addEventListener('click', closeForwardMessageModal);
}
if (cancelForwardBtn) {
cancelForwardBtn.addEventListener('click', closeForwardMessageModal);
}
// Forward message search
const forwardSearch = document.getElementById('forwardMessageSearch');
if (forwardSearch) {
forwardSearch.addEventListener('input', debounceForwardMessageSearch);
}
// Close forward modal on outside click
const forwardModal = document.getElementById('forwardMessageModal');
if (forwardModal) {
forwardModal.addEventListener('click', function(e) {
if (e.target === this) {
closeForwardMessageModal();
}
});
}
// Message form
document.getElementById('messageForm').addEventListener('submit', sendMessage);
// Search users
document.getElementById('searchUsers').addEventListener('input', debounceSearchUsers);
document.getElementById('newMessageSearch').addEventListener('input', debounceNewMessageSearch);
// Mark unseen button
document.getElementById('markUnseenBtn').addEventListener('click', markConversationUnseen);
// Delete conversation button
document.getElementById('deleteConversationBtn').addEventListener('click', deleteConversation);
// Close modal on outside click
document.getElementById('newMessageModal').addEventListener('click', function(e) {
if (e.target === this) {
closeNewMessageModal();
}
});
// Enter key to send message
document.getElementById('messageInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage(e);
}
});
// Show sidebar button on mobile
const showSidebarBtn = document.getElementById('showSidebarBtn');
if (showSidebarBtn) {
showSidebarBtn.addEventListener('click', function() {
openMobileSidebar();
});
}
// Show sidebar on mobile when clicking new message button
if (window.innerWidth <= 768) {
document.getElementById('newMessageBtn').addEventListener('click', function() {
openMobileSidebar();
});
}
// Initial mobile state - show sidebar if no conversation selected
if (window.innerWidth <= 768 && !selectedUserId) {
openMobileSidebar();
}
}
function loadConversations() {
fetch('api/messages.php?action=get_conversations')
.then(response => response.json())
.then(data => {
if (data.success) {
// Check for new messages before rendering
checkForNewMessages(data.conversations);
renderConversations(data.conversations);
// Calculate total unread count from conversations and update header badge
const totalUnread = data.conversations.reduce((sum, conv) => sum + (conv.unread_count || 0), 0);
updateHeaderBadgeFromCount(totalUnread);
// No need to call global updateMessagesBadge() - updateHeaderBadgeFromCount() already handles it
// This prevents redundant API calls
} else {
document.getElementById('conversationsList').innerHTML =
'<div class="loading">' + translations.noConversations + '</div>';
}
})
.catch(error => {
console.error('Error loading conversations:', error);
document.getElementById('conversationsList').innerHTML =
'<div class="loading">' + translations.errorLoadingConversations + '</div>';
});
}
// Update header badge with specific count (used on messages page)
function updateHeaderBadgeFromCount(count) {
const badge = document.getElementById('messagesBadge');
if (!badge) return;
if (count > 0) {
const displayCount = count > 99 ? '99+' : count.toString();
badge.textContent = displayCount;
badge.style.display = 'flex';
badge.style.color = 'white';
badge.style.fontSize = displayCount.length > 1 ? '0.65rem' : '0.7rem';
badge.style.minWidth = displayCount.length > 1 ? '24px' : '20px';
// Add double-digit or triple-digit classes for proper sizing
badge.classList.remove('double-digit', 'triple-digit');
if (displayCount.length === 2) {
badge.classList.add('double-digit');
} else if (displayCount.length >= 3) {
badge.classList.add('triple-digit');
}
// Add pulse animation if not already present
if (!badge.classList.contains('pulse')) {
badge.classList.add('pulse');
}
} else {
badge.style.display = 'none';
badge.classList.remove('pulse', 'double-digit', 'triple-digit');
}
}
function checkForNewMessages(conversations) {
// Only check if page is visible (not in background tab)
if (document.hidden) {
// Still update counts but don't play sound
conversations.forEach(conv => {
previousConversationCounts[conv.other_user_id] = conv.unread_count || 0;
});
return;
}
// Check if any conversation has new unread messages
let hasNewMessage = false;
conversations.forEach(conv => {
const convId = conv.other_user_id;
const currentUnread = conv.unread_count || 0;
const previousUnread = previousConversationCounts[convId] || 0;
// If unread count increased and we're not viewing this conversation
if (currentUnread > previousUnread && selectedUserId != convId) {
hasNewMessage = true;
}
// Update the stored count
previousConversationCounts[convId] = currentUnread;
});
// Play sound if new message detected
if (hasNewMessage) {
playMessageSound();
}
}
function renderConversations(conversations) {
const container = document.getElementById('conversationsList');
if (conversations.length === 0) {
container.innerHTML = '<div class="loading">' + translations.noConversations + '</div>';
return;
}
container.innerHTML = conversations.map(conv => {
const timeAgo = getTimeAgo(conv.last_message_time);
const isActive = selectedUserId == conv.other_user_id;
return `
<div class="conversation-item ${isActive ? 'active' : ''}"
onclick="selectConversation(${conv.other_user_id})">
<a href="/artist_profile.php?id=${conv.other_user_id}"
onclick="event.stopPropagation();"
class="thread-avatar-link"
title="${formatProfileTitle(conv.other_user_name)}">
<div class="thread-avatar">
${isValidImageUrl(conv.avatar) ?
`<img src="${conv.avatar}" alt="${conv.other_user_name}" onerror="this.parentElement.textContent='${conv.avatar.replace(/'/g, "\\'")}'">` :
conv.avatar}
</div>
</a>
<div class="thread-info">
<div class="thread-name">${escapeHtml(conv.other_user_name)}</div>
<div class="thread-preview">${escapeHtml(conv.last_message)}</div>
<div class="thread-time">${timeAgo}</div>
</div>
${conv.unread_count > 0 ? `<div class="unread-badge">${conv.unread_count}</div>` : ''}
</div>
`;
}).join('');
}
function selectConversation(userId) {
selectedUserId = userId;
lastMessageId = null; // Reset last message ID when switching conversations
// Reset unread count for this conversation when viewing it
if (previousConversationCounts[userId]) {
previousConversationCounts[userId] = 0;
}
// On mobile, hide sidebar and show conversation
showMobileConversationView();
loadConversation(userId);
loadConversations(); // Refresh to update active state
}
function loadConversation(userId) {
document.getElementById('noConversation').style.display = 'none';
const conversationView = document.getElementById('conversationView');
conversationView.style.display = 'flex';
// Update mobile header title
fetch(`api/messages.php?action=get_messages&user_id=${userId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('receiverId').value = userId;
renderMessages(data.messages).catch(err => console.error('Error rendering messages:', err));
renderConversationHeader(data.other_user);
// Update mobile conversation title
const mobileTitle = document.getElementById('mobileConversationTitle');
if (mobileTitle && data.other_user) {
mobileTitle.textContent = data.other_user.name || '';
}
// Store last message ID for detecting new messages
if (data.messages && data.messages.length > 0) {
lastMessageId = data.messages[data.messages.length - 1].id;
}
scrollToBottom();
// Update header badge after loading conversation (messages are marked as read)
// Reload conversations to get updated unread counts
loadConversations();
} else {
alert(translations.errorLoadingMessages + ': ' + (data.message || ''));
}
})
.catch(error => {
console.error('Error loading conversation:', error);
alert(translations.errorLoadingMessages);
});
}
// Refresh active conversation to check for new messages
function refreshActiveConversation() {
if (!selectedUserId) return;
fetch(`api/messages.php?action=get_messages&user_id=${selectedUserId}`)
.then(response => response.json())
.then(data => {
if (!data.success || !Array.isArray(data.messages)) {
return;
}
const messages = data.messages;
const container = document.getElementById('messagesList');
const shouldAutoScroll = isScrolledToBottom(container);
const hasMessages = messages.length > 0;
const currentLastMessageId = hasMessages ? messages[messages.length - 1].id : null;
const newLastReadId = getLastReadMessageId(messages);
const messageCountChanged = messages.length !== currentMessages.length;
// Update online status if other_user data is available
if (data.other_user) {
const statusText = data.other_user.is_online === true ? translations.online : translations.offline;
const statusEl = document.getElementById('conversationUserStatus');
if (statusEl) {
statusEl.textContent = statusText;
// Add/remove online class for styling
if (data.other_user.is_online === true) {
statusEl.classList.add('online');
} else {
statusEl.classList.remove('online');
}
}
}
if (lastMessageId && currentLastMessageId && currentLastMessageId > lastMessageId) {
const newMessages = messages.filter(msg => msg.id > lastMessageId);
if (newMessages.length > 0) {
appendNewMessages(newMessages, shouldAutoScroll).catch(err => console.error('Error appending messages:', err));
lastMessageId = currentLastMessageId;
if (!document.hidden) {
playMessageSound();
}
}
} else if (!lastMessageId && currentLastMessageId) {
// First load - set the last message ID and render
lastMessageId = currentLastMessageId;
renderMessages(messages, { autoScroll: shouldAutoScroll }).catch(err => console.error('Error rendering messages:', err));
} else if (!hasMessages) {
// Conversation cleared
lastMessageId = null;
renderMessages(messages, { autoScroll: false }).catch(err => console.error('Error rendering messages:', err));
} else if (messageCountChanged || newLastReadId !== lastReadMessageId) {
// Re-render to update read receipts or reflect message deletions
renderMessages(messages, { autoScroll: shouldAutoScroll }).catch(err => console.error('Error rendering messages:', err));
if (currentLastMessageId) {
lastMessageId = currentLastMessageId;
}
}
})
.catch(error => {
console.error('Error refreshing conversation:', error);
});
}
// Append new messages to the conversation view
async function appendNewMessages(newMessages, autoScroll = true) {
if (!Array.isArray(newMessages) || newMessages.length === 0) {
return;
}
currentMessages = currentMessages.concat(newMessages);
await renderMessages(currentMessages, { autoScroll });
// Update header badge
if (window.updateMessagesBadge) {
window.updateMessagesBadge();
}
// Refresh conversations list to update unread counts
loadConversations();
}
// Check if user is scrolled to bottom of messages
function isScrolledToBottom(element) {
if (!element) return true;
const threshold = 100; // 100px threshold
return element.scrollHeight - element.scrollTop - element.clientHeight < threshold;
}
function getLastReadMessageId(messages) {
if (!Array.isArray(messages)) return null;
let lastId = null;
messages.forEach(msg => {
const isRead = msg.is_read === true || msg.is_read === 1;
if (msg.is_sender && isRead) {
lastId = msg.id;
}
});
return lastId;
}
// Cache for link previews
const linkPreviewCache = {};
// Function to detect URLs in text
function detectUrls(text) {
const urlRegex = /(https?:\/\/[^\s]+)/g;
return text.match(urlRegex) || [];
}
// Function to fetch link preview
async function fetchLinkPreview(url) {
if (linkPreviewCache[url]) {
return linkPreviewCache[url];
}
try {
const response = await fetch(`api/get_link_preview.php?url=${encodeURIComponent(url)}`);
const data = await response.json();
if (data.success && data.preview) {
linkPreviewCache[url] = data.preview;
return data.preview;
}
} catch (error) {
console.error('Error fetching link preview:', error);
}
return null;
}
// Function to format message with link previews
function formatMessageWithLinks(messageText) {
const urls = detectUrls(messageText);
if (urls.length === 0) {
return escapeHtml(messageText);
}
let formattedText = messageText;
urls.forEach(url => {
const displayUrl = url.length > 50 ? url.substring(0, 50) + '...' : url;
const linkHtml = `<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" class="message-link">🔗 ${escapeHtml(displayUrl)}</a>`;
formattedText = formattedText.replace(url, linkHtml);
});
return formattedText;
}
// Function to render link preview HTML
function renderLinkPreview(preview, url) {
if (!preview || (!preview.title && !preview.description && !preview.image)) {
return '';
}
const title = preview.title || preview.site_name || 'Link';
const description = preview.description || '';
const image = preview.image || '';
const siteName = preview.site_name || '';
const hasImage = !!image;
return `
<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" class="link-preview" data-url="${escapeHtml(url)}">
${hasImage ? `
<div class="link-preview-image">
<img src="${escapeHtml(image)}" alt="${escapeHtml(title)}" onerror="this.parentElement.style.display='none'">
<div class="link-preview-icon">🔗</div>
</div>
` : '<div class="link-preview-icon">🔗</div>'}
<div class="link-preview-content">
${siteName ? `<div class="link-preview-site">${escapeHtml(siteName)}</div>` : ''}
<div class="link-preview-title">${escapeHtml(title)}</div>
${description ? `<div class="link-preview-description">${escapeHtml(description)}</div>` : ''}
</div>
</a>
`;
}
function buildMessageHTML(msg, latestReadId) {
const isSent = msg.is_sender;
const time = formatTime(msg.created_at);
const senderId = msg.sender_id || (isSent ? currentUserId : selectedUserId);
const isReadReceiptVisible = Boolean(latestReadId && isSent && msg.id === latestReadId);
let readReceiptContent = translations.messageSeen || 'Seen';
if (translations.messageSeenAt && msg.read_at) {
const formattedSeenTime = formatFullDateTime(msg.read_at);
if (formattedSeenTime) {
readReceiptContent = translations.messageSeenAt.replace(':time', formattedSeenTime);
}
}
const readReceipt = isReadReceiptVisible ? `<span class="message-read-status"><i class="fas fa-check-double"></i> ${readReceiptContent}</span>` : '';
// Detect URLs and format message
const urls = detectUrls(msg.message);
const formattedMessage = formatMessageWithLinks(msg.message);
const firstUrl = urls.length > 0 ? urls[0] : null;
const linkPreviewId = firstUrl ? `link-preview-${msg.id}` : null;
return `
<div class="message-item ${isSent ? 'sent' : 'received'}" data-message-id="${msg.id}">
<a href="/artist_profile.php?id=${senderId}"
onclick="event.stopPropagation();"
class="message-avatar-link"
title="${formatProfileTitle(msg.sender_name)}">
<div class="message-avatar">
${isValidImageUrl(msg.sender_avatar) ?
`<img src="${msg.sender_avatar}" alt="${msg.sender_name}" onerror="this.parentElement.textContent='${msg.sender_avatar.replace(/'/g, "\\'")}'">` :
msg.sender_avatar}
</div>
</a>
<div class="message-content">
<div class="message-bubble">
${formattedMessage}
${firstUrl ? `<div id="${linkPreviewId}" class="link-preview-container"></div>` : ''}
${isSent ? `
<button class="message-forward-btn"
onclick="forwardMessage(${msg.id}, event)"
title="${translations.forwardMessage}"
aria-label="${translations.forwardMessage}">
<i class="fas fa-share"></i>
</button>
<button class="message-unsend-btn"
onclick="unsendMessage(${msg.id}, event)"
title="${translations.unsendMessage}"
aria-label="${translations.unsendMessage}">
<i class="fas fa-trash-alt"></i>
</button>
` : ''}
</div>
<div class="message-meta">
<span class="message-time">${time}</span>
${readReceipt}
</div>
</div>
</div>
`;
}
async function renderMessages(messages, options = {}) {
const container = document.getElementById('messagesList');
if (!container) return;
const autoScroll = Object.prototype.hasOwnProperty.call(options, 'autoScroll') ? options.autoScroll : true;
let distanceFromBottom = 0;
if (!autoScroll) {
distanceFromBottom = container.scrollHeight - container.scrollTop;
}
currentMessages = Array.isArray(messages) ? [...messages] : [];
lastReadMessageId = getLastReadMessageId(currentMessages);
if (currentMessages.length === 0) {
lastReadMessageId = null;
container.innerHTML = '<div class="loading">' + translations.noMessages + '</div>';
} else {
container.innerHTML = currentMessages
.map(msg => buildMessageHTML(msg, lastReadMessageId))
.join('');
// Fetch and render link previews for messages with URLs
currentMessages.forEach((msg) => {
const urls = detectUrls(msg.message);
if (urls.length > 0) {
const firstUrl = urls[0];
const previewContainer = document.getElementById(`link-preview-${msg.id}`);
if (previewContainer) {
// Fetch preview asynchronously
fetchLinkPreview(firstUrl).then(preview => {
if (preview && previewContainer) {
previewContainer.innerHTML = renderLinkPreview(preview, firstUrl);
}
}).catch(error => {
console.error('Error loading link preview:', error);
});
}
}
});
}
if (autoScroll) {
scrollToBottom();
} else {
setTimeout(() => {
container.scrollTop = Math.max(0, container.scrollHeight - distanceFromBottom);
}, 0);
}
}
function renderConversationHeader(user) {
document.getElementById('conversationUserName').textContent = user.name;
// Use actual online status if available, otherwise default to offline
const statusText = user.is_online === true ? translations.online : translations.offline;
const statusEl = document.getElementById('conversationUserStatus');
statusEl.textContent = statusText;
// Add/remove online class for styling
if (user.is_online === true) {
statusEl.classList.add('online');
} else {
statusEl.classList.remove('online');
}
const avatarEl = document.getElementById('conversationAvatar');
const userId = user.id || selectedUserId;
if (!userId) {
console.warn('No user ID available for conversation header');
}
if (isValidImageUrl(user.avatar)) {
avatarEl.innerHTML = userId ? `<a href="/artist_profile.php?id=${userId}" onclick="event.stopPropagation();" class="user-avatar-link" title="${formatProfileTitle(user.name)}"><img src="${user.avatar}" alt="${user.name}" onerror="this.parentElement.textContent='${user.avatar.replace(/'/g, "\\'")}'"></a>` : `<img src="${user.avatar}" alt="${user.name}" onerror="this.parentElement.textContent='${user.avatar.replace(/'/g, "\\'")}'">`;
} else {
avatarEl.innerHTML = userId ? `<a href="/artist_profile.php?id=${userId}" onclick="event.stopPropagation();" class="user-avatar-link" title="${formatProfileTitle(user.name)}">${user.avatar}</a>` : user.avatar;
}
}
function sendMessage(e) {
e.preventDefault();
const messageInput = document.getElementById('messageInput');
const message = messageInput.value.trim();
const receiverId = document.getElementById('receiverId').value;
if (!message || !receiverId) {
return;
}
const sendBtn = document.getElementById('sendBtn');
sendBtn.disabled = true;
const formData = new FormData();
formData.append('action', 'send_message');
formData.append('receiver_id', receiverId);
formData.append('message', message);
fetch('api/messages.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
messageInput.value = '';
// Reload conversation to show the new message
loadConversation(receiverId);
loadConversations();
// Update header badge after sending message
if (window.updateMessagesBadge) {
window.updateMessagesBadge();
}
} else {
alert(translations.errorSendingMessage + ': ' + (data.message || ''));
}
})
.catch(error => {
console.error('Error sending message:', error);
alert(translations.errorSendingMessage);
})
.finally(() => {
sendBtn.disabled = false;
});
}
function openNewMessageModal() {
document.getElementById('newMessageModal').classList.add('active');
document.getElementById('newMessageSearch').focus();
}
function closeNewMessageModal() {
document.getElementById('newMessageModal').classList.remove('active');
document.getElementById('newMessageSearch').value = '';
document.getElementById('newMessageUsers').innerHTML = '';
}
function debounceSearchUsers(e) {
clearTimeout(searchTimeout);
const query = e.target.value.trim();
if (query.length < 2) {
document.getElementById('searchResults').classList.remove('active');
return;
}
searchTimeout = setTimeout(() => searchUsers(query), 300);
}
function debounceNewMessageSearch(e) {
clearTimeout(searchTimeout);
const query = e.target.value.trim();
if (query.length < 2) {
document.getElementById('newMessageUsers').innerHTML = '';
return;
}
searchTimeout = setTimeout(() => searchUsersForNewMessage(query), 300);
}
function searchUsers(query) {
fetch(`api/messages.php?action=search_users&q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
if (data.success) {
renderSearchResults(data.users);
}
})
.catch(error => console.error('Error searching users:', error));
}
function searchUsersForNewMessage(query) {
fetch(`api/messages.php?action=search_users&q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
if (data.success) {
renderNewMessageUsers(data.users);
}
})
.catch(error => console.error('Error searching users:', error));
}
function renderSearchResults(users) {
const container = document.getElementById('searchResults');
if (users.length === 0) {
container.innerHTML = '<div class="search-result-item">' + translations.noUsersFound + '</div>';
container.classList.add('active');
return;
}
container.innerHTML = users.map(user => `
<div class="search-result-item" onclick="selectConversation(${user.id}); document.getElementById('searchUsers').value = ''; document.getElementById('searchResults').classList.remove('active');">
<a href="/artist_profile.php?id=${user.id}"
onclick="event.stopPropagation();"
class="thread-avatar-link"
title="${formatProfileTitle(user.name)}">
<div class="thread-avatar">
${isValidImageUrl(user.avatar) ?
`<img src="${user.avatar}" alt="${user.name}" onerror="this.parentElement.textContent='${user.avatar.replace(/'/g, "\\'")}'">` :
user.avatar}
</div>
</a>
<div class="thread-info">
<div class="thread-name">${escapeHtml(user.name)}</div>
${user.bio ? `<div class="thread-preview">${escapeHtml(user.bio)}</div>` : ''}
</div>
</div>
`).join('');
container.classList.add('active');
}
function renderNewMessageUsers(users) {
const container = document.getElementById('newMessageUsers');
if (users.length === 0) {
container.innerHTML = '<div class="loading">' + translations.noUsersFound + '</div>';
return;
}
container.innerHTML = users.map(user => `
<div class="user-item" onclick="startConversation(${user.id})">
<a href="/artist_profile.php?id=${user.id}"
onclick="event.stopPropagation();"
class="thread-avatar-link"
title="${formatProfileTitle(user.name)}">
<div class="thread-avatar">
${isValidImageUrl(user.avatar) ?
`<img src="${user.avatar}" alt="${user.name}" onerror="this.parentElement.textContent='${user.avatar.replace(/'/g, "\\'")}'">` :
user.avatar}
</div>
</a>
<div class="thread-info">
<div class="thread-name">${escapeHtml(user.name)}</div>
${user.bio ? `<div class="thread-preview">${escapeHtml(user.bio)}</div>` : ''}
</div>
</div>
`).join('');
}
function startConversation(userId) {
closeNewMessageModal();
selectConversation(userId);
}
function markConversationUnseen() {
if (!selectedUserId) {
return;
}
const formData = new FormData();
formData.append('action', 'mark_unseen');
formData.append('user_id', selectedUserId);
fetch('api/messages.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Reload conversations list to update unread counts
loadConversations();
// Update header badge
if (window.updateMessagesBadge) {
window.updateMessagesBadge();
}
// Show success message
showNotification(translations.conversationMarkedUnread, 'success');
} else {
alert(translations.errorMarkingUnread + ': ' + (data.message || ''));
}
})
.catch(error => {
console.error('Error marking conversation as unread:', error);
alert(translations.errorMarkingUnread);
});
}
function deleteConversation() {
if (!selectedUserId) {
return;
}
// Confirm deletion
if (!confirm(translations.deleteConfirm)) {
return;
}
const formData = new FormData();
formData.append('action', 'delete_conversation');
formData.append('user_id', selectedUserId);
fetch('api/messages.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Clear the conversation view
selectedUserId = null;
currentMessages = [];
lastMessageId = null;
lastReadMessageId = null;
document.getElementById('noConversation').style.display = 'flex';
document.getElementById('conversationView').style.display = 'none';
resetMobileLayoutState();
// Reload conversations list
loadConversations();
// Update header badge
if (window.updateMessagesBadge) {
window.updateMessagesBadge();
}
// Show success message
showNotification(translations.conversationDeleted, 'success');
} else {
alert(translations.errorDeleting + ': ' + (data.message || ''));
}
})
.catch(error => {
console.error('Error deleting conversation:', error);
alert(translations.errorDeleting);
});
}
function showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? 'rgba(46, 213, 115, 0.9)' : 'rgba(255, 71, 87, 0.9)'};
color: white;
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 10000;
animation: slideIn 0.3s ease;
`;
document.body.appendChild(notification);
// Remove after 3 seconds
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
function scrollToBottom() {
const messagesList = document.getElementById('messagesList');
if (!messagesList) return;
setTimeout(() => {
messagesList.scrollTop = messagesList.scrollHeight;
}, 100);
}
function getTimeAgo(timestamp) {
const now = new Date();
const time = new Date(timestamp);
const diff = Math.floor((now - time) / 1000);
if (diff < 60) return translations.justNow;
if (diff < 3600) {
const minutes = Math.floor(diff / 60);
return translations.minutesAgo.replace(':count', minutes);
}
if (diff < 86400) {
const hours = Math.floor(diff / 3600);
return translations.hoursAgo.replace(':count', hours);
}
if (diff < 604800) {
const days = Math.floor(diff / 86400);
return translations.daysAgo.replace(':count', days);
}
return time.toLocaleDateString();
}
function formatTime(timestamp) {
const time = new Date(timestamp);
return time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
function parseTimestamp(timestamp) {
if (!timestamp) return null;
let normalized = timestamp.replace(' ', 'T');
let date = new Date(normalized);
if (isNaN(date.getTime())) {
date = new Date(timestamp);
}
return isNaN(date.getTime()) ? null : date;
}
function formatFullDateTime(timestamp) {
const date = parseTimestamp(timestamp);
if (!date) return '';
const datePart = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
const timePart = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
return `${datePart} ${timePart}`;
}
function formatProfileTitle(name) {
const template = translations.viewProfileTemplate || '';
const safeName = name || '';
if (!template) {
return escapeHtml(safeName);
}
return escapeHtml(template.replace('___NAME___', safeName));
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function isValidImageUrl(url) {
if (!url) return false;
// Check if it's a URL (http/https) or a valid relative path starting with /
return url.startsWith('http://') ||
url.startsWith('https://') ||
url.startsWith('/') ||
url.match(/^[a-z0-9_]+\/[^\/]+\.(jpg|jpeg|png|gif|webp)$/i);
}
// Variable to store message being forwarded
let messageToForward = null;
// Function to forward a message
function forwardMessage(messageId, event) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
// Find the message in currentMessages
const message = currentMessages.find(msg => msg.id === messageId);
if (!message) {
alert('Message not found');
return;
}
messageToForward = message;
// Show the forward modal
const modal = document.getElementById('forwardMessageModal');
const preview = document.getElementById('forwardMessagePreview');
// Display message preview
if (preview) {
const messageText = message.message || '';
const truncatedText = messageText.length > 100 ? messageText.substring(0, 100) + '...' : messageText;
preview.innerHTML = `
<div style="font-weight: 600; margin-bottom: 0.5rem; color: white;">${translations.forwarding}</div>
<div>${escapeHtml(truncatedText)}</div>
`;
}
// Clear search and users list
const searchInput = document.getElementById('forwardMessageSearch');
const usersList = document.getElementById('forwardMessageUsers');
if (searchInput) searchInput.value = '';
if (usersList) usersList.innerHTML = '';
// Show modal
if (modal) {
modal.classList.add('active');
if (searchInput) searchInput.focus();
}
}
function closeForwardMessageModal() {
const modal = document.getElementById('forwardMessageModal');
if (modal) {
modal.classList.remove('active');
}
messageToForward = null;
const searchInput = document.getElementById('forwardMessageSearch');
const usersList = document.getElementById('forwardMessageUsers');
if (searchInput) searchInput.value = '';
if (usersList) usersList.innerHTML = '';
}
function debounceForwardMessageSearch(e) {
clearTimeout(searchTimeout);
const query = e.target.value.trim();
if (query.length < 2) {
const usersList = document.getElementById('forwardMessageUsers');
if (usersList) usersList.innerHTML = '';
return;
}
searchTimeout = setTimeout(() => searchUsersForForward(query), 300);
}
function searchUsersForForward(query) {
fetch(`api/messages.php?action=search_users&q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
if (data.success) {
renderForwardMessageUsers(data.users);
}
})
.catch(error => console.error('Error searching users:', error));
}
function renderForwardMessageUsers(users) {
const container = document.getElementById('forwardMessageUsers');
if (!container) return;
if (users.length === 0) {
container.innerHTML = '<div class="loading">' + translations.noUsersFound + '</div>';
return;
}
// Filter out current user and the user the message was originally sent to/received from
const filteredUsers = users.filter(user => {
if (user.id == currentUserId) return false;
if (messageToForward && selectedUserId && user.id == selectedUserId) return false;
return true;
});
if (filteredUsers.length === 0) {
container.innerHTML = '<div class="loading">' + translations.noAvailableUsers + '</div>';
return;
}
container.innerHTML = filteredUsers.map(user => `
<div class="user-item" onclick="confirmForwardMessage(${user.id}, '${escapeHtml(user.name)}')">
<a href="/artist_profile.php?id=${user.id}"
onclick="event.stopPropagation();"
class="thread-avatar-link"
title="${formatProfileTitle(user.name)}">
<div class="thread-avatar">
${isValidImageUrl(user.avatar) ?
`<img src="${user.avatar}" alt="${user.name}" onerror="this.parentElement.textContent='${user.avatar.replace(/'/g, "\\'")}'">` :
user.avatar}
</div>
</a>
<div class="thread-info">
<div class="thread-name">${escapeHtml(user.name)}</div>
${user.bio ? `<div class="thread-preview">${escapeHtml(user.bio)}</div>` : ''}
</div>
</div>
`).join('');
}
function confirmForwardMessage(userId, userName) {
if (!messageToForward) {
alert('No message selected to forward');
return;
}
const confirmText = translations.forwardConfirm.replace(':name', userName);
if (!confirm(confirmText)) {
return;
}
const formData = new FormData();
formData.append('action', 'forward_message');
formData.append('message_id', messageToForward.id);
formData.append('receiver_id', userId);
fetch('api/messages.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
closeForwardMessageModal();
showNotification(translations.forwardSuccess, 'success');
// Optionally open the conversation with the user
const openConversationText = translations.openConversation.replace(':name', userName);
if (confirm(openConversationText)) {
selectConversation(userId);
}
} else {
alert(translations.forwardError + ': ' + (data.message || ''));
}
})
.catch(error => {
console.error('Error forwarding message:', error);
alert('An error occurred while forwarding the message');
});
}
// Function to unsend/delete a message
function unsendMessage(messageId, event) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
// Confirm deletion
if (!confirm(translations.unsendConfirm)) {
return;
}
const messageElement = document.querySelector(`[data-message-id="${messageId}"]`);
if (!messageElement) {
return;
}
// Disable the button and show loading state
const unsendBtn = messageElement.querySelector('.message-unsend-btn');
if (unsendBtn) {
unsendBtn.disabled = true;
unsendBtn.style.opacity = '0.5';
unsendBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
}
const formData = new FormData();
formData.append('action', 'delete_message');
formData.append('message_id', messageId);
fetch('api/messages.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Remove the message from the UI with animation
messageElement.style.opacity = '0';
messageElement.style.transform = 'scale(0.95)';
messageElement.style.transition = 'all 0.3s ease';
messageElement.style.pointerEvents = 'none';
setTimeout(() => {
messageElement.remove();
// Remove from currentMessages array
currentMessages = currentMessages.filter(msg => msg.id !== messageId);
// Update lastMessageId if needed
if (lastMessageId === messageId && currentMessages.length > 0) {
lastMessageId = currentMessages[currentMessages.length - 1].id;
} else if (currentMessages.length === 0) {
lastMessageId = null;
}
// If no messages left, show empty state
const messagesList = document.getElementById('messagesList');
if (messagesList && currentMessages.length === 0) {
messagesList.innerHTML = '<div class="loading">' + translations.noMessages + '</div>';
}
}, 300);
// Refresh conversations list to update last message preview
loadConversations();
} else {
// Re-enable button on error
if (unsendBtn) {
unsendBtn.disabled = false;
unsendBtn.style.opacity = '1';
unsendBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
}
alert('Error: ' + (data.message || 'Failed to unsend message'));
}
})
.catch(error => {
console.error('Error unsending message:', error);
// Re-enable button on error
if (unsendBtn) {
unsendBtn.disabled = false;
unsendBtn.style.opacity = '1';
unsendBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
}
alert('An error occurred while trying to unsend the message');
});
}
// Play notification sound for new messages
function playMessageSound() {
try {
// Create audio context for generating a notification sound
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
// Connect nodes
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
// Configure sound - pleasant notification tone
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(600, audioContext.currentTime + 0.1);
// Volume envelope
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(0.3, audioContext.currentTime + 0.01);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.15);
// Play two quick beeps
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.15);
// Second beep after short delay
setTimeout(() => {
const oscillator2 = audioContext.createOscillator();
const gainNode2 = audioContext.createGain();
oscillator2.connect(gainNode2);
gainNode2.connect(audioContext.destination);
oscillator2.frequency.setValueAtTime(800, audioContext.currentTime);
oscillator2.frequency.exponentialRampToValueAtTime(600, audioContext.currentTime + 0.1);
gainNode2.gain.setValueAtTime(0, audioContext.currentTime);
gainNode2.gain.linearRampToValueAtTime(0.3, audioContext.currentTime + 0.01);
gainNode2.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.15);
oscillator2.start(audioContext.currentTime);
oscillator2.stop(audioContext.currentTime + 0.15);
}, 150);
} catch (error) {
// Fallback: Try using HTML5 Audio API with data URI
try {
const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIG2m98OScTQ8OUKjk8bVkHAU6kdfyzHksBSR3x/DdkEAKFF606euoVRQKRp/g8r5sIQUrgc7y2Yk2CBtpvfDknE0PDlCo5PG1ZBwFOpHX8sx5LAUkd8fw3ZBAC');
audio.volume = 0.3;
audio.play().catch(e => console.log('Could not play notification sound:', e));
} catch (e) {
console.log('Audio notification not available');
}
}
}
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
if (refreshInterval) {
clearInterval(refreshInterval);
}
if (conversationRefreshInterval) {
clearInterval(conversationRefreshInterval);
}
});
</script>
<?php include 'includes/footer.php'; ?>