xgit simple git

nexus

nexus

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

messages/compose.php


1<?php
2require_once __DIR__ . '/../includes/bootstrap.php';
3must_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 
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/* ── To-field autocomplete ──────────────────── */
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/* ── Find user panel ────────────────────────── */
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/* ── Pick user ──────────────────────────────── */
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/* ── Char counter ───────────────────────────── */
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'; ?>