1<?php
2require_once __DIR__ . '/../includes/bootstrap.php';
3must_login();
4
5$toUser = get('to');
6$errs = [];
7
8
9$since = DB::sinceSeconds(900);
10$onlineUsers = DB::rows(
11 "SELECT id, username, avatar, role FROM users
12 WHERE last_seen >= $since AND id != ? AND suspended = 0
13 ORDER BY last_seen DESC LIMIT 12",
14 [$USER['id']]
15);
16
17if ($_SERVER['REQUEST_METHOD'] === 'POST') {
18 if (!csrf_ok()) { $errs[] = 'Invalid request.'; }
19 else {
20 $to = sanitise(post('to'));
21 $subject = sanitise(post('subject'));
22 $body = sanitise(post('body'));
23
24 if (!$to) $errs[] = 'Recipient required.';
25 if (!$body) $errs[] = 'Message body required.';
26 if (strlen($body) > 5000) $errs[] = 'Message too long (max 5000 chars).';
27
28 if (!$errs) {
29 $recipient = DB::row('SELECT * FROM users WHERE username=?', [$to]);
30 if (!$recipient) $errs[] = 'User "@' . e($to) . '" not found.';
31 elseif ($recipient['id'] === $USER['id']) $errs[] = 'You cannot message yourself.';
32 }
33
34 if (!$errs) {
35 $newId = DB::insert(
36 'INSERT INTO messages (sender_id,receiver_id,subject,body) VALUES (?,?,?,?)',
37 [$USER['id'], $recipient['id'], $subject, $body]
38 );
39 add_notification((int)$recipient['id'], 'message', [
40 'from' => $USER['username'],
41 'from_id' => $USER['id'],
42 'subject' => mb_substr($body, 0, 60) . (mb_strlen($body) > 60 ? '…' : ''),
43 ]);
44 go('messages/view.php?id=' . $newId);
45 }
46 }
47}
48
49$PAGE_TITLE = 'New Message';
50include __DIR__ . '/../views/partials/layout.php';
51?>
52
53<div class="cp-layout">
54
55 <!-- ── Compose form ─────────────────────────── -->
56 <div class="cp-main">
57 <div class="form-card">
58 <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:22px">
59 <h1 style="font-size:1.2rem;font-weight:700;margin:0">✉️ New Message</h1>
60 <a href="<?= u('messages/') ?>" class="btn-ghost btn-sm">← Inbox</a>
61 </div>
62
63 <?php if (!empty($errs)): ?>
64 <div class="alert err"><?= implode('<br>', array_map('e', $errs)) ?></div>
65 <?php endif; ?>
66
67 <form method="POST" id="composeForm">
68 <?= csrf_input() ?>
69
70 <!-- To field with autocomplete -->
71 <div class="fg">
72 <label>To <span class="req">*</span></label>
73 <div class="to-input-wrap" style="position:relative">
74 <span class="at-prefix">@</span>
75 <input type="text" name="to" id="toInput" class="fi" required
76 value="<?= e($_POST['to'] ?? $toUser ?? '') ?>"
77 placeholder="username" autocomplete="off">
78 <div id="toSuggestions" class="to-suggestions"></div>
79 </div>
80 </div>
81
82 <!-- Subject -->
83 <div class="fg">
84 <label>Subject <small>(optional)</small></label>
85 <input type="text" name="subject" class="fi" maxlength="150"
86 value="<?= e($_POST['subject'] ?? '') ?>" placeholder="What's this about?">
87 </div>
88
89 <!-- Body -->
90 <div class="fg">
91 <label>Message <span class="req">*</span></label>
92 <textarea name="body" id="msgBody" class="fi" rows="10" required maxlength="5000"
93 placeholder="Write your message…"><?= e($_POST['body'] ?? '') ?></textarea>
94 <div style="display:flex;justify-content:space-between;margin-top:4px">
95 <span class="hint">Max 5000 characters</span>
96 <span class="hint" id="bodyCount">0 / 5000</span>
97 </div>
98 </div>
99
100 <div class="form-actions">
101 <a href="<?= u('messages/') ?>" class="btn-ghost">Cancel</a>
102 <button type="submit" class="btn-primary">
103 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M22 2L11 13M22 2L15 22l-4-9-9-4 20-7z"/></svg>
104 Send Message
105 </button>
106 </div>
107 </form>
108 </div>
109 </div>
110
111 <!-- ── Online people panel ────────────────────── -->
112 <aside class="cp-aside">
113 <div class="mx-widget">
114 <div class="mx-widget-head">
115 <span class="mx-online-dot" style="position:static;margin-right:4px"></span>
116 Online Now
117 <span class="mx-widget-count"><?= count($onlineUsers) ?></span>
118 </div>
119 <?php if (empty($onlineUsers)): ?>
120 <div class="mx-widget-empty">No one online right now</div>
121 <?php else: ?>
122 <p style="padding:10px 14px 4px;font-size:12px;color:var(--muted)">Click to message them directly</p>
123 <div class="mx-online-list">
124 <?php foreach ($onlineUsers as $ou): ?>
125 <button class="mx-online-row cp-pick" type="button"
126 onclick="pickUser('<?= e($ou['username']) ?>')"
127 title="Send to @<?= e($ou['username']) ?>">
128 <div style="position:relative;flex-shrink:0">
129 <?php if ($ou['avatar']): ?>
130 <img src="<?= e($ou['avatar']) ?>" class="av-sm" alt="">
131 <?php else: ?>
132 <span class="av-sm av-init"><?= strtoupper($ou['username'][0]) ?></span>
133 <?php endif; ?>
134 <span class="mx-online-dot mx-online-dot-sm"></span>
135 </div>
136 <div class="mx-online-info">
137 <span class="mx-online-name">@<?= e($ou['username']) ?></span>
138 <span class="role-tag role-<?= e($ou['role']) ?>"><?= e($ou['role']) ?></span>
139 </div>
140 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--faint)" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
141 </button>
142 <?php endforeach; ?>
143 </div>
144 <?php endif; ?>
145 </div>
146
147 <!-- Search any user -->
148 <div class="mx-widget" style="margin-top:14px">
149 <div class="mx-widget-head">🔍 Find User</div>
150 <div style="padding:10px">
151 <input type="text" id="findUser" class="fi" placeholder="Search username…" autocomplete="off">
152 <div id="findResults" class="to-suggestions" style="position:static;margin-top:6px;border-radius:var(--r)"></div>
153 </div>
154 </div>
155 </aside>
156</div>
157
158<script>
159
160var toInp = document.getElementById('toInput');
161var toSug = document.getElementById('toSuggestions');
162var toTmr;
163if (toInp) {
164 toInp.addEventListener('input', function() {
165 clearTimeout(toTmr);
166 var q = this.value.trim().replace(/^@/, '');
167 if (!q) { toSug.style.display = 'none'; return; }
168 toTmr = setTimeout(function() {
169 fetch(NX.base + '/api/search_users.php?q=' + encodeURIComponent(q))
170 .then(function(r) { return r.json(); })
171 .then(function(rows) {
172 if (!rows.length) { toSug.style.display = 'none'; return; }
173 toSug.innerHTML = rows.map(function(u) {
174 return '<div class="to-sug-item" onclick="pickUser(\'' + u.username + '\')">' +
175 '@' + u.username + '</div>';
176 }).join('');
177 toSug.style.display = 'block';
178 });
179 }, 220);
180 });
181 document.addEventListener('click', function(e) {
182 if (!toInp.contains(e.target)) toSug.style.display = 'none';
183 });
184}
185
186
187var findInp = document.getElementById('findUser');
188var findRes = document.getElementById('findResults');
189var findTmr;
190if (findInp) {
191 findInp.addEventListener('input', function() {
192 clearTimeout(findTmr);
193 var q = this.value.trim();
194 if (!q) { findRes.style.display = 'none'; return; }
195 findTmr = setTimeout(function() {
196 fetch(NX.base + '/api/search_users.php?q=' + encodeURIComponent(q))
197 .then(function(r) { return r.json(); })
198 .then(function(rows) {
199 if (!rows.length) { findRes.style.display = 'none'; return; }
200 findRes.innerHTML = rows.map(function(u) {
201 return '<div class="to-sug-item" onclick="pickUser(\'' + u.username + '\')">' +
202 '@' + u.username + '</div>';
203 }).join('');
204 findRes.style.display = 'block';
205 });
206 }, 220);
207 });
208}
209
210
211function pickUser(uname) {
212 document.getElementById('toInput').value = uname;
213 toSug.style.display = 'none';
214 if (findRes) findRes.style.display = 'none';
215 document.getElementById('toInput').focus();
216 document.getElementById('toInput').dispatchEvent(new Event('input'));
217}
218
219
220var body = document.getElementById('msgBody');
221var cnt = document.getElementById('bodyCount');
222if (body && cnt) {
223 body.addEventListener('input', function() {
224 var n = this.value.length;
225 cnt.textContent = n + ' / 5000';
226 cnt.style.color = n > 4500 ? '#ef4444' : '';
227 });
228}
229</script>
230
231<?php include __DIR__ . '/../views/partials/layout_end.php'; ?>