Skip to content

Commit 47def3b

Browse files
authored
Optimize side nav, live dock, and chat. (#5654)
* Remove resize observer from side nav. * Live dock uses sider and is resizeable. * Memoizations in Chat component. * Remove window resize event handler from Chat component. * Add check for ui ready for side nav. * Add check for ui ready for side nav. * Title bar remove dependency and specify cleanup by window type. * Add cleanup to Go Live window. * Memoization in nav tools and auth modal, and fix auth modal hidden on main window. * Fix auth modal undefined primary platform error. * Fixes for tests. * Fix for tests waiting for go live window loading and dual output. * Dual Output test polishes.
1 parent 070a52f commit 47def3b

File tree

16 files changed

+383
-347
lines changed

16 files changed

+383
-347
lines changed

app/components-react/root/Chat.tsx

Lines changed: 51 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,43 @@
11
import * as remote from '@electron/remote';
2-
import React, { useEffect, useRef } from 'react';
2+
import React, { useEffect, useRef, useCallback, useMemo } from 'react';
33
import { Services } from '../service-provider';
44
import styles from './Chat.m.less';
55
import { OS, getOS } from '../../util/operating-systems';
66
import { onUnload } from 'util/unload';
7-
import { debounce } from 'lodash';
7+
import { useVuex } from 'components-react/hooks';
88

99
export default function Chat(props: {
1010
restream: boolean;
1111
visibleChat: string;
1212
setChat: (key: string) => void;
1313
}) {
14-
const { ChatService, RestreamService } = Services;
14+
const { ChatService, RestreamService, WindowsService } = Services;
1515

1616
const chatEl = useRef<HTMLDivElement>(null);
1717

18-
let currentPosition: IVec2 | null;
19-
let currentSize: IVec2 | null;
18+
const currentPosition = useRef<IVec2 | null>(null);
19+
const currentSize = useRef<IVec2 | null>(null);
20+
const mountedRef = useRef<boolean>(true);
21+
const service = useMemo(() => (props.restream ? RestreamService : ChatService), [props.restream]);
22+
const windowId = useMemo(() => remote.getCurrentWindow().id, []);
2023

21-
let leaveFullScreenTrigger: Function;
24+
const { hideStyleBlockers } = useVuex(() => ({
25+
hideStyleBlockers: WindowsService.state.main.hideStyleBlockers,
26+
}));
27+
28+
const leaveFullScreenTrigger = useCallback(() => {
29+
setTimeout(() => {
30+
setupChat();
31+
checkResize();
32+
}, 1000);
33+
}, []);
2234

2335
useEffect(() => {
24-
const service = props.restream ? RestreamService : ChatService;
2536
const cancelUnload = onUnload(() => service.actions.unmountChat(remote.getCurrentWindow().id));
2637

27-
window.addEventListener('resize', debounce(checkResize, 100));
28-
2938
// Work around an electron bug on mac where chat is not interactable
3039
// after leaving fullscreen until chat is remounted.
3140
if (getOS() === OS.Mac) {
32-
leaveFullScreenTrigger = () => {
33-
setTimeout(() => {
34-
setupChat();
35-
checkResize();
36-
}, 1000);
37-
};
38-
3941
remote.getCurrentWindow().on('leave-full-screen', leaveFullScreenTrigger);
4042
}
4143

@@ -44,52 +46,55 @@ export default function Chat(props: {
4446
setTimeout(checkResize, 100);
4547

4648
return () => {
47-
window.removeEventListener('resize', debounce(checkResize, 100));
48-
4949
if (getOS() === OS.Mac) {
5050
remote.getCurrentWindow().removeListener('leave-full-screen', leaveFullScreenTrigger);
5151
}
5252

5353
service.actions.unmountChat(remote.getCurrentWindow().id);
5454
cancelUnload();
55-
};
56-
}, [props.restream]);
57-
58-
function setupChat() {
59-
const service = props.restream ? RestreamService : ChatService;
60-
const windowId = remote.getCurrentWindow().id;
61-
62-
ChatService.actions.unmountChat();
63-
RestreamService.actions.unmountChat(windowId);
64-
65-
service.actions.mountChat(windowId);
66-
currentPosition = null;
67-
currentSize = null;
68-
}
6955

70-
function checkResize() {
71-
const service = props.restream ? RestreamService : ChatService;
56+
mountedRef.current = false;
57+
};
58+
}, []);
7259

73-
if (!chatEl.current) return;
60+
const checkResize = useCallback(() => {
61+
if (!chatEl.current || !mountedRef.current) return;
7462

7563
const rect = chatEl.current.getBoundingClientRect();
7664

77-
if (currentPosition == null || currentSize == null || rectChanged(rect)) {
78-
currentPosition = { x: rect.left, y: rect.top };
79-
currentSize = { x: rect.width, y: rect.height };
65+
if (currentPosition.current == null || currentSize == null || rectChanged(rect)) {
66+
currentPosition.current = { x: rect.left, y: rect.top };
67+
currentSize.current = { x: rect.width, y: rect.height };
8068

81-
service.actions.setChatBounds(currentPosition, currentSize);
69+
service.actions.setChatBounds(currentPosition.current, currentSize.current);
8270
}
83-
}
71+
}, [service, hideStyleBlockers]);
8472

85-
function rectChanged(rect: ClientRect) {
73+
const rectChanged = useCallback((rect: DOMRect) => {
74+
if (!currentPosition.current || !currentSize.current) return;
8675
return (
87-
rect.left !== currentPosition?.x ||
88-
rect.top !== currentPosition?.y ||
89-
rect.width !== currentSize?.x ||
90-
rect.height !== currentSize?.y
76+
rect.left !== currentPosition.current?.x ||
77+
rect.top !== currentPosition.current?.y ||
78+
rect.width !== currentSize.current?.x ||
79+
rect.height !== currentSize.current?.y
9180
);
92-
}
81+
}, []);
82+
83+
useEffect(() => {
84+
if (!hideStyleBlockers && mountedRef.current) {
85+
// Small delay to ensure DOM has updated after style blockers removed
86+
setTimeout(() => checkResize(), 50);
87+
}
88+
}, [hideStyleBlockers, checkResize]);
89+
90+
const setupChat = useCallback(() => {
91+
ChatService.actions.unmountChat();
92+
RestreamService.actions.unmountChat(windowId);
93+
94+
service.actions.mountChat(windowId);
95+
currentPosition.current = null;
96+
currentSize.current = null;
97+
}, [service, checkResize]);
9398

9499
return <div className={styles.chat} ref={chatEl} />;
95100
}

app/components-react/shared/AuthModal.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import React, { CSSProperties } from 'react';
1+
import React, { CSSProperties, useMemo } from 'react';
22
import { Button, Form, Modal } from 'antd';
33
import styles from './AuthModal.m.less';
44
import { $t } from 'services/i18n';
55
import { Services } from 'components-react/service-provider';
66
import cx from 'classnames';
7+
import { useVuex } from 'components-react/hooks';
78

89
interface AuthModalProps {
910
showModal: boolean;
10-
prompt: string;
11+
prompt?: string;
1112
handleAuth: () => void;
1213
handleShowModal: (status: boolean) => void;
1314
title?: string;
@@ -19,11 +20,30 @@ interface AuthModalProps {
1920
}
2021

2122
export function AuthModal(p: AuthModalProps) {
22-
const title = p?.title || Services.UserService.isLoggedIn ? $t('Log Out') : $t('Login');
23-
const prompt = p?.prompt;
23+
const { UserService } = Services;
24+
25+
const { isLoggedIn, primaryPlatform, name } = useVuex(() => ({
26+
isLoggedIn: UserService.views.isLoggedIn,
27+
primaryPlatform: UserService.views.auth?.primaryPlatform,
28+
name: UserService.views.username,
29+
}));
30+
31+
const title = p?.title || isLoggedIn ? $t('Log Out') : $t('Login');
2432
const confirm = p?.confirm || $t('Yes');
2533
const cancel = p?.cancel || $t('No');
2634

35+
const prompt = useMemo(() => {
36+
if (p.prompt) return p.prompt;
37+
38+
// Instagram doesn't provide a username, since we're not really linked, pass undefined for a generic logout msg w/o it
39+
const username =
40+
isLoggedIn && primaryPlatform && primaryPlatform !== 'instagram' ? name : undefined;
41+
42+
return username
43+
? $t('Are you sure you want to log out %{username}?', { username })
44+
: $t('Are you sure you want to log out?');
45+
}, [p.prompt, name, isLoggedIn, primaryPlatform]);
46+
2747
return (
2848
<Modal
2949
footer={null}

app/components-react/shared/TitleBar.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useMemo, useState } from 'react';
1+
import React, { useEffect, useMemo, useState, useCallback } from 'react';
22
import cx from 'classnames';
33
import { useVuex } from '../hooks';
44
import { Services } from '../service-provider';
@@ -30,35 +30,47 @@ export default function TitleBar(props: { windowId: string; className?: string }
3030
const primeTheme = /prime/.test(theme);
3131
const [errorState, setErrorState] = useState(false);
3232

33-
useEffect(lifecycle, []);
33+
useEffect(() => {
34+
lifecycle();
3435

35-
function lifecycle() {
36-
if (Utils.isDevMode()) {
36+
return () => {
37+
if (Utils.isDevMode() && Utils.isMainWindow()) {
38+
ipcRenderer.removeAllListeners('unhandledErrorState');
39+
}
40+
41+
if (Utils.isDevMode() && Utils.isChildWindow()) {
42+
ipcRenderer.removeListener('unhandledErrorState', () => setErrorState(true));
43+
}
44+
};
45+
}, []);
46+
47+
const lifecycle = useCallback(() => {
48+
if (Utils.isDevMode() && Utils.isMainWindow()) {
3749
ipcRenderer.on('unhandledErrorState', () => setErrorState(true));
3850
}
39-
}
51+
}, []);
4052

41-
function minimize() {
53+
const minimize = useCallback(() => {
4254
remote.getCurrentWindow().minimize();
43-
}
55+
}, []);
4456

45-
function maximize() {
57+
const maximize = useCallback(() => {
4658
const win = remote.getCurrentWindow();
4759

4860
if (win.isMaximized()) {
4961
win.unmaximize();
5062
} else {
5163
win.maximize();
5264
}
53-
}
65+
}, []);
5466

55-
function close() {
67+
const close = useCallback(() => {
5668
if (Utils.isMainWindow() && StreamingService.isStreaming) {
5769
if (!confirm($t('Are you sure you want to exit while live?'))) return;
5870
}
5971

6072
remote.getCurrentWindow().close();
61-
}
73+
}, []);
6274

6375
return (
6476
<>

app/components-react/sidebar/NavTools.tsx

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo, useState } from 'react';
1+
import React, { useMemo, useState, useCallback } from 'react';
22
import cx from 'classnames';
33
import electron from 'electron';
44
import Utils from 'services/utils';
@@ -63,65 +63,58 @@ export default function SideNav() {
6363
const [dashboardOpening, setDashboardOpening] = useState(false);
6464
const [showModal, setShowModal] = useState(false);
6565

66-
function openSettingsWindow(category?: TCategoryName) {
66+
const openSettingsWindow = useCallback((category?: TCategoryName) => {
6767
SettingsService.actions.showSettings(category);
68-
}
68+
}, []);
6969

70-
function openDevTools() {
70+
const openDevTools = useCallback(() => {
7171
electron.ipcRenderer.send('openDevTools');
72-
}
72+
}, []);
7373

74-
async function openDashboard(page?: string) {
75-
UsageStatisticsService.actions.recordClick('SideNav2', page || 'dashboard');
76-
if (dashboardOpening) return;
77-
setDashboardOpening(true);
74+
const openDashboard = useCallback(
75+
async (page?: string) => {
76+
UsageStatisticsService.actions.recordClick('SideNav2', page || 'dashboard');
77+
if (dashboardOpening) return;
78+
setDashboardOpening(true);
7879

79-
try {
80-
const link = await MagicLinkService.getDashboardMagicLink(page);
81-
remote.shell.openExternal(link);
82-
} catch (e: unknown) {
83-
console.error('Error generating dashboard magic link', e);
84-
}
80+
try {
81+
const link = await MagicLinkService.getDashboardMagicLink(page);
82+
remote.shell.openExternal(link);
83+
} catch (e: unknown) {
84+
console.error('Error generating dashboard magic link', e);
85+
}
8586

86-
setDashboardOpening(false);
87-
}
87+
setDashboardOpening(false);
88+
},
89+
[dashboardOpening],
90+
);
8891

8992
const throttledOpenDashboard = throttle(openDashboard, 2000, { trailing: false });
9093

91-
// Instagram doesn't provide a username, since we're not really linked, pass undefined for a generic logout msg w/o it
92-
const username =
93-
isLoggedIn && UserService.views.auth!.primaryPlatform !== 'instagram'
94-
? UserService.username
95-
: undefined;
96-
97-
const confirmMsg = username
98-
? $t('Are you sure you want to log out %{username}?', { username })
99-
: $t('Are you sure you want to log out?');
100-
101-
function openHelp() {
94+
const openHelp = useCallback(() => {
10295
UsageStatisticsService.actions.recordClick('SideNav2', 'help');
10396
remote.shell.openExternal(UrlService.supportLink);
104-
}
97+
}, []);
10598

106-
async function upgradeToPrime() {
99+
const upgradeToPrime = useCallback(async () => {
107100
UsageStatisticsService.actions.recordClick('SideNav2', 'prime');
108101
MagicLinkService.linkToPrime('slobs-side-nav');
109-
}
102+
}, []);
110103

111-
const handleAuth = () => {
104+
const handleAuth = useCallback(() => {
112105
if (isLoggedIn) {
113106
Services.DualOutputService.actions.setDualOutputModeIfPossible(false, true);
114107
UserService.actions.logOut();
115108
} else {
116109
WindowsService.actions.closeChildWindow();
117110
UserService.actions.showLogin();
118111
}
119-
};
112+
}, [isLoggedIn]);
120113

121-
const handleShowModal = (status: boolean) => {
122-
updateStyleBlockers('main', status);
114+
const handleShowModal = useCallback((status: boolean) => {
123115
setShowModal(status);
124-
};
116+
updateStyleBlockers('main', status);
117+
}, []);
125118

126119
return (
127120
<>
@@ -222,7 +215,6 @@ export default function SideNav() {
222215
</Menu>
223216
<AuthModal
224217
title={$t('Confirm')}
225-
prompt={confirmMsg}
226218
showModal={showModal}
227219
handleAuth={handleAuth}
228220
handleShowModal={handleShowModal}

0 commit comments

Comments
 (0)