fix(desktop): harden capture and release packaging (#2049)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-27 23:54:38 +02:00
1 parent 5ee59c4675
commit 374db9ed2b
25 files changed
+1394 -1317

No files matched your search

@@ -8,12 +8,11 @@ import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import {buildAppProtocolUrl} from '@app/features/ui/utils/AppProtocol';
import {isDesktop, openExternalUrl} from '@app/features/ui/utils/NativeUtils';
import {checkDesktopAvailable, navigateInDesktop} from '@app/features/voice/utils/DesktopRpcClient';
import {msg} from '@lingui/core/macro';
import {Trans, useLingui} from '@lingui/react/macro';
import {ArrowSquareOutIcon} from '@phosphor-icons/react';
import type React from 'react';
import {useEffect, useState} from 'react';
import {useState} from 'react';
interface DesktopDeepLinkPromptProps {
code: string;
@@ -36,25 +35,9 @@ const FAILED_TO_OPEN_IN_DESKTOP_APP_DESCRIPTOR = msg({
export const DesktopDeepLinkPrompt: React.FC<DesktopDeepLinkPromptProps> = ({code, kind, preferLogin = false}) => {
const {i18n} = useLingui();
const [isLoading, setIsLoading] = useState(false);
const [desktopAvailable, setDesktopAvailable] = useState<boolean | null>(null);
const [error, setError] = useState<string | null>(null);
const isMobileBrowser = Platform.isMobileBrowser;
const useProtocolLaunch = kind === 'invite';
const shouldProbeDesktopAvailability = !useProtocolLaunch;
useEffect(() => {
if (isDesktop() || !shouldProbeDesktopAvailability) return;
let cancelled = false;
checkDesktopAvailable().then(({available}) => {
if (!cancelled) {
setDesktopAvailable(available);
}
});
return () => {
cancelled = true;
};
}, [shouldProbeDesktopAvailability]);
if (isDesktop() || isMobileBrowser) return null;
if (shouldProbeDesktopAvailability && desktopAvailable !== true) return null;
const getPath = (): string => {
switch (kind) {
case 'invite':
@@ -69,20 +52,12 @@ export const DesktopDeepLinkPrompt: React.FC<DesktopDeepLinkPromptProps> = ({cod
const handleOpen = async () => {
setIsLoading(true);
setError(null);
if (useProtocolLaunch) {
try {
await openExternalUrl(buildAppProtocolUrl(path));
} catch {
setError(i18n._(FAILED_TO_OPEN_IN_DESKTOP_APP_DESCRIPTOR));
} finally {
setIsLoading(false);
}
return;
}
const result = await navigateInDesktop(path);
setIsLoading(false);
if (!result.success) {
try {
await openExternalUrl(buildAppProtocolUrl(path));
} catch {
setError(i18n._(FAILED_TO_OPEN_IN_DESKTOP_APP_DESCRIPTOR));
} finally {
setIsLoading(false);
}
};
return (
@@ -180,10 +180,6 @@ export function handleDeepLinkUrl(rawUrl: string): boolean {
return true;
}
export function handleRpcNavigation(path: string): void {
RouterUtils.transitionTo(path);
}
let listenerStarted = false;
export async function startDeepLinkHandling(): Promise<void> {
@@ -206,17 +202,6 @@ export async function startDeepLinkHandling(): Promise<void> {
logger.error(' Failed to handle URL', url, error);
}
});
if (typeof electronApi.onRpcNavigate === 'function') {
electronApi.onRpcNavigate((path: string) => {
try {
handleRpcNavigation(path);
} catch (error) {
logger.error(' Failed to handle RPC navigation', path, error);
}
});
} else {
logger.warn(' onRpcNavigate not available on this host version');
}
return;
}
}
@@ -344,7 +344,6 @@ export interface ElectronAPI {
pasteFromClipboard: () => Promise<void>;
onDeepLink: (callback: (url: string) => void) => () => void;
getInitialDeepLink: () => Promise<string | null>;
onRpcNavigate: (callback: (path: string) => void) => () => void;
autostartEnable: () => Promise<void>;
autostartDisable: () => Promise<void>;
autostartIsEnabled: () => Promise<boolean>;
@@ -1,117 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import Config from '@app/features/app/config/Config';
const RPC_PORT_STABLE = 21863;
const RPC_PORT_CANARY = 21864;
const RPC_PORTS =
Config.PUBLIC_RELEASE_CHANNEL === 'canary' ? [RPC_PORT_CANARY, RPC_PORT_STABLE] : [RPC_PORT_STABLE, RPC_PORT_CANARY];
interface RpcResponse<T = unknown> {
success: boolean;
data?: T;
error?: string;
}
interface HealthResponse {
status: string;
channel: string;
version: string;
platform: string;
}
interface NavigateResponse {
navigated: boolean;
path: string;
}
let cachedAvailablePort: number | null = null;
let lastHealthCheck = 0;
const HEALTH_CHECK_CACHE_MS = 5000;
async function rpcFetch<T>(port: number, endpoint: string, options?: RequestInit): Promise<RpcResponse<T> | null> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2000);
try {
const response = await fetch(`http://127.0.0.1:${port}${endpoint}`, {
...options,
signal: controller.signal,
});
return (await response.json()) as RpcResponse<T>;
} catch {
return null;
} finally {
clearTimeout(timeout);
}
}
export async function checkDesktopAvailable(): Promise<{
available: boolean;
port: number | null;
info: HealthResponse | null;
}> {
const now = Date.now();
if (cachedAvailablePort !== null && now - lastHealthCheck < HEALTH_CHECK_CACHE_MS) {
const result = await rpcFetch<HealthResponse>(cachedAvailablePort, '/health');
if (result?.success && result.data) {
return {available: true, port: cachedAvailablePort, info: result.data};
}
cachedAvailablePort = null;
}
for (const port of RPC_PORTS) {
const result = await rpcFetch<HealthResponse>(port, '/health');
if (result?.success && result.data) {
cachedAvailablePort = port;
lastHealthCheck = now;
return {available: true, port, info: result.data};
}
}
cachedAvailablePort = null;
return {available: false, port: null, info: null};
}
export async function navigateInDesktop(path: string): Promise<{
success: boolean;
error?: string;
}> {
const {available, port} = await checkDesktopAvailable();
if (!available || port === null) {
return {success: false, error: 'desktop_unavailable'};
}
const result = await rpcFetch<NavigateResponse>(port, '/navigate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({method: 'navigate', params: {path}}),
});
if (!result) {
return {success: false, error: 'desktop_rpc_unreachable'};
}
if (!result.success) {
return {success: false, error: result.error ?? 'desktop_rpc_unknown'};
}
return {success: true};
}
export async function focusDesktop(): Promise<{
success: boolean;
error?: string;
}> {
const {available, port} = await checkDesktopAvailable();
if (!available || port === null) {
return {success: false, error: 'desktop_unavailable'};
}
const result = await rpcFetch(port, '/focus', {method: 'POST'});
if (!result) {
return {success: false, error: 'desktop_rpc_unreachable'};
}
if (!result.success) {
return {success: false, error: result.error ?? 'desktop_rpc_unknown'};
}
return {success: true};
}
export function resetDesktopRpcCache(): void {
cachedAvailablePort = null;
lastHealthCheck = 0;
}
-1
View File
@@ -483,7 +483,6 @@ export interface ElectronAPI {
passkeyIsSupported?(): Promise<boolean>;
passkeyRegister?(options: unknown, requestContext?: {pin?: string}): Promise<RegistrationResponseJSON>;
passkeyAuthenticate?(options: unknown, requestContext?: {pin?: string}): Promise<AuthenticationResponseJSON>;
onRpcNavigate?(callback: (path: string) => void): () => void;
getOpenH264Status?(): Promise<OpenH264Status>;
setOpenH264Enabled?(enabled: boolean): Promise<OpenH264Status>;
virtmic?: VirtmicApi;