Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4c54dad7a |
@@ -89,6 +89,9 @@ 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';
|
||||
|
||||
@@ -981,6 +984,28 @@ 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);
|
||||
@@ -1078,6 +1103,26 @@ 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 => {
|
||||
@@ -1396,19 +1441,50 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 1,
|
||||
background: 'transparent',
|
||||
// Minimised: cover the frame so only the bar shows.
|
||||
background: pipMinimized ? color.Surface.Container : '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' }}>
|
||||
{document.fullscreenEnabled && (
|
||||
<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 && (
|
||||
<IconButton
|
||||
type="button"
|
||||
size="300"
|
||||
@@ -1440,14 +1516,23 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
pointerEvents: 'none',
|
||||
...(pipMinimized
|
||||
? {
|
||||
flexGrow: 1,
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
↗ Return to call
|
||||
{pipMinimized ? `↗ ${callEmbed.room.name ?? 'Call'}` : '↗ Return to call'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PipMuteOverlay callEmbed={callEmbed} />
|
||||
{(['se', 'sw', 'ne', 'nw'] as Corner[]).map((corner) => {
|
||||
{!pipMinimized && <PipMuteOverlay callEmbed={callEmbed} />}
|
||||
{(pipMinimized ? [] : (['se', 'sw', 'ne', 'nw'] as Corner[])).map((corner) => {
|
||||
const s = corner.includes('s');
|
||||
const e2 = corner.includes('e');
|
||||
const dots = [
|
||||
|
||||
@@ -16,7 +16,7 @@ import { SettingTile } from '../setting-tile';
|
||||
import { UsageSwitcher } from './UsageSwitcher';
|
||||
import { ImagePackProfile, ImagePackProfileEdit } from './PackMeta';
|
||||
import * as css from './style.css';
|
||||
import { useStrippedFilePicker } from '../../hooks/useStrippedFilePicker';
|
||||
import { useFilePicker } from '../../hooks/useFilePicker';
|
||||
import { CompactUploadCardRenderer } from '../upload-card';
|
||||
import { UploadSuccess } from '../../state/upload';
|
||||
import { getImageInfo, TUploadContent } from '../../utils/matrix';
|
||||
@@ -60,7 +60,7 @@ export const ImagePackContent = as<'div', ImagePackContentProps>(
|
||||
[imagePack, savedImages, uploadedImages],
|
||||
);
|
||||
|
||||
const pickFiles = useStrippedFilePicker(
|
||||
const pickFiles = useFilePicker(
|
||||
useCallback(
|
||||
(pickedFiles: File[]) => {
|
||||
const uniqueFiles = pickedFiles.map((file) => {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { nameInitials } from '../../utils/common';
|
||||
import { BreakWord } from '../../styles/Text.css';
|
||||
import { LINKIFY_OPTS } from '../../plugins/react-custom-html-parser';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
import { useStrippedFilePicker } from '../../hooks/useStrippedFilePicker';
|
||||
import { useFilePicker } from '../../hooks/useFilePicker';
|
||||
import { useObjectURL } from '../../hooks/useObjectURL';
|
||||
import { createUploadAtom, UploadSuccess } from '../../state/upload';
|
||||
import { CompactUploadCardRenderer } from '../upload-card';
|
||||
@@ -111,7 +111,7 @@ export function ImagePackProfileEdit({ meta, onCancel, onSave }: ImagePackProfil
|
||||
return undefined;
|
||||
}, [imageFile]);
|
||||
|
||||
const pickFile = useStrippedFilePicker(setImageFile, false);
|
||||
const pickFile = useFilePicker(setImageFile, false);
|
||||
|
||||
const handleRemoveUpload = useCallback(() => {
|
||||
setImageFile(undefined);
|
||||
|
||||
@@ -40,7 +40,7 @@ import { useImagePackRooms } from '../../../hooks/useImagePackRooms';
|
||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useStrippedFilePicker } from '../../../hooks/useStrippedFilePicker';
|
||||
import { useFilePicker } from '../../../hooks/useFilePicker';
|
||||
import { CompactUploadCardRenderer } from '../../../components/upload-card';
|
||||
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
@@ -68,7 +68,7 @@ function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) {
|
||||
const imagePackRooms = useImagePackRooms(room.roomId, roomToParents);
|
||||
|
||||
const [iconFile, setIconFile] = useState<File>();
|
||||
const pickFile = useStrippedFilePicker(setIconFile, false);
|
||||
const pickFile = useFilePicker(setIconFile, false);
|
||||
|
||||
const [tagColor, setTagColor] = useState<string | undefined>(tag?.color);
|
||||
const [tagIcon, setTagIcon] = useState<MemberPowerTagIcon | undefined>(tag?.icon);
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useFilePicker } from './useFilePicker';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { stripImageMetadata } from '../utils/stripImageMetadata';
|
||||
|
||||
/**
|
||||
* [Gitea #332] `useFilePicker` for images that get published (emoji / sticker
|
||||
* packs, pack avatars, power-level badges): applies "Remove Photo Metadata"
|
||||
* like message attachments and avatars, so a phone photo's GPS location and
|
||||
* timestamp don't go to every member of the room or space.
|
||||
*/
|
||||
export const useStrippedFilePicker = <M extends boolean | undefined = undefined>(
|
||||
onSelect: (file: M extends true ? File[] : File) => void,
|
||||
multiple?: M,
|
||||
) => {
|
||||
const [strip] = useSetting(settingsAtom, 'stripImageMetadata');
|
||||
const handle = useCallback(
|
||||
(picked: File | File[]) => {
|
||||
if (!strip) {
|
||||
onSelect(picked as M extends true ? File[] : File);
|
||||
return;
|
||||
}
|
||||
const clean = (f: File) => stripImageMetadata(f).then((r) => r.file);
|
||||
if (Array.isArray(picked)) {
|
||||
Promise.all(picked.map(clean)).then((files) =>
|
||||
onSelect(files as M extends true ? File[] : File),
|
||||
);
|
||||
} else {
|
||||
clean(picked).then((file) => onSelect(file as M extends true ? File[] : File));
|
||||
}
|
||||
},
|
||||
[strip, onSelect],
|
||||
);
|
||||
return useFilePicker(handle as (file: M extends true ? File[] : File) => void, multiple);
|
||||
};
|
||||
Reference in New Issue
Block a user