mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
feat(desktop): sign every Windows release artifact (#1548)
This commit is contained in:
1 parent
873c203b5d
commit
6e66c92dca
89 files changed
+2408
-2740
No files matched your search
@@ -6,7 +6,6 @@ import DeveloperMode from '@app/features/devtools/state/DeveloperMode';
|
||||
import {UNKNOWN_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {
|
||||
formatClientBuildInfo,
|
||||
formatDesktopBuildVariantLabel,
|
||||
formatReleaseChannelLabel,
|
||||
getClientInfo,
|
||||
getClientInfoSync,
|
||||
@@ -84,12 +83,7 @@ export const ClientInfo = observer(() => {
|
||||
const buildVersion = Config.PUBLIC_BUILD_VERSION || 'dev';
|
||||
const desktopReleaseChannel = desktopChannel ? formatReleaseChannelLabel(desktopChannel) : null;
|
||||
const primaryDesktopReleaseChannel = desktopReleaseChannel ?? releaseChannel;
|
||||
const desktopBuildVariant =
|
||||
clientInfo.desktopBuildVariant && clientInfo.desktopBuildVariant !== 'default'
|
||||
? formatDesktopBuildVariantLabel(clientInfo.desktopBuildVariant)
|
||||
: null;
|
||||
const desktopVersionLabel =
|
||||
desktopVersion && desktopBuildVariant ? `${desktopVersion} (${desktopBuildVariant})` : desktopVersion;
|
||||
const desktopVersionLabel = desktopVersion;
|
||||
const desktopBuildLabel = desktopVersion
|
||||
? i18n._(DESKTOP_BUILD_DESCRIPTOR, {
|
||||
desktopChannel: primaryDesktopReleaseChannel,
|
||||
|
||||
@@ -126,7 +126,6 @@ class Updater {
|
||||
currentVersion: string | null = null;
|
||||
channel: string | null = null;
|
||||
private desktopArch: string | null = null;
|
||||
private desktopBuildVariant: string | null = null;
|
||||
private isNative: boolean;
|
||||
private backgroundCheckStarted = false;
|
||||
private backgroundCheckInterval: number | null = null;
|
||||
@@ -234,10 +233,6 @@ class Updater {
|
||||
return getUpdaterDisplayVersion(this.snapshot);
|
||||
}
|
||||
|
||||
get buildVariant(): string | null {
|
||||
return this.desktopBuildVariant;
|
||||
}
|
||||
|
||||
private transition(event: UpdaterMachineEvent): void {
|
||||
runInAction(() => {
|
||||
this.snapshot = transitionUpdaterMachineSnapshot(this.snapshot, event);
|
||||
@@ -258,7 +253,6 @@ class Updater {
|
||||
runInAction(() => {
|
||||
this.currentVersion = info.desktopVersion ?? null;
|
||||
this.channel = info.desktopChannel ?? null;
|
||||
this.desktopBuildVariant = info.desktopBuildVariant ?? null;
|
||||
this.desktopArch = info.desktopArch ?? info.arch ?? null;
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -8,12 +8,9 @@ import type {
|
||||
RegistrationResponseJSON,
|
||||
} from '@simplewebauthn/browser';
|
||||
|
||||
export type DesktopBuildVariant = 'default' | 'windows-game-capture';
|
||||
|
||||
export interface DesktopInfo {
|
||||
version: string;
|
||||
channel: 'stable' | 'canary';
|
||||
buildVariant: DesktopBuildVariant;
|
||||
arch: string;
|
||||
hardwareArch: string;
|
||||
runningUnderRosetta: boolean;
|
||||
@@ -308,7 +305,6 @@ export interface AppMetricsSnapshot {
|
||||
|
||||
export interface ElectronAPI {
|
||||
platform: NodeJS.Platform;
|
||||
buildVariant: DesktopBuildVariant;
|
||||
getDesktopInfo: () => Promise<DesktopInfo>;
|
||||
getGpuInfo?: () => Promise<GpuInfo>;
|
||||
getAppMetrics?: () => Promise<AppMetricsSnapshot>;
|
||||
|
||||
@@ -19,7 +19,6 @@ export interface ClientInfo {
|
||||
arch?: string;
|
||||
desktopVersion?: string;
|
||||
desktopChannel?: string;
|
||||
desktopBuildVariant?: string;
|
||||
desktopArch?: string;
|
||||
desktopOS?: string;
|
||||
desktopRunningUnderRosetta?: boolean;
|
||||
@@ -47,13 +46,6 @@ export function formatReleaseChannelLabel(value: string): string {
|
||||
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
||||
}
|
||||
|
||||
export function formatDesktopBuildVariantLabel(value: string): string {
|
||||
if (value === 'windows-game-capture') {
|
||||
return 'Windows Game Capture';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const ARCHITECTURE_PATTERNS: ReadonlyArray<{
|
||||
pattern: RegExp;
|
||||
label: string;
|
||||
@@ -191,7 +183,6 @@ function getDesktopContextFromInfo(desktopInfo: DesktopInfo): Partial<ClientInfo
|
||||
return {
|
||||
desktopVersion: normalize(desktopInfo.version),
|
||||
desktopChannel: normalize(desktopInfo.channel),
|
||||
desktopBuildVariant: normalize(desktopInfo.buildVariant),
|
||||
desktopArch: normalizeArchitectureValue(desktopInfo.hardwareArch ?? desktopInfo.arch),
|
||||
desktopOS: normalize(desktopInfo.os),
|
||||
desktopRunningUnderRosetta: desktopInfo.runningUnderRosetta,
|
||||
@@ -312,14 +303,9 @@ export function formatClientBuildInfo(info: ClientInfo, options: {unknownLabel?:
|
||||
const desktopChannel = info.desktopChannel ? formatReleaseChannelLabel(info.desktopChannel) : null;
|
||||
const hasDesktopBuild = Boolean(info.desktopVersion);
|
||||
const primaryDesktopChannel = desktopChannel ?? releaseChannel;
|
||||
const desktopBuildVariant =
|
||||
info.desktopBuildVariant && info.desktopBuildVariant !== 'default'
|
||||
? formatDesktopBuildVariantLabel(info.desktopBuildVariant)
|
||||
: null;
|
||||
const webChannelPrefix = hasDesktopBuild && primaryDesktopChannel === releaseChannel ? '' : `${releaseChannel} `;
|
||||
const parts = [
|
||||
info.desktopVersion ? `${primaryDesktopChannel} Desktop ${info.desktopVersion}` : '',
|
||||
desktopBuildVariant ? `Desktop variant ${desktopBuildVariant}` : '',
|
||||
`${webChannelPrefix}Web ${buildVersion}`,
|
||||
osDescription,
|
||||
shouldShowBrowserInfo ? browserInfo : '',
|
||||
@@ -406,7 +392,6 @@ export async function getGatewayClientProperties(geo?: {latitude?: string | null
|
||||
build_version: Config.PUBLIC_BUILD_VERSION ?? 'dev',
|
||||
desktop_app_version: info.desktopVersion ?? null,
|
||||
desktop_app_channel: info.desktopChannel ?? null,
|
||||
desktop_app_variant: info.desktopBuildVariant ?? null,
|
||||
desktop_arch: info.desktopArch ?? info.arch ?? null,
|
||||
desktop_os: info.desktopOS ?? info.osName ?? null,
|
||||
e2ee_capable: isLiveKitE2EECapable(),
|
||||
|
||||
@@ -4414,6 +4414,10 @@ class MediaEngineFacade extends Store {
|
||||
options?: DeviceScreenShareCaptureOptions,
|
||||
publishOptions?: TrackPublishOptions,
|
||||
): Promise<void> {
|
||||
if (!isNativeVoiceEngineSelected()) {
|
||||
await voiceEngineV2AppScreenShareExecutionAdapter.startDeviceScreenShare(this.room, options, publishOptions);
|
||||
return;
|
||||
}
|
||||
await voiceEngineV2AppScreenShareExecutionAdapter.startNativeDeviceScreenShare(
|
||||
await this.getNativeDeviceScreenShareCaptureOptions(options),
|
||||
{
|
||||
@@ -4512,6 +4516,13 @@ class MediaEngineFacade extends Store {
|
||||
options?: ScreenShareCaptureOptions,
|
||||
publishOptions?: TrackPublishOptions,
|
||||
): Promise<boolean> {
|
||||
if (!isNativeVoiceEngineSelected()) {
|
||||
return voiceEngineV2AppScreenShareExecutionAdapter.replaceActiveDisplayScreenShare(
|
||||
this.room,
|
||||
options,
|
||||
publishOptions,
|
||||
);
|
||||
}
|
||||
return voiceEngineV2AppScreenShareExecutionAdapter.replaceActiveNativeDisplayScreenShareFromActiveSource(
|
||||
options,
|
||||
publishOptions,
|
||||
@@ -4522,6 +4533,13 @@ class MediaEngineFacade extends Store {
|
||||
options?: DeviceScreenShareCaptureOptions,
|
||||
publishOptions?: TrackPublishOptions,
|
||||
): Promise<boolean> {
|
||||
if (!isNativeVoiceEngineSelected()) {
|
||||
return voiceEngineV2AppScreenShareExecutionAdapter.replaceActiveDeviceScreenShare(
|
||||
this.room,
|
||||
options,
|
||||
publishOptions,
|
||||
);
|
||||
}
|
||||
return voiceEngineV2AppScreenShareExecutionAdapter.replaceActiveNativeDeviceScreenShare(
|
||||
await this.getNativeDeviceScreenShareCaptureOptions(options),
|
||||
publishOptions,
|
||||
|
||||
@@ -55,7 +55,10 @@ let nativeSupportPromise: Promise<boolean> | null = null;
|
||||
let nativePrewarmPromise: Promise<void> | null = null;
|
||||
let nativeCapabilitiesSnapshot: VoiceEngineV2BridgeCapabilities | null = null;
|
||||
|
||||
const NATIVE_VOICE_ENGINE_FORCE_DISABLED: boolean = true;
|
||||
|
||||
function isNativeVoiceEngineRequired(): boolean {
|
||||
if (NATIVE_VOICE_ENGINE_FORCE_DISABLED) return false;
|
||||
return isElectronPlatform();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const nativeVoiceEngineSelected = vi.fn<() => boolean>();
|
||||
const electronApi = vi.fn<() => unknown>();
|
||||
|
||||
vi.mock('@app/features/voice/engine/native_voice_engine/NativeVoiceEngineSelection', () => ({
|
||||
isNativeVoiceEngineSelected: () => nativeVoiceEngineSelected(),
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/ui/utils/NativeUtils', () => ({
|
||||
getElectronAPI: () => electronApi(),
|
||||
}));
|
||||
|
||||
const {
|
||||
isVoiceEngineV2AppNativeAudioDeviceBridgeAvailable,
|
||||
isVoiceEngineV2AppNativeScreenShareAudioBridgeAvailable,
|
||||
isVoiceEngineV2AppNativeScreenShareBridgeAvailable,
|
||||
isVoiceEngineV2AppNativeScreenShareEncodingUpdateAvailable,
|
||||
} = await import('./VoiceEngineV2AppNativeBridge');
|
||||
|
||||
const FULLY_CAPABLE_BRIDGE = {
|
||||
getCapabilities: async () => ({
|
||||
screenShare: true,
|
||||
screenShareAudio: true,
|
||||
screenShareEncodingUpdate: true,
|
||||
}),
|
||||
listAudioInputDevices: async () => [],
|
||||
listAudioOutputDevices: async () => [],
|
||||
};
|
||||
|
||||
describe('VoiceEngineV2AppNativeBridge availability', () => {
|
||||
beforeEach(() => {
|
||||
electronApi.mockReturnValue({voiceEngine: FULLY_CAPABLE_BRIDGE});
|
||||
});
|
||||
|
||||
it('reports native capabilities when the native voice engine is selected', async () => {
|
||||
nativeVoiceEngineSelected.mockReturnValue(true);
|
||||
expect(await isVoiceEngineV2AppNativeScreenShareBridgeAvailable()).toBe(true);
|
||||
expect(await isVoiceEngineV2AppNativeScreenShareEncodingUpdateAvailable()).toBe(true);
|
||||
expect(await isVoiceEngineV2AppNativeScreenShareAudioBridgeAvailable()).toBe(true);
|
||||
expect(isVoiceEngineV2AppNativeAudioDeviceBridgeAvailable()).toBe(true);
|
||||
});
|
||||
|
||||
it('reports nothing available when the native voice engine is not selected, even on a fully capable bridge', async () => {
|
||||
nativeVoiceEngineSelected.mockReturnValue(false);
|
||||
expect(await isVoiceEngineV2AppNativeScreenShareBridgeAvailable()).toBe(false);
|
||||
expect(await isVoiceEngineV2AppNativeScreenShareEncodingUpdateAvailable()).toBe(false);
|
||||
expect(await isVoiceEngineV2AppNativeScreenShareAudioBridgeAvailable()).toBe(false);
|
||||
expect(isVoiceEngineV2AppNativeAudioDeviceBridgeAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not query the bridge at all when the native voice engine is not selected', async () => {
|
||||
nativeVoiceEngineSelected.mockReturnValue(false);
|
||||
const getCapabilities = vi.fn(async () => ({screenShare: true}));
|
||||
electronApi.mockReturnValue({voiceEngine: {...FULLY_CAPABLE_BRIDGE, getCapabilities}});
|
||||
await isVoiceEngineV2AppNativeScreenShareBridgeAvailable();
|
||||
expect(getCapabilities).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {getElectronAPI} from '@app/features/ui/utils/NativeUtils';
|
||||
import {isNativeVoiceEngineSelected} from '@app/features/voice/engine/native_voice_engine/NativeVoiceEngineSelection';
|
||||
import type {VoiceEngineV2BridgeApi, VoiceEngineV2BridgeCapabilities} from '@fluxer/voice_engine_v2/bridge';
|
||||
|
||||
function buildMissingBridgeError(context: string): Error {
|
||||
@@ -26,21 +27,25 @@ export async function getVoiceEngineV2AppNativeBridgeCapabilities(): Promise<Voi
|
||||
}
|
||||
|
||||
export async function isVoiceEngineV2AppNativeScreenShareBridgeAvailable(): Promise<boolean> {
|
||||
if (!isNativeVoiceEngineSelected()) return false;
|
||||
const capabilities = await getVoiceEngineV2AppNativeBridgeCapabilities();
|
||||
return capabilities?.screenShare === true;
|
||||
}
|
||||
|
||||
export async function isVoiceEngineV2AppNativeScreenShareEncodingUpdateAvailable(): Promise<boolean> {
|
||||
if (!isNativeVoiceEngineSelected()) return false;
|
||||
const capabilities = await getVoiceEngineV2AppNativeBridgeCapabilities();
|
||||
return capabilities?.screenShareEncodingUpdate === true;
|
||||
}
|
||||
|
||||
export async function isVoiceEngineV2AppNativeScreenShareAudioBridgeAvailable(): Promise<boolean> {
|
||||
if (!isNativeVoiceEngineSelected()) return false;
|
||||
const capabilities = await getVoiceEngineV2AppNativeBridgeCapabilities();
|
||||
return capabilities?.screenShareAudio === true;
|
||||
}
|
||||
|
||||
export function isVoiceEngineV2AppNativeAudioDeviceBridgeAvailable(): boolean {
|
||||
if (!isNativeVoiceEngineSelected()) return false;
|
||||
const bridge = getVoiceEngineV2AppNativeBridge();
|
||||
if (!bridge) return false;
|
||||
return typeof bridge.listAudioInputDevices === 'function' && typeof bridge.listAudioOutputDevices === 'function';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions';
|
||||
import {getElectronAPI} from '@app/features/ui/utils/NativeUtils';
|
||||
import {isNativeVoiceEngineSelected} from '@app/features/voice/engine/native_voice_engine/NativeVoiceEngineSelection';
|
||||
import {
|
||||
markScreenShareCaptureActive,
|
||||
markScreenShareCaptureEnded,
|
||||
@@ -199,6 +200,7 @@ export interface NativeEngineScreenCapture {
|
||||
}
|
||||
|
||||
export async function isNativeScreenCaptureAvailable(): Promise<boolean> {
|
||||
if (!isNativeVoiceEngineSelected()) return false;
|
||||
const electronApi = getElectronAPI();
|
||||
const api = electronApi?.nativeScreenCapture ?? getNativeScreenCaptureApi();
|
||||
if (!api) return false;
|
||||
|
||||
@@ -765,6 +765,7 @@ class VoiceSettings {
|
||||
}
|
||||
|
||||
getBackgroundImageId(): string {
|
||||
if (!areVoiceBackgroundsAvailable()) return NONE_BACKGROUND_ID;
|
||||
return this.backgroundImageId;
|
||||
}
|
||||
|
||||
@@ -1119,10 +1120,6 @@ class VoiceSettings {
|
||||
backgroundImages = backgroundImages.slice(0, 3);
|
||||
}
|
||||
}
|
||||
if (!areVoiceBackgroundsAvailable()) {
|
||||
backgroundImages = [];
|
||||
backgroundImageId = NONE_BACKGROUND_ID;
|
||||
}
|
||||
if (backgroundImageId !== NONE_BACKGROUND_ID && backgroundImageId !== BLUR_BACKGROUND_ID) {
|
||||
const imageExists = backgroundImages.some((img: BackgroundImage) => img.id === backgroundImageId);
|
||||
if (!imageExists) {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// @vitest-environment happy-dom
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const voiceBackgroundsAvailable = vi.fn<() => boolean>();
|
||||
|
||||
vi.mock('@app/features/voice/utils/VoiceBackgroundAvailability', () => ({
|
||||
areVoiceBackgroundsAvailable: () => voiceBackgroundsAvailable(),
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/app/utils/LimitResolverAdapter', () => ({
|
||||
LimitResolver: {resolve: () => 0},
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/app/utils/LimitUtils', () => ({
|
||||
isLimitToggleEnabled: () => false,
|
||||
}));
|
||||
|
||||
const {default: VoiceSettings, NONE_BACKGROUND_ID} = await import('./VoiceSettings');
|
||||
|
||||
const SAVED_BACKGROUND = {id: 'saved-background', createdAt: 1, mediaKind: 'static' as const};
|
||||
|
||||
describe('VoiceSettings background persistence when voice backgrounds are unavailable', () => {
|
||||
beforeEach(() => {
|
||||
voiceBackgroundsAvailable.mockReturnValue(true);
|
||||
VoiceSettings.updateSettings({backgroundImages: [SAVED_BACKGROUND], backgroundImageId: SAVED_BACKGROUND.id});
|
||||
voiceBackgroundsAvailable.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('keeps the uploaded background list across a no-op revalidation', () => {
|
||||
VoiceSettings.updateSettings({});
|
||||
expect(VoiceSettings.backgroundImages).toEqual([SAVED_BACKGROUND]);
|
||||
});
|
||||
|
||||
it('keeps the stored selection so it comes back once backgrounds are available again', () => {
|
||||
VoiceSettings.updateSettings({});
|
||||
expect(VoiceSettings.backgroundImageId).toBe(SAVED_BACKGROUND.id);
|
||||
voiceBackgroundsAvailable.mockReturnValue(true);
|
||||
expect(VoiceSettings.getBackgroundImageId()).toBe(SAVED_BACKGROUND.id);
|
||||
});
|
||||
|
||||
it('never hands out an active background while the feature is unavailable', () => {
|
||||
VoiceSettings.updateSettings({});
|
||||
expect(VoiceSettings.getBackgroundImageId()).toBe(NONE_BACKGROUND_ID);
|
||||
expect(VoiceSettings.getBackgroundImages()).toEqual([SAVED_BACKGROUND]);
|
||||
});
|
||||
|
||||
it('still drops a selection that no longer matches a stored background', () => {
|
||||
VoiceSettings.updateSettings({backgroundImages: []});
|
||||
expect(VoiceSettings.backgroundImageId).toBe(NONE_BACKGROUND_ID);
|
||||
});
|
||||
});
|
||||
Vendored
-4
@@ -4,8 +4,6 @@ import type {VoiceEngineV2BridgeApi} from '@fluxer/voice_engine_v2/bridge';
|
||||
import type {AuthenticationResponseJSON, RegistrationResponseJSON} from '@simplewebauthn/browser';
|
||||
|
||||
export type InputMonitoringPermissionStatus = 'granted' | 'denied' | 'not-determined' | 'unsupported';
|
||||
export type DesktopBuildVariant = 'default' | 'windows-game-capture';
|
||||
|
||||
export interface DesktopSource {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -20,7 +18,6 @@ export interface DesktopSource {
|
||||
export interface DesktopInfo {
|
||||
version: string;
|
||||
channel: 'stable' | 'canary';
|
||||
buildVariant: DesktopBuildVariant;
|
||||
arch: string;
|
||||
hardwareArch: string;
|
||||
runningUnderRosetta: boolean;
|
||||
@@ -352,7 +349,6 @@ export interface AppMetricsSnapshot {
|
||||
export interface ElectronAPI {
|
||||
platform: 'darwin' | 'win32' | 'linux' | string;
|
||||
buildChannel: 'stable' | 'canary';
|
||||
buildVariant: DesktopBuildVariant;
|
||||
openExternal(url: string): Promise<void>;
|
||||
downloadFile(url: string, suggestedName: string): Promise<DownloadResult>;
|
||||
onUpdaterEvent(callback: (event: UpdaterEvent) => void): () => void;
|
||||
|
||||
Reference in new issue
Block a user