xgit simple git

nexus

nexus

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

messages/view.php


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// Mark as read
24if ($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)
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// Handle POST actions
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// Scroll to highlighted message
226var hl = document.querySelector('.mv-highlight');
227if (hl) hl.scrollIntoView({behavior:'smooth', block:'center'});
228 
229// Scroll thread to bottom if viewing latest
230var thread = document.querySelector('.mv-thread');
231if (thread && !hl) thread.scrollTop = thread.scrollHeight;
232 
233// Char counter
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'; ?>