Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be71931a81 |
@@ -89,9 +89,6 @@ import { zIndices } from '../styles/zIndex';
|
||||
|
||||
const PIP_MIN_W = 200;
|
||||
const PIP_MIN_H = 112;
|
||||
/** [Gitea #323] Minimised PiP bar size. */
|
||||
const PIP_MINI_W = 220;
|
||||
const PIP_MINI_H = 40;
|
||||
|
||||
type Corner = 'se' | 'sw' | 'ne' | 'nw';
|
||||
|
||||
@@ -984,28 +981,6 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
);
|
||||
|
||||
const [pipIsFullscreen, setPipIsFullscreen] = useState(false);
|
||||
// [Gitea #323] Minimised PiP: a small draggable bar (room name + expand)
|
||||
// instead of the video. Remembered; phones start minimised.
|
||||
const [pipMinimized, setPipMinimized] = useState<boolean>(() => {
|
||||
try {
|
||||
const v = localStorage.getItem('pip-minimized');
|
||||
if (v !== null) return v === '1';
|
||||
} catch {
|
||||
/* storage unavailable */
|
||||
}
|
||||
return window.innerWidth < 600;
|
||||
});
|
||||
const pipFullSizeRef = React.useRef({ width: 280, height: 158 });
|
||||
const togglePipMinimized = useCallback(() => {
|
||||
setPipMinimized((m) => {
|
||||
try {
|
||||
localStorage.setItem('pip-minimized', m ? '0' : '1');
|
||||
} catch {
|
||||
/* storage unavailable */
|
||||
}
|
||||
return !m;
|
||||
});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const onFsChange = () => setPipIsFullscreen(!!document.fullscreenElement);
|
||||
document.addEventListener('fullscreenchange', onFsChange);
|
||||
@@ -1103,26 +1078,6 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
}
|
||||
}, [pipMode, callVisible]);
|
||||
|
||||
// [Gitea #323] Apply the minimised / full PiP size (after the entry effect
|
||||
// below has positioned and sized it). Keeps the bar on screen.
|
||||
React.useEffect(() => {
|
||||
const el = callEmbedRef.current;
|
||||
if (!el || !pipMode) return;
|
||||
if (pipMinimized) {
|
||||
if (el.offsetHeight > PIP_MINI_H) {
|
||||
pipFullSizeRef.current = { width: el.offsetWidth, height: el.offsetHeight };
|
||||
}
|
||||
el.style.width = `${PIP_MINI_W}px`;
|
||||
el.style.height = `${PIP_MINI_H}px`;
|
||||
} else {
|
||||
el.style.width = `${pipFullSizeRef.current.width}px`;
|
||||
el.style.height = `${pipFullSizeRef.current.height}px`;
|
||||
}
|
||||
const rect = el.getBoundingClientRect();
|
||||
el.style.left = `${Math.max(0, Math.min(rect.left, window.innerWidth - el.offsetWidth))}px`;
|
||||
el.style.top = `${Math.max(0, Math.min(rect.top, window.innerHeight - el.offsetHeight))}px`;
|
||||
}, [pipMode, pipMinimized, callEmbedRef]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!pipMode) return;
|
||||
const onPipWindowResize = (): void => {
|
||||
@@ -1441,50 +1396,19 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 1,
|
||||
// Minimised: cover the frame so only the bar shows.
|
||||
background: pipMinimized ? color.Surface.Container : 'transparent',
|
||||
background: 'transparent',
|
||||
cursor: 'grab',
|
||||
// [Gitea #43] The call's "Share your screen?" prompt is drawn
|
||||
// inside the frame: let the click reach its Share button.
|
||||
pointerEvents: pipPromptOpen ? 'none' : undefined,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: pipMinimized ? 'center' : 'flex-start',
|
||||
padding: '6px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: config.space.S100,
|
||||
alignItems: 'center',
|
||||
...(pipMinimized ? { width: '100%' } : {}),
|
||||
}}
|
||||
>
|
||||
{/* Leftmost: the top-right corner is a resize handle. */}
|
||||
<IconButton
|
||||
type="button"
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Surface"
|
||||
fill="None"
|
||||
aria-label={pipMinimized ? 'Expand call' : 'Minimise call'}
|
||||
aria-pressed={pipMinimized}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
togglePipMinimized();
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onTouchStart={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
background: 'rgba(0,0,0,0.65)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
<Icon size="100" src={pipMinimized ? Icons.ChevronTop : Icons.Minus} />
|
||||
</IconButton>
|
||||
{!pipMinimized && document.fullscreenEnabled && (
|
||||
<div style={{ display: 'flex', gap: config.space.S100, alignItems: 'center' }}>
|
||||
{document.fullscreenEnabled && (
|
||||
<IconButton
|
||||
type="button"
|
||||
size="300"
|
||||
@@ -1516,23 +1440,14 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
pointerEvents: 'none',
|
||||
...(pipMinimized
|
||||
? {
|
||||
flexGrow: 1,
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{pipMinimized ? `↗ ${callEmbed.room.name ?? 'Call'}` : '↗ Return to call'}
|
||||
↗ Return to call
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!pipMinimized && <PipMuteOverlay callEmbed={callEmbed} />}
|
||||
{(pipMinimized ? [] : (['se', 'sw', 'ne', 'nw'] as Corner[])).map((corner) => {
|
||||
<PipMuteOverlay callEmbed={callEmbed} />
|
||||
{(['se', 'sw', 'ne', 'nw'] as Corner[]).map((corner) => {
|
||||
const s = corner.includes('s');
|
||||
const e2 = corner.includes('e');
|
||||
const dots = [
|
||||
|
||||
@@ -47,8 +47,12 @@ import {
|
||||
recentForwardTargetsAtom,
|
||||
addRecentForwardTarget,
|
||||
} from '../../../state/recentForwardTargets';
|
||||
import { buildForwardContent, buildPlaintextAttachmentContent } from './forwardContent';
|
||||
import { useLocalRoomNames } from '../../../hooks/useRoomMeta';
|
||||
import {
|
||||
buildForwardContent,
|
||||
buildPlaintextAttachmentContent,
|
||||
forwardEncryptionWarning,
|
||||
} from './forwardContent';
|
||||
import { useLocalRoomNames, getLocalRoomName } from '../../../hooks/useRoomMeta';
|
||||
|
||||
type RoomRowProps = {
|
||||
room: Room;
|
||||
@@ -275,6 +279,14 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
||||
// Selection persists across query changes: a room selected then filtered out
|
||||
// of the rendered slice stays selected.
|
||||
const [selectedRoomIds, setSelectedRoomIds] = useState<Set<string>>(new Set());
|
||||
const encryptionWarning = forwardEncryptionWarning(
|
||||
!!mx.getRoom(mEvent.getRoomId() ?? '')?.hasEncryptionStateEvent(),
|
||||
[...selectedRoomIds]
|
||||
.map((id) => mx.getRoom(id))
|
||||
.filter((r): r is Room => !!r && !r.hasEncryptionStateEvent())
|
||||
// The name the user sees (their own "Rename for me" name, if set).
|
||||
.map((r) => getLocalRoomName(mx, r.roomId) ?? r.name),
|
||||
);
|
||||
|
||||
const toggleRoom = useCallback((roomId: string) => {
|
||||
setSelectedRoomIds((prev) => {
|
||||
@@ -590,7 +602,13 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
||||
shrink="No"
|
||||
direction="Column"
|
||||
style={{ padding: `${config.space.S200} ${config.space.S400}` }}
|
||||
gap="200"
|
||||
>
|
||||
{encryptionWarning && (
|
||||
<Text size="T200" role="note" style={{ color: color.Warning.Main }}>
|
||||
{encryptionWarning}
|
||||
</Text>
|
||||
)}
|
||||
<Button
|
||||
variant="Primary"
|
||||
size="400"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { MatrixClient, MatrixEvent } from 'matrix-js-sdk';
|
||||
import { buildForwardContent, getForwardedMeta } from './forwardContent';
|
||||
import { buildForwardContent, forwardEncryptionWarning, getForwardedMeta } from './forwardContent';
|
||||
|
||||
// Pure content builder buildForwardContent: refuses undecryptable events, forwards
|
||||
// the latest edit (`m.new_content`), and strips reply fallbacks + `m.relates_to`.
|
||||
@@ -171,3 +171,16 @@ test('getForwardedMeta rejects malformed metadata', () => {
|
||||
assert.equal(getForwardedMeta({ 'io.lotus.forwarded': { sender: 1 } }), undefined);
|
||||
assert.equal(getForwardedMeta({}), undefined);
|
||||
});
|
||||
|
||||
test('forwardEncryptionWarning: only from an encrypted room into unencrypted ones (#330)', () => {
|
||||
assert.equal(forwardEncryptionWarning(false, ['general']), undefined);
|
||||
assert.equal(forwardEncryptionWarning(true, []), undefined);
|
||||
assert.match(
|
||||
forwardEncryptionWarning(true, ['general']) ?? '',
|
||||
/^general isn't encrypted\..*that room, along with who originally wrote it\.$/,
|
||||
);
|
||||
assert.match(
|
||||
forwardEncryptionWarning(true, ['a', 'b']) ?? '',
|
||||
/^2 of the rooms you picked aren't encrypted\..*those rooms/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -122,3 +122,23 @@ export async function buildPlaintextAttachmentContent(
|
||||
|
||||
return plainContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Gitea #330] The note shown when forwarding out of an encrypted room into
|
||||
* rooms that aren't: the text (and a re-uploaded attachment) becomes readable
|
||||
* by the server, and the original author is shown with it. Undefined when
|
||||
* there's nothing to warn about.
|
||||
*/
|
||||
export function forwardEncryptionWarning(
|
||||
sourceEncrypted: boolean,
|
||||
unencryptedDestNames: string[],
|
||||
): string | undefined {
|
||||
if (!sourceEncrypted || unencryptedDestNames.length === 0) return undefined;
|
||||
const where =
|
||||
unencryptedDestNames.length === 1
|
||||
? `${unencryptedDestNames[0]} isn't encrypted.`
|
||||
: `${unencryptedDestNames.length} of the rooms you picked aren't encrypted.`;
|
||||
return `${where} The forwarded message (and any attachment) will be readable by the server and everyone in ${
|
||||
unencryptedDestNames.length === 1 ? 'that room' : 'those rooms'
|
||||
}, along with who originally wrote it.`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user