Round 2 fixes from on-device testing (#2,#3,#6,#8,#9,#13,#14,#18 + call re-ring)
#2 Don't ping/notify when you're ACTIVELY viewing a chat (app visible + chat open). Alert only when a different chat, OR the open chat while the app is minimised (the backgrounded case that used to stay silent). #3 A reaction to my message now raises an unread badge on that conversation (like a new message), not just a notification. #6 Image lightbox pulls EVERY image in the conversation via /api/messages/media (older images aren't in the DOM yet) — nav arrows reach them all. Nav buttons always in the DOM; syncArrows shows/hides at the ends and for a single image. #8 On app resume (visibilitychange / native appStateChange), reconnect the chat socket if it isn't OPEN and re-pull the sidebar so online/last-seen refresh — iOS freezes the WebView so the socket can be dead while its onclose lags, leaving contacts stuck on a stale "Offline". #9 Real cause was iOS "sticky :hover": a single tap latched :hover and popped the action bar. Gate the hover-reveal behind @media (hover:hover) so touch reveals actions ONLY via long-press; a plain tap performs the primary action. #13 Pinned bar gains a "‹ 1 of n ›" pager to walk through multiple pinned messages (shown only when more than one is pinned). #14 Editing a message no longer eats a half-written draft — the real draft is set aside on edit start and restored on save/cancel. edited_at is now in the message DTO so the "edited" tag survives a reload. #18 One "Delete" entry opens a branded dialog with "Delete for me" / "Delete for everyone" (icons + descriptions) and a ✕/backdrop cancel, replacing the two separate menu items. New: a participant who LEAVES a still-running call is no longer auto-rung back in on every socket reconnect. Track who left per call; replayActiveCalls sends them noRing state (refreshes the Join affordance without ringing). An explicit re-invite clears that and rings again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+21
-6
@@ -67,7 +67,7 @@ async function startGroupCall(group, teamId, user) {
|
||||
if (existing) return { room: existing.room, uuid: existing.uuid, active: true, already: true };
|
||||
let room; do { room = A.numericCode(6); } while (meetingRooms.has(room));
|
||||
meetingRooms.set(room, new Map());
|
||||
const call = { room, uuid: crypto.randomUUID(), startedAt: now(), startedBy: user.id, startedByName: user.name || user.email };
|
||||
const call = { room, uuid: crypto.randomUUID(), startedAt: now(), startedBy: user.id, startedByName: user.name || user.email, left: new Set() };
|
||||
// Log the call as a meeting so it appears under Past meetings (history) with the group name.
|
||||
try { const hid = A.id(); await R.scheduledMeetings.create({ id: hid, teamId, groupId: group, roomCode: room, title: 'Group call', description: null, scheduledAt: now(), createdBy: user.id }); call.historyId = hid; call.teamId = teamId; } catch (_) {}
|
||||
groupCalls.set(group, call); roomToGroupCall.set(room, group); roomHost.set(room, user.id); // creator = host
|
||||
@@ -103,7 +103,7 @@ async function startDmCall(me, otherId, teamId) {
|
||||
let room; do { room = A.numericCode(6); } while (meetingRooms.has(room));
|
||||
meetingRooms.set(room, new Map());
|
||||
const byName = me.name || me.email;
|
||||
const call = { room, uuid: crypto.randomUUID(), startedAt: now(), startedBy: me.id, startedByName: byName, users: [me.id, otherId], teamId, answered: false };
|
||||
const call = { room, uuid: crypto.randomUUID(), startedAt: now(), startedBy: me.id, startedByName: byName, users: [me.id, otherId], teamId, answered: false, left: new Set() };
|
||||
// Log to history (both participants) so the call shows under Past meetings with its transcript.
|
||||
try { const hid = A.id(); await R.scheduledMeetings.create({ id: hid, teamId, groupId: null, roomCode: room, title: 'Direct Call', description: null, scheduledAt: now(), createdBy: me.id, participants: [me.id, otherId] }); call.historyId = hid; } catch (_) {}
|
||||
dmCalls.set(key, call); roomToDmCall.set(room, key); roomHost.set(room, me.id); // caller = host
|
||||
@@ -192,7 +192,9 @@ async function replayActiveCalls(userId, ws) {
|
||||
for (const [, call] of dmCalls) {
|
||||
if (call.answered) continue;
|
||||
if (call.users.includes(userId) && call.startedBy !== userId) {
|
||||
try { ws.send(JSON.stringify({ type: 'dm-call', active: true, room: call.room, uuid: call.uuid, with: call.startedBy, by: call.startedBy, byName: call.startedByName })); } catch (_) {}
|
||||
// #New1: if this user already LEFT the call, don't ring them back in (noRing) — just refresh the "Join" state.
|
||||
const noRing = !!(call.left && call.left.has(userId));
|
||||
try { ws.send(JSON.stringify({ type: 'dm-call', active: true, room: call.room, uuid: call.uuid, with: call.startedBy, by: call.startedBy, byName: call.startedByName, noRing })); } catch (_) {}
|
||||
}
|
||||
}
|
||||
for (const [group, call] of groupCalls) {
|
||||
@@ -200,11 +202,24 @@ async function replayActiveCalls(userId, ws) {
|
||||
let member = false; try { member = await R.conversations.isMember(group, userId); } catch (_) {}
|
||||
if (!member) continue;
|
||||
let gName = 'Group'; try { const g = await R.conversations.byId(group); if (g) gName = g.name || 'Group'; } catch (_) {}
|
||||
try { ws.send(JSON.stringify({ type: 'group-call', group, active: true, room: call.room, uuid: call.uuid, by: call.startedBy, startedByName: call.startedByName, groupName: gName })); } catch (_) {}
|
||||
const noRing = !!(call.left && call.left.has(userId)); // #New1: left already → refresh Join, don't re-ring
|
||||
try { ws.send(JSON.stringify({ type: 'group-call', group, active: true, room: call.room, uuid: call.uuid, by: call.startedBy, startedByName: call.startedByName, groupName: gName, noRing })); } catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// #New1: a participant who EXPLICITLY leaves an active (still-running) call must not be auto-rung back into
|
||||
// it. We remember who left per call; replayActiveCalls (on their next socket reconnect) then sends the call
|
||||
// state with noRing:true so their client refreshes the passive "Join" affordance without ringing / popping
|
||||
// CallKit again. Before this, every reconnect (constant on mobile) re-rang the leaver until the call ended.
|
||||
function callForRoom(room) {
|
||||
const gid = roomToGroupCall.get(room); if (gid) { const c = groupCalls.get(gid); if (c) return c; }
|
||||
const key = roomToDmCall.get(room); if (key) { const c = dmCalls.get(key); if (c) return c; }
|
||||
return null;
|
||||
}
|
||||
function markLeft(room, userId) { if (!userId) return; const c = callForRoom(room); if (c) { if (!c.left) c.left = new Set(); c.left.add(userId); } }
|
||||
function clearLeft(room, ids) { const c = callForRoom(room); if (c && c.left) { for (const id of (ids || [])) c.left.delete(id); } } // an explicit re-invite should ring again
|
||||
|
||||
// Called from signaling when any mesh room empties.
|
||||
async function endCallByRoom(room) { await endGroupCallByRoom(room); await endDmCallByRoom(room); }
|
||||
|
||||
@@ -259,7 +274,7 @@ async function promoteDmToGroup(room, inviter, inviteeIds) {
|
||||
// Migrate the LIVE call: DM → group (same room/uuid/history so media, transcript and Past-meetings all continue).
|
||||
if (call.ringTimer) { try { clearTimeout(call.ringTimer); } catch (_) {} call.ringTimer = null; }
|
||||
dmCalls.delete(key); roomToDmCall.delete(room);
|
||||
groupCalls.set(gid, { room, uuid: call.uuid, startedAt: call.startedAt, startedBy: owner, startedByName: call.startedByName, teamId, historyId: call.historyId });
|
||||
groupCalls.set(gid, { room, uuid: call.uuid, startedAt: call.startedAt, startedBy: owner, startedByName: call.startedByName, teamId, historyId: call.historyId, left: new Set() });
|
||||
roomToGroupCall.set(room, gid);
|
||||
postSystem(gid, teamId, '📞 ' + (call.startedByName || 'Someone') + ' turned this into a group call').catch(() => {});
|
||||
// Tell every member's client: refresh the sidebar (the new group appears) and mark the call active (banner
|
||||
@@ -271,4 +286,4 @@ async function promoteDmToGroup(room, inviter, inviteeIds) {
|
||||
return gid;
|
||||
}
|
||||
|
||||
module.exports = { startGroupCall, startDmCall, endGroupCallByRoom, endDmCallByRoom, endCallByRoom, declineDmCall, markDmAnswered, replayActiveCalls, promoteDmToGroup, finalizeTranscript, meetingContext, fmtDur, pairKey };
|
||||
module.exports = { startGroupCall, startDmCall, endGroupCallByRoom, endDmCallByRoom, endCallByRoom, declineDmCall, markDmAnswered, replayActiveCalls, promoteDmToGroup, markLeft, clearLeft, finalizeTranscript, meetingContext, fmtDur, pairKey };
|
||||
|
||||
Reference in New Issue
Block a user