Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca284a75e3 | ||
|
|
b07eb35159 | ||
|
|
f41de57893 |
@@ -90,9 +90,6 @@ import { useDenoiseNotices } from '../hooks/useDenoiseNotices';
|
||||
|
||||
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;
|
||||
/** Default PiP top: just below the 64px-ish room header. */
|
||||
const PIP_DEFAULT_TOP = 72;
|
||||
|
||||
@@ -1006,28 +1003,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);
|
||||
@@ -1128,26 +1103,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 => {
|
||||
@@ -1466,50 +1421,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"
|
||||
@@ -1541,23 +1465,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 = [
|
||||
|
||||
@@ -108,6 +108,7 @@ import {
|
||||
TRANSLATE_TARGET_LANGUAGES,
|
||||
normalizeLang,
|
||||
isSupportedTargetLang,
|
||||
translationUnavailableText,
|
||||
} from '../../../utils/translation/langUtils';
|
||||
import { chromeTranslationEngine } from '../../../utils/translation/chromeEngine';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
@@ -2756,7 +2757,7 @@ function Messages() {
|
||||
description={
|
||||
translationSupported
|
||||
? 'Translate messages written in other languages into this one. Runs entirely on your device — message text never leaves your computer or touches a cloud service.'
|
||||
: 'On-device translation isn’t available in this browser. Use a Chromium desktop browser (Chrome/Edge 138+) or the Lotus desktop app.'
|
||||
: translationUnavailableText(isTauriEnv())
|
||||
}
|
||||
after={
|
||||
<SettingsSelect
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
translationUnavailableText,
|
||||
normalizeLang,
|
||||
sameLanguage,
|
||||
languageName,
|
||||
@@ -53,3 +54,9 @@ test('curated list: unique codes, English present', () => {
|
||||
assert.equal(new Set(codes).size, codes.length);
|
||||
assert.ok(codes.includes('en'));
|
||||
});
|
||||
|
||||
test('translationUnavailableText: no "use the desktop app" inside the desktop app (#349)', () => {
|
||||
assert.doesNotMatch(translationUnavailableText(true), /desktop app\.$|Lotus desktop app/);
|
||||
assert.match(translationUnavailableText(true), /this version of the desktop app/);
|
||||
assert.match(translationUnavailableText(false), /this browser/);
|
||||
});
|
||||
|
||||
@@ -86,6 +86,16 @@ export function isSupportedTargetLang(code: string): boolean {
|
||||
return CURATED_NAME.has(normalizeLang(code));
|
||||
}
|
||||
|
||||
/**
|
||||
* [Gitea #349] Why translation is unavailable, by platform. The desktop app
|
||||
* on Linux/macOS (WebKit) lacks the on-device APIs too, so it must not send
|
||||
* people to "the Lotus desktop app" they're already using.
|
||||
*/
|
||||
export const translationUnavailableText = (desktopApp: boolean): string =>
|
||||
desktopApp
|
||||
? 'On-device translation isn’t available in this version of the desktop app. It works in Chrome or Edge 138+ on a computer.'
|
||||
: 'On-device translation isn’t available in this browser. Use Chrome or Edge 138+ on a computer.';
|
||||
|
||||
/** Pure: why a manual Translate did nothing (#287). */
|
||||
export const translateSkipMessage = (from: string | undefined, targetLang: string): string =>
|
||||
from
|
||||
|
||||
Reference in New Issue
Block a user