Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78691f1fff | ||
|
|
400ee76b63 | ||
|
|
47c1e435b7 | ||
|
|
4650796120 |
@@ -20,7 +20,6 @@ import {
|
||||
import {
|
||||
EventTimelineSetHandlerMap,
|
||||
EventType,
|
||||
JoinRule,
|
||||
RelationType,
|
||||
Room,
|
||||
RoomEvent,
|
||||
@@ -72,7 +71,8 @@ import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { useCallPreferences } from '../state/hooks/callPreferences';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { getStateEvent, getStateEvents, getMemberName } from '../utils/room';
|
||||
import { getStateEvent, getMemberName } from '../utils/room';
|
||||
import { isRingableRoom } from '../utils/callRing';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels';
|
||||
import { getRoomCreatorsForRoomId } from '../hooks/useRoomCreators';
|
||||
@@ -581,12 +581,7 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
||||
// Only ring in rooms where the call button is visible: DMs or invite-only rooms
|
||||
// with no space parent. Persistent voice rooms (call rooms), space channels,
|
||||
// restricted rooms, and public rooms must never trigger ringing.
|
||||
if (room.isCallRoom()) return;
|
||||
const isDirect = directs.has(room.roomId);
|
||||
const isSpaceChild = getStateEvents(room, StateEvent.SpaceParent).length > 0;
|
||||
const joinRule = room.getJoinRule();
|
||||
const isPrivateInviteGroup = !isSpaceChild && joinRule === JoinRule.Invite;
|
||||
if (!isDirect && !isPrivateInviteGroup) return;
|
||||
if (!isRingableRoom(room, directs.has(room.roomId))) return;
|
||||
|
||||
const info: IncomingCallInfo = {
|
||||
room,
|
||||
|
||||
@@ -160,6 +160,7 @@ import { scheduledMessagesAtom } from '../../state/scheduledMessages';
|
||||
import { createErrorToast, toastQueueAtom } from '../../state/toast';
|
||||
import { getThreadDraftKey } from '../../state/room/thread';
|
||||
import { stripTrackingParamsInHtml, stripTrackingParamsInText } from '../../utils/urlTracking';
|
||||
import { plaintextCachesWiped } from '../../state/plaintextCaches';
|
||||
|
||||
const GifPicker = React.lazy(() =>
|
||||
import('../../components/GifPicker').then((m) => ({ default: m.GifPicker })),
|
||||
@@ -562,6 +563,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
// Persist the current editor state to the draft atom + localStorage; an empty
|
||||
// editor clears both so the draft indicators never claim a draft that isn't there.
|
||||
const persistDraft = useCallback(() => {
|
||||
// [Gitea #319] Logout already wiped the drafts; don't write this one back.
|
||||
if (plaintextCachesWiped()) return;
|
||||
if (!isEmptyEditor(editor)) {
|
||||
const parsedDraft = JSON.parse(JSON.stringify(editor.children));
|
||||
setMsgDraft(parsedDraft);
|
||||
|
||||
@@ -138,6 +138,7 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
|
||||
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
|
||||
import { useIsDirectRoom } from '../../hooks/useRoom';
|
||||
import { isRingableRoom } from '../../utils/callRing';
|
||||
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
|
||||
import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||
@@ -2030,6 +2031,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
[EventType.RTCNotification]: (mEventId, mEvent, item) => {
|
||||
const senderId = mEvent.getSender() ?? '';
|
||||
if (!senderId || senderId === mx.getUserId()) return null;
|
||||
// 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;
|
||||
const content = mEvent.getContent();
|
||||
const mention =
|
||||
content['m.mentions']?.room ||
|
||||
|
||||
@@ -26,7 +26,7 @@ const api = {
|
||||
: Object.getOwnPropertyDescriptor(target, key),
|
||||
});
|
||||
|
||||
const { clearPlaintextCaches } = await import('./plaintextCaches');
|
||||
const { clearPlaintextCaches, plaintextCachesWiped } = await import('./plaintextCaches');
|
||||
|
||||
test('clearPlaintextCaches removes every plaintext/PII localStorage key', () => {
|
||||
store.clear();
|
||||
@@ -94,3 +94,13 @@ test('clearPlaintextCaches removes the call-volume list and the settings-sync st
|
||||
assert.equal(store.has('settings-sync-meta:@me:server'), false);
|
||||
assert.equal(store.has('settings'), true, 'device preferences are kept');
|
||||
});
|
||||
|
||||
test('clearPlaintextCaches clears the encrypted-search opt-in and flags the wipe (#296, #319)', () => {
|
||||
store.clear();
|
||||
store.set('searchCacheEnabled', 'true');
|
||||
store.set('mxjssdk_memory_filter_FILTER_SYNC_@me:server', '0');
|
||||
clearPlaintextCaches('@me:server');
|
||||
assert.equal(store.has('searchCacheEnabled'), false);
|
||||
assert.equal(store.has('mxjssdk_memory_filter_FILTER_SYNC_@me:server'), false);
|
||||
assert.equal(plaintextCachesWiped(), true, 'draft writes stop until the page reloads');
|
||||
});
|
||||
|
||||
@@ -112,7 +112,34 @@ const clearCallVolumesAndSyncMeta = (userId?: string): void => {
|
||||
});
|
||||
};
|
||||
|
||||
// [Gitea #319] Set once a logout has wiped the caches. Logout then reloads the
|
||||
// page, and the reload's `pagehide` made RoomInput flush the open composer's
|
||||
// draft straight back to localStorage; draft writes check this and stop.
|
||||
// Module state, so it resets with the new page.
|
||||
let wiped = false;
|
||||
export const plaintextCachesWiped = (): boolean => wiped;
|
||||
|
||||
/**
|
||||
* [Gitea #296] Device-wide flags/keys that carry the previous account's choice
|
||||
* or id: the encrypted-search opt-in (the next account would otherwise start
|
||||
* indexing decrypted text without opting in) and the SDK's per-user filter key.
|
||||
*/
|
||||
const clearAccountScopedFlags = (userId?: string): void => {
|
||||
const keys = [
|
||||
'searchCacheEnabled',
|
||||
...(userId ? [`mxjssdk_memory_filter_FILTER_SYNC_${userId}`] : []),
|
||||
];
|
||||
keys.forEach((key) => {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const clearPlaintextCaches = (userId?: string): void => {
|
||||
wiped = true;
|
||||
clearTranslationCache();
|
||||
clearScheduledMessages();
|
||||
clearRecentSearches();
|
||||
@@ -124,5 +151,6 @@ export const clearPlaintextCaches = (userId?: string): void => {
|
||||
clearCallSession();
|
||||
clearStatusMessage();
|
||||
clearCallVolumesAndSyncMeta(userId);
|
||||
clearAccountScopedFlags(userId);
|
||||
if (userId) clearNavToActivePathStore(userId);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import type { Room } from 'matrix-js-sdk';
|
||||
import { isRingableRoom } from './callRing';
|
||||
|
||||
const fakeRoom = (opts: { callRoom?: boolean; joinRule?: string; spaceParent?: boolean }): Room =>
|
||||
({
|
||||
isCallRoom: () => !!opts.callRoom,
|
||||
getJoinRule: () => opts.joinRule ?? 'invite',
|
||||
getLiveTimeline: () => ({
|
||||
getState: () => ({ getStateEvents: () => (opts.spaceParent ? [{}] : []) }),
|
||||
}),
|
||||
}) as unknown as Room;
|
||||
|
||||
test('voice rooms never ring or show "missed call", even as DMs', () => {
|
||||
assert.equal(isRingableRoom(fakeRoom({ callRoom: true }), false), false);
|
||||
assert.equal(isRingableRoom(fakeRoom({ callRoom: true }), true), false);
|
||||
});
|
||||
|
||||
test('DMs and invite-only groups outside a space ring', () => {
|
||||
assert.equal(isRingableRoom(fakeRoom({ joinRule: 'public' }), true), true);
|
||||
assert.equal(isRingableRoom(fakeRoom({ joinRule: 'invite' }), false), true);
|
||||
});
|
||||
|
||||
test('space channels, public and restricted rooms do not ring', () => {
|
||||
assert.equal(isRingableRoom(fakeRoom({ joinRule: 'invite', spaceParent: true }), false), false);
|
||||
assert.equal(isRingableRoom(fakeRoom({ joinRule: 'public' }), false), false);
|
||||
assert.equal(isRingableRoom(fakeRoom({ joinRule: 'restricted' }), false), false);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { JoinRule, Room } from 'matrix-js-sdk';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { getStateEvents } from './room';
|
||||
|
||||
/**
|
||||
* Whether a call notification in this room may ring — and, by the same rule,
|
||||
* whether an unanswered one may show as "Missed call" (Gitea #162 follow-up).
|
||||
* Only DMs and invite-only groups outside any space ring, i.e. rooms where the
|
||||
* call button is shown. Persistent voice rooms, space channels, restricted and
|
||||
* public rooms never do: joining a voice channel alone is normal and isn't a
|
||||
* "call" anyone missed.
|
||||
*/
|
||||
export const isRingableRoom = (room: Room, isDirect: boolean): boolean => {
|
||||
if (room.isCallRoom()) return false;
|
||||
if (isDirect) return true;
|
||||
const isSpaceChild = getStateEvents(room, StateEvent.SpaceParent).length > 0;
|
||||
return !isSpaceChild && room.getJoinRule() === JoinRule.Invite;
|
||||
};
|
||||
Reference in New Issue
Block a user