1<?php
2require_once __DIR__ . '/../includes/bootstrap.php';
3must_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
18if (!$msg) render_404();
19if ($msg['sender_id'] !== $USER['id'] && $msg['receiver_id'] !== $USER['id']) render_403();
20if ($msg['receiver_id'] === $USER['id'] && $msg['deleted_by_receiver']) render_404();
21if ($msg['sender_id'] === $USER['id'] && $msg['deleted_by_sender']) render_404();
22
23
24if ($msg['receiver_id'] === $USER['id'] && !$msg['is_read']) {
25 DB::run('UPDATE messages SET `is_read`=1 WHERE id=?', [$id]);
26}
27
28
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
37function 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
48if ($_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;
88include __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
226var hl = document.querySelector('.mv-highlight');
227if (hl) hl.scrollIntoView({behavior:'smooth', block:'center'});
228
229
230var thread = document.querySelector('.mv-thread');
231if (thread && !hl) thread.scrollTop = thread.scrollHeight;
232
233
234var ta = document.getElementById('replyBody');
235var cc = document.getElementById('charCnt');
236if (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'; ?>