xgit simple git

nexus

nexus

clone git clone https://kb.hax.al/nexus

messages/view.php

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'; ?>