xgit simple git

nexus

nexus

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

messages/compose.php

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