feat: PiP mute indicator, export history, activity log, unverified device warning

- PiP call window: mute overlay using MutationObserver on EC iframe's
  [data-testid="incall_mute"] button (data-kind="primary" = muted),
  same pattern as screenshare detection in CallControl.ts

- P2-4 Export Room History: new tab in room settings — Plain Text / JSON /
  HTML formats, optional date range, progress counter, paginated via
  paginateEventTimeline, blob download; E2EE-aware (skips failed decryptions)

- P2-6 Room Activity Log: new tab in room settings — filterable log of
  m.room.member, m.room.power_levels, m.room.name/topic/avatar/server_acl
  events with human-readable descriptions, relative timestamps, Load More
  pagination

- P2-10 Unverified Device Warning: warnOnUnverifiedDevices setting (default
  off); Warning.Container banner above composer in encrypted rooms with
  unverified devices; toggle in Settings → General → Privacy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 22:13:22 -04:00
parent 6d0b778755
commit ee717e8361
10 changed files with 1030 additions and 1 deletions
+32
View File
@@ -26,6 +26,7 @@ import {
Scroll,
Spinner,
Text,
color,
config,
toRem,
} from 'folds';
@@ -122,6 +123,7 @@ import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
import { useComposingCheck } from '../../hooks/useComposingCheck';
import { VoiceMessageRecorder } from '../../components/VoiceMessageRecorder';
import { PollCreator } from './PollCreator';
import { useRoomUnverifiedDeviceCount } from '../../hooks/useDeviceVerificationStatus';
const GifPicker = React.lazy(() =>
import('../../components/GifPicker').then((m) => ({ default: m.GifPicker })),
@@ -144,6 +146,15 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
const [warnOnUnverifiedDevices] = useSetting(settingsAtom, 'warnOnUnverifiedDevices');
const crypto = mx.getCrypto();
const roomUnverifiedDeviceCount = useRoomUnverifiedDeviceCount(crypto, room);
const isEncrypted = room.hasEncryptionStateEvent();
const showUnverifiedWarning =
warnOnUnverifiedDevices &&
isEncrypted &&
roomUnverifiedDeviceCount !== undefined &&
roomUnverifiedDeviceCount > 0;
const direct = useIsDirectRoom();
const commands = useCommands(mx, room);
const emojiBtnRef = useRef<HTMLButtonElement>(null);
@@ -718,6 +729,27 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
requestClose={handleCloseAutocomplete}
/>
)}
{showUnverifiedWarning && (
<Box
alignItems="Center"
gap="200"
style={{
padding: `${config.space.S100} ${config.space.S300}`,
background: color.Warning.Container,
}}
>
<Icon
size="100"
src={Icons.Shield}
style={{ color: color.Warning.OnContainer, flexShrink: 0 }}
/>
<Text size="T200" style={{ color: color.Warning.OnContainer }}>
{roomUnverifiedDeviceCount}{' '}
{roomUnverifiedDeviceCount === 1 ? 'unverified device' : 'unverified devices'} in this
room
</Text>
</Box>
)}
<CustomEditor
editableName="RoomInput"
editor={editor}