Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a2f18e3d5 |
@@ -297,7 +297,7 @@ export function AccountDataEditor({
|
||||
</Chip>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton type="button" onClick={requestClose} variant="Surface" aria-label="Close">
|
||||
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -111,8 +111,6 @@ type IncomingCallInfo = {
|
||||
intent?: string;
|
||||
notificationType: RTCNotificationType;
|
||||
refEventId: string;
|
||||
/** [Gitea #313] The rtc.notification event itself: what a decline must reference (MSC4310). */
|
||||
notificationEventId?: string;
|
||||
};
|
||||
type IncomingCallProps = {
|
||||
dm: boolean;
|
||||
@@ -285,9 +283,7 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
|
||||
fill="Soft"
|
||||
size="400"
|
||||
radii="400"
|
||||
onClick={() =>
|
||||
dm ? onReject(room, info.notificationEventId ?? info.refEventId) : onIgnore()
|
||||
}
|
||||
onClick={() => (dm ? onReject(room, info.refEventId) : onIgnore())}
|
||||
before={<Icon size="200" src={Icons.Cross} filled />}
|
||||
>
|
||||
<Text as="span" size="B400">
|
||||
@@ -459,9 +455,7 @@ function IncomingCallBanner({ dm, info, onIgnore, onAnswer, onReject }: Incoming
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
onClick={() =>
|
||||
dm ? onReject(room, info.notificationEventId ?? info.refEventId) : onIgnore()
|
||||
}
|
||||
onClick={() => (dm ? onReject(room, info.refEventId) : onIgnore())}
|
||||
before={<Icon size="100" src={Icons.Cross} filled />}
|
||||
>
|
||||
<Text as="span" size="B300">
|
||||
@@ -602,7 +596,6 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
||||
: undefined,
|
||||
notificationType,
|
||||
refEventId,
|
||||
notificationEventId: event.getId(),
|
||||
};
|
||||
|
||||
// [Gitea #312] Don't ring for a call that is already over or handled: after
|
||||
@@ -611,10 +604,7 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
||||
// stale RTC session). The caller's membership can arrive just after the
|
||||
// ring, so an absent one gets a short grace period before giving up.
|
||||
const myUserId = mx.getSafeUserId();
|
||||
if (
|
||||
declinedRing(room, [refEventId, event.getId()], myUserId) ||
|
||||
hasActiveCallMembership(room, myUserId)
|
||||
) {
|
||||
if (declinedRing(room, refEventId, myUserId) || hasActiveCallMembership(room, myUserId)) {
|
||||
return;
|
||||
}
|
||||
if (hasActiveCallMembership(room, sender)) {
|
||||
@@ -623,10 +613,7 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (Date.now() >= senderTs + lifetime) return;
|
||||
if (
|
||||
declinedRing(room, [refEventId, event.getId()], myUserId) ||
|
||||
hasActiveCallMembership(room, myUserId)
|
||||
) {
|
||||
if (declinedRing(room, refEventId, myUserId) || hasActiveCallMembership(room, myUserId)) {
|
||||
return;
|
||||
}
|
||||
if (hasActiveCallMembership(room, sender)) setCallInfo(info);
|
||||
@@ -698,10 +685,7 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
||||
if (eventRoom?.roomId !== room.roomId || !data.liveEvent) return;
|
||||
if (event.getType() === EventType.RTCDecline && event.getSender() === myUserId) {
|
||||
const related = event.getRelation()?.event_id;
|
||||
// [Gitea #313] Correct clients reference the notification; older Lotus
|
||||
// builds referenced the call membership. Accept both.
|
||||
if (!related || related === refEventId || related === callInfo.notificationEventId)
|
||||
dismiss(); // declined elsewhere
|
||||
if (!related || related === refEventId) dismiss(); // declined elsewhere
|
||||
}
|
||||
};
|
||||
session.on(MatrixRTCSessionEvent.MembershipsChanged, checkMemberships);
|
||||
|
||||
@@ -315,7 +315,7 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
|
||||
Setup Device Verification
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton type="button" size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
@@ -361,7 +361,7 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
|
||||
Reset Device Verification
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton type="button" size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -275,7 +275,7 @@ export function StateEventEditor({ type, stateKey, requestClose }: StateEventEdi
|
||||
</Chip>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton type="button" onClick={requestClose} variant="Surface" aria-label="Close">
|
||||
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -143,7 +143,7 @@ export function PollCreator({ roomId, threadRootId, onClose }: PollCreatorProps)
|
||||
Create Poll
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton type="button" size="300" radii="300" onClick={onClose} aria-label="Close">
|
||||
<IconButton size="300" radii="300" onClick={onClose} aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -118,7 +118,7 @@ export function ReportRoomModal({ roomId, onClose }: ReportRoomModalProps) {
|
||||
Report Room
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton type="button" size="300" onClick={onClose} radii="300" aria-label="Close">
|
||||
<IconButton size="300" onClick={onClose} radii="300" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -124,7 +124,7 @@ export function ReportUserModal({ userId, onClose }: ReportUserModalProps) {
|
||||
Report User
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton type="button" size="300" onClick={onClose} radii="300" aria-label="Close">
|
||||
<IconButton size="300" onClick={onClose} radii="300" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -138,7 +138,7 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
|
||||
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
|
||||
import { useIsDirectRoom } from '../../hooks/useRoom';
|
||||
import { declinedRing, isRingableRoom } from '../../utils/callRing';
|
||||
import { isRingableRoom } from '../../utils/callRing';
|
||||
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
|
||||
import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||
@@ -2037,9 +2037,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
// Same rule as ringing: no "Missed call" in voice rooms or space
|
||||
// channels, where starting a call is just joining the channel.
|
||||
if (!isRingableRoom(room, direct)) return null;
|
||||
// [Gitea #313] A call we declined isn't a missed call.
|
||||
const ringRef = mEvent.getRelation()?.event_id;
|
||||
if (declinedRing(room, [mEventId, ringRef], mx.getSafeUserId())) return null;
|
||||
const content = mEvent.getContent();
|
||||
const mention =
|
||||
content['m.mentions']?.room ||
|
||||
|
||||
@@ -202,7 +202,7 @@ export function ScheduleMessageModal({
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton type="button" size="300" radii="300" onClick={onClose} aria-label="Close">
|
||||
<IconButton size="300" radii="300" onClick={onClose} aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -725,13 +725,7 @@ export const MessageDeleteItem = as<
|
||||
Delete Message
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton
|
||||
type="button"
|
||||
size="300"
|
||||
onClick={handleClose}
|
||||
radii="300"
|
||||
aria-label="Close"
|
||||
>
|
||||
<IconButton size="300" onClick={handleClose} radii="300" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
@@ -871,13 +865,7 @@ export const MessageReportItem = as<
|
||||
Report Message
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton
|
||||
type="button"
|
||||
size="300"
|
||||
onClick={handleClose}
|
||||
radii="300"
|
||||
aria-label="Close"
|
||||
>
|
||||
<IconButton size="300" onClick={handleClose} radii="300" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
CALL_SESSION_HEARTBEAT_MS,
|
||||
clearCallSession,
|
||||
decideRejoin,
|
||||
othersInCall,
|
||||
rejoinToastBody,
|
||||
readCallSession,
|
||||
writeCallSession,
|
||||
} from '../utils/callRejoin';
|
||||
@@ -112,6 +114,27 @@ export function useCallRejoin(embed: CallEmbed | undefined, joined: boolean): vo
|
||||
};
|
||||
dismissToast(TOAST_ID);
|
||||
clearCallSession();
|
||||
// [Gitea #302] A DM call the other person has since left is over;
|
||||
// starting it again would ring them, so just say so.
|
||||
const dm = l.directs.has(room.roomId);
|
||||
if (
|
||||
dm &&
|
||||
!othersInCall(
|
||||
session.memberships.map((m) => m.userId ?? ''),
|
||||
mx.getUserId() ?? '',
|
||||
)
|
||||
) {
|
||||
// Own id: clicking a toast removes it by id after its onClick runs.
|
||||
setToast({
|
||||
id: `${TOAST_ID}-ended`,
|
||||
iconSrc: Icons.Phone,
|
||||
displayName: 'Call ended',
|
||||
body: `${room.name ?? 'They'} already left the call.`,
|
||||
roomName: room.name ?? '',
|
||||
roomId: room.roomId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
(l.directs.has(room.roomId) ? l.startDmCall : l.startRoomCall)(room, pref);
|
||||
} catch {
|
||||
@@ -133,6 +156,7 @@ export function useCallRejoin(embed: CallEmbed | undefined, joined: boolean): vo
|
||||
.filter((m) => m.userId === mx.getUserId())
|
||||
.map((m) => m.deviceId),
|
||||
memberCount: memberships.length,
|
||||
dm: latest.current.directs.has(room.roomId),
|
||||
});
|
||||
// The RTC session fills in a moment after sync; keep waiting while it is
|
||||
// empty unless this is the final (timed-out) look.
|
||||
@@ -152,7 +176,7 @@ export function useCallRejoin(embed: CallEmbed | undefined, joined: boolean): vo
|
||||
id: TOAST_ID,
|
||||
iconSrc: Icons.Phone,
|
||||
displayName: 'Rejoin voice?',
|
||||
body: `You were in the call in ${room.name ?? 'a room'} before the restart. Tap to rejoin.`,
|
||||
body: rejoinToastBody(room.name, latest.current.directs.has(room.roomId)),
|
||||
roomName: room.name ?? '',
|
||||
roomId: room.roomId,
|
||||
sticky: true,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { CALL_REJOIN_MAX_AGE_MS, decideRejoin, RejoinContext } from './callRejoin';
|
||||
import {
|
||||
CALL_REJOIN_MAX_AGE_MS,
|
||||
decideRejoin,
|
||||
othersInCall,
|
||||
rejoinToastBody,
|
||||
RejoinContext,
|
||||
} from './callRejoin';
|
||||
|
||||
const base: RejoinContext = {
|
||||
record: {
|
||||
@@ -54,3 +60,21 @@ describe('decideRejoin', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('decideRejoin: DM calls are never auto-rejoined, only offered (#302)', () => {
|
||||
assert.deepEqual(decideRejoin({ ...base, mode: 'auto', dm: true }), {
|
||||
action: 'ask',
|
||||
roomId: '!r',
|
||||
});
|
||||
assert.deepEqual(decideRejoin({ ...base, mode: 'auto' }), { action: 'auto', roomId: '!r' });
|
||||
});
|
||||
|
||||
it('rejoinToastBody and othersInCall (#302)', () => {
|
||||
assert.equal(
|
||||
rejoinToastBody('Bob Builder', true),
|
||||
'You were in a call with Bob Builder before the restart. Tap to rejoin.',
|
||||
);
|
||||
assert.match(rejoinToastBody('Voice Lounge', false), /in the call in Voice Lounge/);
|
||||
assert.equal(othersInCall(['@me:x'], '@me:x'), false);
|
||||
assert.equal(othersInCall(['@me:x', '@bob:x'], '@me:x'), true);
|
||||
});
|
||||
|
||||
@@ -67,6 +67,8 @@ export type RejoinContext = {
|
||||
ownMemberDevices: string[];
|
||||
/** Number of call memberships in the room, ours included. */
|
||||
memberCount: number;
|
||||
/** A DM call: rejoining one may ring the other person. */
|
||||
dm?: boolean;
|
||||
};
|
||||
|
||||
export type RejoinDecision = { action: 'none' } | { action: 'ask' | 'auto'; roomId: string };
|
||||
@@ -83,5 +85,18 @@ export function decideRejoin(ctx: RejoinContext): RejoinDecision {
|
||||
// Nobody there any more (our own stale membership doesn't count).
|
||||
const others = ctx.memberCount - ctx.ownMemberDevices.length;
|
||||
if (others <= 0) return { action: 'none' };
|
||||
// [Gitea #302] Never rejoin a DM call on our own: if the other person has
|
||||
// just left, starting it again would ring them. Ask instead.
|
||||
if (ctx.dm && mode === 'auto') return { action: 'ask', roomId: record.roomId };
|
||||
return { action: mode, roomId: record.roomId };
|
||||
}
|
||||
|
||||
/** Pure: whether anyone but us is still in the call (by call membership). */
|
||||
export const othersInCall = (memberUserIds: string[], myUserId: string): boolean =>
|
||||
memberUserIds.some((u) => u !== myUserId);
|
||||
|
||||
/** The rejoin toast's text; DMs name the person rather than "the call in <room>". */
|
||||
export const rejoinToastBody = (roomName: string | undefined, dm: boolean): string =>
|
||||
dm
|
||||
? `You were in a call with ${roomName ?? 'them'} before the restart. Tap to rejoin.`
|
||||
: `You were in the call in ${roomName ?? 'a room'} before the restart. Tap to rejoin.`;
|
||||
|
||||
@@ -67,18 +67,3 @@ test('declinedRing finds our own decline of that ring only (#312)', () => {
|
||||
assert.equal(declinedRing(room, '$ring2', '@me:x'), false, "someone else's decline");
|
||||
assert.equal(declinedRing(room, '$ring3', '@me:x'), false);
|
||||
});
|
||||
|
||||
test('declinedRing matches a decline of the notification or of the membership (#313)', () => {
|
||||
const decline = (rel: string) => ({
|
||||
getType: () => 'org.matrix.msc4310.rtc.decline',
|
||||
getSender: () => '@me:x',
|
||||
getRelation: () => ({ event_id: rel }),
|
||||
});
|
||||
const room = stateRoom([], [decline('$notification')]);
|
||||
assert.equal(declinedRing(room, ['$membership', '$notification'], '@me:x'), true);
|
||||
assert.equal(declinedRing(room, ['$membership'], '@me:x'), false);
|
||||
assert.equal(
|
||||
declinedRing(stateRoom([], [decline('$membership')]), ['$membership', undefined], '@me:x'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -40,18 +40,7 @@ export const hasActiveCallMembership = (room: Room, userId: string): boolean =>
|
||||
* declined it (our own RTCDecline relates to it) — e.g. replayed together with
|
||||
* the ring in the first sync after a restart, before any dismiss listener.
|
||||
*/
|
||||
export const declinedRing = (
|
||||
room: Room,
|
||||
ringIds: string | Array<string | undefined>,
|
||||
myUserId: string,
|
||||
): boolean => {
|
||||
// [Gitea #313] A decline references the notification event (MSC4310); older
|
||||
// Lotus builds referenced the call membership instead. Match either.
|
||||
const ids = new Set((Array.isArray(ringIds) ? ringIds : [ringIds]).filter(Boolean));
|
||||
return declinedAny(room, ids, myUserId);
|
||||
};
|
||||
|
||||
const declinedAny = (room: Room, ids: Set<string | undefined>, myUserId: string): boolean =>
|
||||
export const declinedRing = (room: Room, refEventId: string, myUserId: string): boolean =>
|
||||
room
|
||||
.getLiveTimeline()
|
||||
.getEvents()
|
||||
@@ -59,5 +48,5 @@ const declinedAny = (room: Room, ids: Set<string | undefined>, myUserId: string)
|
||||
(ev) =>
|
||||
ev.getType() === 'org.matrix.msc4310.rtc.decline' &&
|
||||
ev.getSender() === myUserId &&
|
||||
ids.has(ev.getRelation()?.event_id),
|
||||
ev.getRelation()?.event_id === refEventId,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user