fix(app): correct shortcuts, nagbar, stream menu, share audio (#2599)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-08 19:09:59 +02:00
1 parent 55846b24ea
commit fc2b6b5299
37 files changed
+1074 -268

No files matched your search

@@ -25,7 +25,6 @@ import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import * as PopoutCommands from '@app/features/ui/commands/PopoutCommands';
import * as TextCopyCommands from '@app/features/ui/commands/TextCopyCommands';
import * as ToastCommands from '@app/features/ui/commands/ToastCommands';
import {MockAvatar} from '@app/features/ui/components/MockAvatar';
import {StatusIndicator} from '@app/features/ui/components/StatusIndicator';
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
@@ -52,9 +51,8 @@ import Users from '@app/features/user/state/Users';
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
import * as ProfileDisplayUtils from '@app/features/user/utils/ProfileDisplayUtils';
import {createMockProfile} from '@app/features/user/utils/ProfileUtils';
import {COPIED_STATS_JSON_DESCRIPTOR} from '@app/features/voice/components/StatsForNerdsCopyDescriptors';
import {copyVoiceDiagnostics} from '@app/features/voice/commands/VoiceDiagnosticsCommands';
import MediaEngine from '@app/features/voice/engine/MediaEngineFacade';
import {buildStatsForNerdsCopyPayload, collectStatsForNerdsSnapshot} from '@app/features/voice/utils/StatsForNerdsCopy';
import {MEDIA_PROXY_PROFILE_BANNER_SIZE_POPOUT} from '@fluxer/constants/src/MediaProxyAssetSizes';
import {StatusTypes} from '@fluxer/constants/src/StatusConstants';
import type {MessageDescriptor} from '@lingui/core';
@@ -462,12 +460,7 @@ export const UserAreaPopout = observer(() => {
});
}, [i18n]);
const handleCopyStats = useCallback(() => {
const data = collectStatsForNerdsSnapshot();
void buildStatsForNerdsCopyPayload(data).then((payload) => {
void navigator.clipboard.writeText(JSON.stringify(payload, null, 2)).then(() => {
ToastCommands.createToast({type: 'success', children: i18n._(COPIED_STATS_JSON_DESCRIPTOR)});
});
});
void copyVoiceDiagnostics(i18n);
}, [i18n]);
const handleCopyUserTag = useCallback(() => {
if (!currentUser) {
@@ -1277,6 +1277,7 @@ const SKELETON_NAGBAR_ROW_SHAPES: Record<NagbarType, SkeletonNagbarRowShape> = {
[NagbarType.PREMIUM_ONBOARDING]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
[NagbarType.GIFT_INVENTORY]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
[NagbarType.DESKTOP_DOWNLOAD]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
[NagbarType.DESKTOP_UPDATE_READY]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
[NagbarType.GUILD_MEMBERSHIP_CTA]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
[NagbarType.VISIONARY_MFA]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
[NagbarType.VOICE_SESSION_RESTORE]: {tone: SkeletonNagbarTone.VOICE, hasActions: true},
@@ -11,6 +11,7 @@ import Config from '@app/features/app/config/Config';
import {isClientReconnecting} from '@app/features/app/state/ClientReadiness';
import Initialization from '@app/features/app/state/Initialization';
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import Updater from '@app/features/app/state/Updater';
import Authentication from '@app/features/auth/state/Authentication';
import Channels from '@app/features/channel/state/Channels';
import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions';
@@ -230,6 +231,7 @@ export const useNagbarConditions = (): NagbarConditions => {
const canShowLinuxInputAccess = NativePermission.shouldShowLinuxInputAccessNagbar;
const canShowSoftwareEncoder = SoftwareEncoderWarning.showWarning;
const canShowStreamerMode = StreamerMode.shouldShowNagbar;
const canShowDesktopUpdateReady = Updater.shouldShowUpdateReadyNagbar;
const canShowBuildEnvironment =
!BUILD_ENVIRONMENT_HIDDEN_RELEASE_CHANNELS.has(Config.PUBLIC_RELEASE_CHANNEL) &&
!nagbarState.buildEnvironmentDismissedThisSession;
@@ -288,6 +290,7 @@ export const useNagbarConditions = (): NagbarConditions => {
canShowLinuxInputAccess,
canShowSoftwareEncoder,
canShowStreamerMode,
canShowDesktopUpdateReady,
};
};
export const useActiveNagbars = (conditions: NagbarConditions): Array<NagbarState> => {
@@ -407,6 +410,12 @@ export const useActiveNagbars = (conditions: NagbarConditions): Array<NagbarStat
visible: conditions.canShowStreamerMode,
dismissible: true,
},
{
type: NagbarType.DESKTOP_UPDATE_READY,
priority: -1.5,
visible: conditions.canShowDesktopUpdateReady,
dismissible: true,
},
];
return selectVisibleNagbars(nagbars);
}, [conditions]);
@@ -15,6 +15,7 @@ export const NagbarType = {
PREMIUM_ONBOARDING: 'premium-onboarding',
GIFT_INVENTORY: 'gift-inventory',
DESKTOP_DOWNLOAD: 'desktop-download',
DESKTOP_UPDATE_READY: 'desktop-update-ready',
GUILD_MEMBERSHIP_CTA: 'guild-membership-cta',
VISIONARY_MFA: 'visionary-mfa',
VOICE_SESSION_RESTORE: 'voice-session-restore',
@@ -50,6 +51,7 @@ export interface NagbarConditions {
canShowPremiumOnboarding: boolean;
canShowGiftInventory: boolean;
canShowDesktopDownload: boolean;
canShowDesktopUpdateReady: boolean;
canShowGuildMembershipCta: boolean;
canShowVisionaryMfa: boolean;
canShowVoiceSessionRestore: boolean;
@@ -7,6 +7,7 @@ import {ConnectionNagbar} from '@app/features/app/components/layout/app_layout/n
import {CorruptedInstallationNagbar} from '@app/features/app/components/layout/app_layout/nagbars/CorruptedInstallationNagbar';
import {DesktopDownloadNagbar} from '@app/features/app/components/layout/app_layout/nagbars/DesktopDownloadNagbar';
import {DesktopNotificationNagbar} from '@app/features/app/components/layout/app_layout/nagbars/DesktopNotificationNagbar';
import {DesktopUpdateReadyNagbar} from '@app/features/app/components/layout/app_layout/nagbars/DesktopUpdateReadyNagbar';
import {EmailVerificationNagbar} from '@app/features/app/components/layout/app_layout/nagbars/EmailVerificationNagbar';
import {GiftInventoryNagbar} from '@app/features/app/components/layout/app_layout/nagbars/GiftInventoryNagbar';
import {GuildMembershipCtaNagbar} from '@app/features/app/components/layout/app_layout/nagbars/GuildMembershipCtaNagbar';
@@ -153,6 +154,14 @@ export const NagbarContainer: React.FC<NagbarContainerProps> = observer(({nagbar
data-flx="app.app-layout.nagbar-container.desktop-download-nagbar"
/>
);
case NagbarType.DESKTOP_UPDATE_READY:
return (
<DesktopUpdateReadyNagbar
key={nagbar.type}
isMobile={mobileLayout.enabled}
data-flx="app.app-layout.nagbar-container.desktop-update-ready-nagbar"
/>
);
case NagbarType.GUILD_MEMBERSHIP_CTA:
return (
<GuildMembershipCtaNagbar
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Nagbar} from '@app/features/app/components/layout/Nagbar';
import {NagbarButton} from '@app/features/app/components/layout/NagbarButton';
import {NagbarContent} from '@app/features/app/components/layout/NagbarContent';
import {NAGBAR_TONES, NagbarToneKind} from '@app/features/app/components/layout/NagbarTones';
import {PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
import Updater from '@app/features/app/state/Updater';
import {
DESKTOP_VERSION_HAS_BEEN_DOWNLOADED_DESCRIPTOR,
RESTART_FLUXER_DESCRIPTOR,
THE_DESKTOP_UPDATE_HAS_BEEN_DOWNLOADED_DESCRIPTOR,
} from '@app/features/updater/commands/UpdaterModalCommands';
import {useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
export const DesktopUpdateReadyNagbar = observer(({isMobile}: {isMobile: boolean}) => {
const {i18n} = useLingui();
const version = Updater.updateInfo.native.version;
return (
<Nagbar
isMobile={isMobile}
backgroundColor={NAGBAR_TONES[NagbarToneKind.BRAND].backgroundColor}
textColor={NAGBAR_TONES[NagbarToneKind.BRAND].textColor}
dismissible
onDismiss={Updater.dismissUpdateReadyNagbar}
data-flx="app.app-layout.nagbars.desktop-update-ready-nagbar.nagbar"
>
<NagbarContent
isMobile={isMobile}
onDismiss={Updater.dismissUpdateReadyNagbar}
message={
version
? i18n._(DESKTOP_VERSION_HAS_BEEN_DOWNLOADED_DESCRIPTOR, {version, productName: PRODUCT_NAME})
: i18n._(THE_DESKTOP_UPDATE_HAS_BEEN_DOWNLOADED_DESCRIPTOR, {productName: PRODUCT_NAME})
}
actions={
<NagbarButton
isMobile={isMobile}
onClick={() => void Updater.applyUpdate()}
data-flx="app.app-layout.nagbars.desktop-update-ready-nagbar.restart-button"
>
{i18n._(RESTART_FLUXER_DESCRIPTOR, {productName: PRODUCT_NAME})}
</NagbarButton>
}
data-flx="app.app-layout.nagbars.desktop-update-ready-nagbar.nagbar-content"
/>
</Nagbar>
);
});
@@ -0,0 +1,112 @@
// @vitest-environment happy-dom
// SPDX-License-Identifier: AGPL-3.0-or-later
import {installVoiceMenuTestBootstrap} from '@app/features/ui/action_menu/items/__fixtures__/VoiceMenuTestBootstrap';
import type {UpdaterEvent} from '@app/types/electron.d';
import {afterEach, describe, expect, test, vi} from 'vitest';
const {pushUpdateReadyModal} = vi.hoisted(() => ({pushUpdateReadyModal: vi.fn()}));
vi.mock('@app/features/updater/commands/UpdaterModalCommands', () => ({
pushDesktopUpdateDownloadFailedModal: vi.fn(),
pushDesktopUpdateInstallFailedModal: vi.fn(),
pushManualUpdateAvailableModal: vi.fn(),
pushUnsupportedUpdateModal: vi.fn(),
pushUpdateAvailableModal: vi.fn(),
pushUpdateCheckFailedModal: vi.fn(),
pushUpdateReadyModal,
pushUpToDateModal: vi.fn(),
}));
vi.mock('@lingui/core/macro', () => ({
msg: (descriptor: {message: string}) => descriptor,
}));
vi.mock('@app/features/platform/utils/ClientInfo', () => ({
getClientInfo: () =>
Promise.resolve({desktopVersion: '1.0.0', desktopChannel: 'canary', desktopArch: 'x64', arch: 'x64'}),
}));
installVoiceMenuTestBootstrap();
let nativeEventListener: ((event: UpdaterEvent) => void) | null = null;
let onUpdaterCheck: (() => void) | null = null;
let loadedUpdater: {dispose: () => void} | null = null;
function installElectronApi(): void {
nativeEventListener = null;
onUpdaterCheck = null;
(window as unknown as {electron: unknown}).electron = {
platform: 'win32',
buildChannel: 'canary',
onUpdaterEvent: (listener: (event: UpdaterEvent) => void) => {
nativeEventListener = listener;
return () => {
nativeEventListener = null;
};
},
updaterCheck: () => {
onUpdaterCheck?.();
return Promise.resolve();
},
updaterDownload: () => Promise.resolve(),
updaterInstall: () => Promise.resolve(),
};
}
function emit(event: UpdaterEvent): void {
if (!nativeEventListener) throw new Error('Updater never subscribed to native updater events');
nativeEventListener(event);
}
async function loadUpdater() {
vi.resetModules();
installElectronApi();
const {default: Updater} = await import('@app/features/app/state/Updater');
loadedUpdater = Updater;
await vi.waitFor(() => {
expect(nativeEventListener).not.toBeNull();
expect(Updater.lastCheckedAt).not.toBeNull();
});
pushUpdateReadyModal.mockClear();
return Updater;
}
function emitUserDownloadCompletion(version: string): void {
emit({type: 'available', context: 'user', version, downloadSize: 1000, downloadStarted: true});
emit({type: 'downloaded', context: 'user', version});
}
afterEach(() => {
loadedUpdater?.dispose();
loadedUpdater = null;
});
describe('updater update-ready surface', () => {
test('does not push a blocking modal when a user-initiated download finishes outside a check', async () => {
const Updater = await loadUpdater();
emitUserDownloadCompletion('2.0.0');
expect(Updater.nativeUpdateReady).toBe(true);
expect(pushUpdateReadyModal).not.toHaveBeenCalled();
});
test('announces the ready update through a dismissible nagbar instead', async () => {
const Updater = await loadUpdater();
expect(Updater.shouldShowUpdateReadyNagbar).toBe(false);
emitUserDownloadCompletion('2.0.0');
expect(Updater.shouldShowUpdateReadyNagbar).toBe(true);
Updater.dismissUpdateReadyNagbar();
expect(Updater.shouldShowUpdateReadyNagbar).toBe(false);
emit({type: 'downloaded', context: 'background', version: '2.1.0'});
expect(Updater.shouldShowUpdateReadyNagbar).toBe(true);
});
test('still answers a user-initiated check with the update ready modal', async () => {
const Updater = await loadUpdater();
emitUserDownloadCompletion('2.0.0');
pushUpdateReadyModal.mockClear();
onUpdaterCheck = () => emit({type: 'available', context: 'user', version: '2.0.0', downloadStarted: false});
await Updater.checkForUpdates(true, true);
expect(pushUpdateReadyModal).toHaveBeenCalledTimes(1);
});
});
+17 -3
View File
@@ -131,6 +131,7 @@ class Updater {
private backgroundCheckInterval: number | null = null;
private backgroundCheckCleanups: Array<() => void> = [];
private unsubscribeNativeEvents: (() => void) | null = null;
private updateReadyNagbarDismissedVersion: string | null = null;
constructor() {
makeAutoObservable(this, {}, {autoBind: true});
@@ -193,6 +194,18 @@ class Updater {
return this.updateInfo.native.available && this.updateInfo.native.downloaded;
}
get shouldShowUpdateReadyNagbar(): boolean {
return (
this.nativeUpdateReady &&
!this.updateInfo.native.installing &&
this.updateReadyNagbarDismissedVersion !== this.updateReadyNagbarVersionKey
);
}
private get updateReadyNagbarVersionKey(): string {
return this.updateInfo.native.version ?? 'unknown';
}
get nativeDownloadInFlight(): boolean {
return this.updateInfo.native.downloading && !this.updateInfo.native.downloaded;
}
@@ -348,9 +361,6 @@ class Updater {
break;
}
this.transition({type: 'native.downloaded', version: event.version ?? null});
if (shouldShowImmediateUserResult) {
this.showCurrentUpdateState();
}
break;
case 'progress':
if (!shouldSurfaceNativeDesktopUpdate || !this.nativeDownloadProgressSupported) {
@@ -661,6 +671,10 @@ class Updater {
}
}
dismissUpdateReadyNagbar(): void {
this.updateReadyNagbarDismissedVersion = this.updateReadyNagbarVersionKey;
}
reset(): void {
this.transition({type: 'reset'});
}