Round 3: session longevity, in-chat tone, pin audit, older-msg loader, long-press sheet, draft fix
Session (New): stop the ~24h auto-logout. SESSION_TTL 24h -> 90d, and /api/me now
SLIDES the session forward + re-stamps the cookie on every app load / focus /
6h heartbeat — so an actively-used session never lapses; you only log out by
choosing to. Login no longer depends on "remember me".
#2 A new message in the chat you're actively viewing now plays a soft, distinct
in-chat tone (playMsgTone) — no popup — instead of being silent. A different
chat / a backgrounded chat still gets the alert ping + notification.
#13 Pin/unpin is now written to the audit log (actor + which message, and whose pin
was removed on an unpin) — the accountability gap when anyone can unpin.
Pagination: a floating "Loading earlier messages…" pill now shows while older
history is being fetched (loadOlder had no visible indicator).
#9 Mobile long-press now opens a dimmed + blurred bottom ACTION SHEET (quick
reactions + reply/edit/forward/copy/pin/delete) instead of the flaky hover-style
reveal that hid behind images and broke after the lightbox opened.
#14 editTarget is cleared on conversation switch — starting an edit then switching
chats used to leave editTarget set, which silently stopped ALL draft saving.
Also added Edit to the shared action list so mobile long-press can edit too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+19
-1
@@ -267,7 +267,7 @@ route('POST', '/api/login', async (req, res) => {
|
||||
}
|
||||
|
||||
const tok = A.token();
|
||||
const ttl = remember ? 1000 * 60 * 60 * 24 * 30 : SESSION_TTL; // 30 days if remembered, else 24h
|
||||
const ttl = SESSION_TTL; // long-lived (90d) and slid forward on /api/me — no more 24h overnight logout (remember-me is now moot)
|
||||
await R.authSessions.create({ token: tok, userId: u.id, mfaPassed: true, ttl });
|
||||
res.setHeader('Set-Cookie', `sid=${tok}; HttpOnly; Path=/; Max-Age=${ttl / 1000}`);
|
||||
audit({ team_id: u.team_id, user_id: u.id, user_email: u.email, action: 'login' });
|
||||
@@ -359,6 +359,17 @@ route('GET', '/api/ice', async (req, res) => {
|
||||
route('GET', '/api/me', async (req, res) => {
|
||||
const u = await currentUser(req);
|
||||
if (!u) return json(res, 401, { error: 'unauthorized' });
|
||||
// Sliding session: a web (cookie) client hitting /api/me — on app load, focus, or the periodic heartbeat —
|
||||
// pushes its expiry out to a fresh full window and re-stamps the cookie. So any regular use keeps you logged
|
||||
// in indefinitely; you only lapse after SESSION_TTL of NO use at all, or by logging out. (Native clients use
|
||||
// the refresh-token flow, so we only renew here when the request actually carried the sid cookie.)
|
||||
try {
|
||||
const tok = parseCookies(req).sid;
|
||||
if (tok && u._session && u._session.token === tok) {
|
||||
await R.authSessions.touch(tok, SESSION_TTL);
|
||||
res.setHeader('Set-Cookie', `sid=${tok}; HttpOnly; Path=/; Max-Age=${SESSION_TTL / 1000}`);
|
||||
}
|
||||
} catch (_) {}
|
||||
json(res, 200, { id: u.id, email: u.email, role: u.role, teamId: u.team_id, name: u.name || null, avatarUrl: u.avatar_url || null, status: u.status || 'active' });
|
||||
});
|
||||
// Set my presence status: 'active' | 'away' | 'onleave' ('incall' is derived, not settable).
|
||||
@@ -1655,6 +1666,13 @@ route('POST', '/api/messages/pin', async (req, res) => {
|
||||
if (!canSee) return json(res, 403, { error: 'not allowed' });
|
||||
const pin = on !== false; // default true
|
||||
await R.messages.setPinned(id, pin ? now() : null, pin ? u.id : null);
|
||||
// #13: log every pin/unpin so there's an accountable trail — anyone can unpin anyone's pin, but who did it
|
||||
// (and, for an unpin, whose pin they removed) is now recorded in the audit log.
|
||||
try {
|
||||
const where = m.conversation_id ? ('group ' + m.conversation_id) : ('dm with ' + (m.sender_id === u.id ? m.recipient_id : m.sender_id));
|
||||
const whose = (!pin && m.pinned_by && m.pinned_by !== u.id) ? (' (originally pinned by ' + m.pinned_by + ')') : '';
|
||||
audit({ team_id: u.team_id, user_id: u.id, user_email: u.email, action: pin ? 'message.pin' : 'message.unpin', detail: where + ' · message ' + id + whose });
|
||||
} catch (_) {}
|
||||
const evt = { type: 'chat-pinned', id, on: pin, by: u.name || u.email, conversation_id: m.conversation_id || null };
|
||||
if (m.conversation_id) { for (const mid of await R.conversations.members(m.conversation_id)) { try { CHAT.pushToUser(mid, evt); } catch (_) {} } }
|
||||
else { try { CHAT.pushToUser(m.recipient_id, evt); } catch (_) {} try { CHAT.pushToUser(m.sender_id, evt); } catch (_) {} }
|
||||
|
||||
Reference in New Issue
Block a user