1 <?php 2 require_once __DIR__ . '/../includes/bootstrap.php'; 3 must_login(); 4 5 $id = (int)get('id'); 6 $msg = DB::row(" 7 SELECT m.*, 8 s.username AS sname, s.avatar AS sav, s.role AS srole, 9 s.karma AS skarma, s.last_seen AS s_last_seen, 10 r.username AS rname, r.avatar AS rav, r.role AS rrole, 11 r.last_seen AS r_last_seen 12 FROM messages m 13 JOIN users s ON s.id = m.sender_id 14 JOIN users r ON r.id = m.receiver_id 15 WHERE m.id = ? 16 ", [$id]); 17 18 if (!$msg) render_404(); 19 if ($msg['sender_id'] !== $USER['id'] && $msg['receiver_id'] !== $USER['id']) render_403(); 20 if ($msg['receiver_id'] === $USER['id'] && $msg['deleted_by_receiver']) render_404(); 21 if ($msg['sender_id'] === $USER['id'] && $msg['deleted_by_sender']) render_404(); 22 23 // Mark as read 24 if ($msg['receiver_id'] === $USER['id'] && !$msg['is_read']) { 25 DB::run('UPDATE messages SET `is_read`=1 WHERE id=?', [$id]); 26 } 27 28 // Determine the "other" person 29 $otherId = $msg['sender_id'] === $USER['id'] ? $msg['receiver_id'] : $msg['sender_id']; 30 $otherName = $msg['sender_id'] === $USER['id'] ? $msg['rname'] : $msg['sname']; 31 $otherAv = $msg['sender_id'] === $USER['id'] ? $msg['rav'] : $msg['sav']; 32 $otherRole = $msg['sender_id'] === $USER['id'] ? $msg['rrole'] : $msg['srole']; 33 $otherSeen = $msg['sender_id'] === $USER['id'] ? $msg['r_last_seen'] : $msg['s_last_seen']; 34 $otherOnline = strtotime($otherSeen) >= (time() - 900); 35 36 // Conversation thread (all messages between these two users) 37 function conv_id_local(int $a, int $b): string { return min($a,$b).'-'.max($a,$b); } 38 $convId = conv_id_local((int)$USER['id'], $otherId); 39 $thread = DB::rows(" 40 SELECT m.*, u.username AS sender_name, u.avatar AS sender_av 41 FROM messages m JOIN users u ON u.id = m.sender_id 42 WHERE (m.sender_id=? AND m.receiver_id=?) 43 OR (m.sender_id=? AND m.receiver_id=?) 44 ORDER BY m.created_at ASC 45 ", [$USER['id'], $otherId, $otherId, $USER['id']]); 46 47 // Handle POST actions 48 if ($_SERVER['REQUEST_METHOD'] === 'POST' && csrf_ok()) { 49 $action = post('action'); 50 51 if ($action === 'delete_msg') { 52 $delId = (int)post('del_id'); 53 $dm = DB::row('SELECT * FROM messages WHERE id=?', [$delId]); 54 if ($dm && ($dm['sender_id'] === $USER['id'] || $dm['receiver_id'] === $USER['id'])) { 55 if ($dm['sender_id'] === $USER['id']) DB::run('UPDATE messages SET deleted_by_sender=1 WHERE id=?', [$delId]); 56 if ($dm['receiver_id'] === $USER['id']) DB::run('UPDATE messages SET deleted_by_receiver=1 WHERE id=?', [$delId]); 57 } 58 go('messages/view.php?id=' . $id); 59 } 60 61 if ($action === 'delete') { 62 if ($msg['receiver_id'] === $USER['id']) 63 DB::run('UPDATE messages SET deleted_by_receiver=1 WHERE id=?', [$id]); 64 else 65 DB::run('UPDATE messages SET deleted_by_sender=1 WHERE id=?', [$id]); 66 go('messages/'); 67 } 68 69 if ($action === 'reply') { 70 $body = sanitise(post('body')); 71 if ($body) { 72 $subject = $msg['subject'] ? ('Re: ' . ltrim($msg['subject'], 'Re: ')) : ''; 73 DB::insert( 74 'INSERT INTO messages (sender_id,receiver_id,subject,body) VALUES (?,?,?,?)', 75 [$USER['id'], $otherId, $subject, $body] 76 ); 77 add_notification($otherId, 'message', [ 78 'from' => $USER['username'], 79 'from_id' => $USER['id'], 80 'subject' => mb_substr($body, 0, 60) . (mb_strlen($body) > 60 ? '…' : ''), 81 ]); 82 go('messages/view.php?id=' . $id . '&replied=1'); 83 } 84 } 85 } 86 87 $PAGE_TITLE = $msg['subject'] ?: 'Conversation with @' . $otherName; 88 include __DIR__ . '/../views/partials/layout.php'; 89 ?> 90 91 <div class="mv-layout"> 92 93 <!-- ── Thread sidebar ───────────────────────── --> 94 <aside class="mv-aside"> 95 <!-- Other person card --> 96 <div class="mv-user-card"> 97 <div class="mv-user-av"> 98 <?php if ($otherAv): ?> 99 <img src="<?= e($otherAv) ?>" class="av-lg" alt=""> 100 <?php else: ?> 101 <span class="av-lg av-init"><?= strtoupper($otherName[0]) ?></span> 102 <?php endif; ?> 103 <?php if ($otherOnline): ?> 104 <span class="mv-online-dot" title="Online now"></span> 105 <?php endif; ?> 106 </div> 107 <div class="mv-user-info"> 108 <a href="<?= u('users/profile.php?u=' . urlencode($otherName)) ?>" class="mv-user-name"> 109 @<?= e($otherName) ?> 110 </a> 111 <span class="role-tag role-<?= e($otherRole) ?>"><?= e($otherRole) ?></span> 112 <div class="mv-user-status <?= $otherOnline ? 'online' : 'offline' ?>"> 113 <span class="mv-status-dot"></span> 114 <?= $otherOnline ? 'Online now' : 'Last seen <span class="ago" data-ts="' . e($otherSeen) . '"></span>' ?> 115 </div> 116 </div> 117 <div class="mv-user-actions"> 118 <a href="<?= u('users/profile.php?u=' . urlencode($otherName)) ?>" class="btn-ghost btn-sm">Profile</a> 119 <a href="<?= u('messages/compose.php?to=' . urlencode($otherName)) ?>" class="btn-ghost btn-sm">New Message</a> 120 </div> 121 </div> 122 123 <!-- Conversation stats --> 124 <div class="mv-conv-stats"> 125 <div class="mv-cs-item"> 126 <span class="mv-cs-num"><?= count($thread) ?></span> 127 <span class="mv-cs-lbl">messages</span> 128 </div> 129 <div class="mv-cs-item"> 130 <span class="mv-cs-num"><?= count(array_filter($thread, fn($m) => !$m['is_read'] && $m['sender_id'] == $otherId)) ?></span> 131 <span class="mv-cs-lbl">unread</span> 132 </div> 133 </div> 134 135 <a href="<?= u('messages/') ?>" class="btn-ghost btn-block" style="margin-top:4px">← Back to Inbox</a> 136 </aside> 137 138 <!-- ── Main thread ───────────────────────────── --> 139 <div class="mv-main"> 140 141 <!-- Thread header --> 142 <div class="mv-thread-head"> 143 <h1 class="mv-thread-title"> 144 <?= $msg['subject'] ? e($msg['subject']) : 'Conversation with @' . e($otherName) ?> 145 </h1> 146 <form method="POST" style="display:inline-block"> 147 <?= csrf_input() ?><input type="hidden" name="action" value="delete"> 148 <button class="btn-ghost btn-sm" style="color:var(--red)" 149 onclick="return confirm('Delete this message thread?')">🗑 Delete</button> 150 </form> 151 </div> 152 153 <?php if (isset($_GET['replied'])): ?> 154 <div class="alert ok" style="margin-bottom:16px">✓ Reply sent.</div> 155 <?php endif; ?> 156 157 <!-- Message thread --> 158 <div class="mv-thread"> 159 <?php foreach ($thread as $tm): 160 $isMine = ((int)$tm['sender_id'] === (int)$USER['id']); 161 $canDel = ($tm['sender_id'] == $USER['id'] && !$tm['deleted_by_sender']) 162 || ($tm['receiver_id'] == $USER['id'] && !$tm['deleted_by_receiver']); 163 $isHighlight = ((int)$tm['id'] === $id && !$isMine); 164 ?> 165 <div class="mv-msg <?= $isMine ? 'mv-mine' : 'mv-theirs' ?> <?= $isHighlight ? 'mv-highlight' : '' ?>" 166 id="msg-<?= $tm['id'] ?>"> 167 168 <!-- Avatar --> 169 <div class="mv-msg-av" <?= $isMine ? 'style="order:3"' : '' ?>> 170 <?php if ($tm['sender_av']): ?> 171 <img src="<?= e($tm['sender_av']) ?>" class="av-sm" alt=""> 172 <?php else: ?> 173 <span class="av-sm av-init"><?= strtoupper($tm['sender_name'][0]) ?></span> 174 <?php endif; ?> 175 </div> 176 177 <!-- Bubble --> 178 <div class="mv-msg-bubble <?= $isMine ? 'mv-bubble-mine' : 'mv-bubble-theirs' ?>"> 179 <div class="mv-msg-body"><?= nl2br(e($tm['body'])) ?></div> 180 <div class="mv-msg-meta"> 181 <span class="ago" data-ts="<?= e($tm['created_at']) ?>"></span> 182 <?php if ($isMine && $tm['is_read']): ?> 183 <span class="mv-read-tick" title="Read">✓✓</span> 184 <?php elseif ($isMine): ?> 185 <span class="mv-sent-tick" title="Sent">✓</span> 186 <?php endif; ?> 187 <?php if ($canDel): ?> 188 <form method="POST" style="display:inline"> 189 <?= csrf_input() ?> 190 <input type="hidden" name="action" value="delete_msg"> 191 <input type="hidden" name="del_id" value="<?= $tm['id'] ?>"> 192 <button class="mv-del-btn" title="Delete" onclick="return confirm('Delete this message?')">✕</button> 193 </form> 194 <?php endif; ?> 195 </div> 196 </div> 197 </div> 198 <?php endforeach; ?> 199 </div> 200 201 <!-- Reply box --> 202 <div class="mv-reply-box" id="replyBox"> 203 <div class="mv-reply-head"> 204 <span>↩ Reply to @<?= e($otherName) ?></span> 205 <?php if ($otherOnline): ?> 206 <span style="color:var(--green);font-size:12px">● Online — will see it right away</span> 207 <?php endif; ?> 208 </div> 209 <form method="POST" id="replyForm"> 210 <?= csrf_input() ?> 211 <input type="hidden" name="action" value="reply"> 212 <textarea name="body" id="replyBody" class="mv-reply-ta" rows="4" required 213 maxlength="5000" placeholder="Write your reply…"></textarea> 214 <div class="mv-reply-footer"> 215 <span class="mv-char-cnt" id="charCnt">0 / 5000</span> 216 <button type="submit" class="btn-primary">Send Reply</button> 217 </div> 218 </form> 219 </div> 220 221 </div> 222 </div> 223 224 <script> 225 // Scroll to highlighted message 226 var hl = document.querySelector('.mv-highlight'); 227 if (hl) hl.scrollIntoView({behavior:'smooth', block:'center'}); 228 229 // Scroll thread to bottom if viewing latest 230 var thread = document.querySelector('.mv-thread'); 231 if (thread && !hl) thread.scrollTop = thread.scrollHeight; 232 233 // Char counter 234 var ta = document.getElementById('replyBody'); 235 var cc = document.getElementById('charCnt'); 236 if (ta && cc) { 237 ta.addEventListener('input', function() { 238 var n = this.value.length; 239 cc.textContent = n + ' / 5000'; 240 cc.style.color = n > 4500 ? '#ef4444' : ''; 241 }); 242 } 243 </script> 244 245 <?php include __DIR__ . '/../views/partials/layout_end.php'; ?>