1 <?php 2 require_once __DIR__ . '/../includes/bootstrap.php'; 3 must_login(); 4 5 $toUser = get('to'); 6 $errs = []; 7 8 // Online users for quick selection 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 17 if ($_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'; 50 include __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 /* ── To-field autocomplete ──────────────────── */ 160 var toInp = document.getElementById('toInput'); 161 var toSug = document.getElementById('toSuggestions'); 162 var toTmr; 163 if (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 /* ── Find user panel ────────────────────────── */ 187 var findInp = document.getElementById('findUser'); 188 var findRes = document.getElementById('findResults'); 189 var findTmr; 190 if (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 /* ── Pick user ──────────────────────────────── */ 211 function 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 /* ── Char counter ───────────────────────────── */ 220 var body = document.getElementById('msgBody'); 221 var cnt = document.getElementById('bodyCount'); 222 if (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'; ?>