feat(chat): emoji speed, action rework, copy, phone links, image zoom, upload %, profile card, last seen (batch82)
#6 Emoji were slow because Twemoji swapped EVERY emoji for an <img> fetched individually from a CDN — opening the picker fired hundreds of image requests. Now uses the OS's own colour emoji font: instant, zero network. twemojify() kept as a no-op. #4 Message hover row reworked: three one-tap reactions (Like/Laugh/Surprised) + the emoji picker + (own) Edit. Reply / Forward / Copy / Delete moved behind a ⋮ menu. Added Copy. The row now sits FULLY above the bubble (was top:-14px, overlapping the first text line). #9 Phone numbers linkify to tel: — mobile gets the OS "call this number?" prompt; desktop has no dialer so it offers to copy. Regex kept conservative (10–15 digits, needs +/grouping) so it won't grab amounts, dates or 6-digit meeting codes. #3 Image preview zooms: wheel + pinch + double-click + ± buttons, drag to pan, keys (+/-/0), cursor-anchored. Arrows hide while zoomed so panning isn't hijacked. #8 Upload progress: fetch() can't report upload progress at all, so a large file just said "uploading…". Switched to XHR (upload.onprogress) → real bar + %, and cancel aborts in flight. #1 Clicking a sender in a group opens a mini profile card (photo, presence, last seen) with a Message button that opens the 1:1 (and a view-photo button). #2 Last seen: new users.last_seen column, stamped on connect and when the last socket drops; carried on the presence broadcast, so an offline contact reads "Last seen 10 minutes ago" instead of a bare "Offline". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+4
-3
@@ -716,7 +716,7 @@ route('GET', '/api/messages/contacts', async (req, res) => {
|
||||
if (!u) return json(res, 401, { error: 'unauthorized' });
|
||||
const rows = R.users.listByTenant(u.team_id).filter((x) => x.id !== u.id && x.active !== 0);
|
||||
const cAv = avatarsFor(u.team_id); // duplicate-row DP fallback
|
||||
json(res, 200, rows.map((x) => ({ id: x.id, name: x.name || x.email, email: x.email, online: CHAT.isOnline(x.id), avatar: cAv[x.id] || null })));
|
||||
json(res, 200, rows.map((x) => ({ id: x.id, name: x.name || x.email, email: x.email, online: CHAT.isOnline(x.id), avatar: cAv[x.id] || null, lastSeen: x.last_seen || null, status: x.status || 'active' })));
|
||||
});
|
||||
|
||||
// Cross-tenant people search via the BizGaze directory (token stays server-side). Results are
|
||||
@@ -745,7 +745,8 @@ route('GET', '/api/messages/conversations', async (req, res) => {
|
||||
const names = {};
|
||||
const avatars = {};
|
||||
const statuses = {};
|
||||
for (const x of R.users.listByTenant(u.team_id)) { names[x.id] = x.name || x.email; statuses[x.id] = x.status || 'active'; }
|
||||
const seen = {};
|
||||
for (const x of R.users.listByTenant(u.team_id)) { names[x.id] = x.name || x.email; statuses[x.id] = x.status || 'active'; seen[x.id] = x.last_seen || null; }
|
||||
Object.assign(avatars, avatarsFor(u.team_id)); // same person / two rows → borrow the DP (see avatarsFor)
|
||||
const favs = new Set(R.favorites.forUser(u.id));
|
||||
const inCall = new Set();
|
||||
@@ -764,7 +765,7 @@ route('GET', '/api/messages/conversations', async (req, res) => {
|
||||
const dmItems = [...byOther.values()].map((c) => {
|
||||
const dc = dmCalls.get(CALLS.pairKey(u.id, c.other));
|
||||
return {
|
||||
kind: 'dm', id: c.other, contactId: c.other, name: names[c.other] || 'Unknown', online: CHAT.isOnline(c.other), avatar: avatars[c.other] || null,
|
||||
kind: 'dm', id: c.other, contactId: c.other, name: names[c.other] || 'Unknown', online: CHAT.isOnline(c.other), avatar: avatars[c.other] || null, lastSeen: seen[c.other] || null,
|
||||
callActive: !!dc, callRoom: dc ? dc.room : null, favorite: favs.has('dm:' + c.other), status: inCall.has(c.other) ? 'incall' : (statuses[c.other] || 'active'),
|
||||
last_body: c.last.body || (c.last.attachment_id ? '📎 Attachment' : ''), last_at: c.last.created_at, last_from_me: c.last.sender_id === u.id, unread: c.unread,
|
||||
last_status: c.last.sender_id === u.id ? (c.last.read_at ? 'read' : (c.last.delivered_at ? 'delivered' : 'sent')) : null, // tick for my last message
|
||||
|
||||
Reference in New Issue
Block a user