p.handleAuth()}>{$t('Yes')}
p.handleShowModal(false)}>{$t('No')}
@@ -307,21 +317,7 @@ function LoginMenuItem(p: {
{!isLoggedIn ? (
{menuTitles(menuItem.key)}
) : (
- isOpen && (
- <>
- {platform && (
-
- )}
-
{platform?.username || $t('Log Out')}
-
- >
- )
+ isOpen &&
)}
);
diff --git a/app/components-react/sidebar/PlatformIndicator.m.less b/app/components-react/sidebar/PlatformIndicator.m.less
new file mode 100644
index 000000000000..642eb8c60032
--- /dev/null
+++ b/app/components-react/sidebar/PlatformIndicator.m.less
@@ -0,0 +1,75 @@
+// TODO: these are duplicated from NavTools.m.less, consider refactoring out if not needed there
+.platform-logo {
+ margin-right: 10px;
+
+ &-twitch {
+ color: var(--twitch) !important;
+ }
+ &-youtube {
+ color: var(--youtube) !important;
+ }
+ &-facebook {
+ color: var(--facebook) !important;
+ }
+ &-trovo, &-twitter, &-tiktok, &-instagram {
+ width: 15px;
+ height: 15px;
+ }
+ &-tiktok {
+ color: var(--tiktok) !important;
+ }
+ &-streamlabs {
+ color: var(--teal) !important;
+ }
+ &-default {
+ color: var(--logged-in) !important;
+ }
+}
+
+.username {
+ margin-bottom: 0px;
+ flex-grow: 1;
+ line-height: 20px;
+ margin: 2px 0 0 0;
+}
+
+.login-arrow {
+ justify-self: flex-end;
+ margin-left: 5px;
+ transform: scaleX(-1);
+ -moz-transform: scaleX(-1);
+ -webkit-transform: scaleX(-1);
+ -ms-transform: scaleX(-1);
+ transition: all 0.2s ease-in-out;
+}
+
+.platform-icons {
+ display: flex;
+ flex-direction: row;
+ gap: 1px;
+ align-items: center;
+ justify-content: center;
+
+ // Override the zooming effect applied to sidebar icons
+ i {
+ &,
+ &:hover {
+ transform: none !important;
+ transition: none !important;
+ }
+ }
+
+ // Custom destination icon
+ :global(.fa-globe) {
+ margin-right: 10px;
+ }
+
+ :global(.fa-globe) {
+ &,
+ &:hover,
+ &:focus,
+ &:active {
+ color: var(--icon) !important;
+ }
+ }
+}
diff --git a/app/components-react/sidebar/PlatformIndicator.tsx b/app/components-react/sidebar/PlatformIndicator.tsx
new file mode 100644
index 000000000000..d44cedfa22f6
--- /dev/null
+++ b/app/components-react/sidebar/PlatformIndicator.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import cx from 'classnames';
+import { IPlatformAuth, TPlatform } from 'services/platforms';
+import { $t } from 'services/i18n';
+import { useVuex } from 'components-react/hooks';
+import { Services } from 'components-react/service-provider';
+import PlatformLogo from 'components-react/shared/PlatformLogo';
+import { IPlatformFlags } from 'services/streaming';
+import styles from './PlatformIndicator.m.less';
+
+interface IPlatformIndicatorProps {
+ platform: IPlatformAuth | undefined;
+}
+
+interface IMultiPlatformIndicatorProps {
+ hasCustomDestinations: boolean;
+ enabledPlatforms: [TPlatform, IPlatformFlags][];
+}
+
+export default function PlatformIndicator({ platform }: IPlatformIndicatorProps) {
+ const { StreamSettingsService, RestreamService } = Services;
+ const restreamEnabled = RestreamService.views.canEnableRestream;
+ const { platforms, customDestinations } = useVuex(() => ({
+ platforms: StreamSettingsService.views.settings.goLiveSettings?.platforms,
+ customDestinations: StreamSettingsService.views.settings.goLiveSettings?.customDestinations,
+ }));
+
+ const enabledPlatformsTuple: [TPlatform, IPlatformFlags][] = platforms
+ ? (Object.entries(platforms).filter(([_, p]) => p.enabled) as [TPlatform, IPlatformFlags][])
+ : [];
+
+ const hasMultiplePlatforms = enabledPlatformsTuple.length > 1;
+ const hasCustomDestinations = customDestinations?.some(d => d.enabled) || false;
+
+ if (hasMultiplePlatforms || hasCustomDestinations) {
+ return (
+
+ );
+ }
+
+ // TODO: do we need to check for protected mode
+ return
;
+}
+
+const SinglePlatformIndicator = ({ platform }: Pick
) => {
+ const username = platform?.type === 'instagram' ? undefined : platform?.username;
+
+ return (
+ <>
+ {platform && (
+
+ )}
+ {username || $t('Log Out')}
+
+ >
+ );
+};
+
+const MultiPlatformIndicator = ({
+ hasCustomDestinations,
+ enabledPlatforms,
+}: IMultiPlatformIndicatorProps) => {
+ const displayedDestinations = (hasCustomDestinations ? 1 : 0) + enabledPlatforms.length;
+ // I found that 6 is the max we should be displaying without wrapping, logged in text hidden at 4
+ const platformsToDisplay = enabledPlatforms.slice(0, 6 - (hasCustomDestinations ? 1 : 0));
+
+ return (
+
+
+ {platformsToDisplay.map(([platform, _]) => (
+
+ ))}
+ {hasCustomDestinations &&
}
+
+ {displayedDestinations < 4 && (
+
+ {$t('Logged In')}
+
+ )}
+
+
+ );
+};
diff --git a/app/components-react/windows/go-live/DestinationSwitchers.tsx b/app/components-react/windows/go-live/DestinationSwitchers.tsx
index 35fed1173c05..e7e8308c2618 100644
--- a/app/components-react/windows/go-live/DestinationSwitchers.tsx
+++ b/app/components-react/windows/go-live/DestinationSwitchers.tsx
@@ -25,6 +25,7 @@ export function DestinationSwitchers(p: { showSelector?: boolean }) {
switchCustomDestination,
isPrimaryPlatform,
isPlatformLinked,
+ isRestreamEnabled,
} = useGoLiveSettings();
// use these references to apply debounce
// for error handling and switch animation
@@ -60,8 +61,42 @@ export function DestinationSwitchers(p: { showSelector?: boolean }) {
}
function togglePlatform(platform: TPlatform, enabled: boolean) {
- enabledPlatformsRef.current = enabledPlatformsRef.current.filter(p => p !== platform);
- if (enabled) enabledPlatformsRef.current.push(platform);
+ // On non multistream mode, switch the platform that was just selected while disabling all the others,
+ // allow TikTok to be added as an extra platform
+ if (!isRestreamEnabled) {
+ /*
+ * If TikTok is the platform being toggled:
+ * - Preserve the currently active platform so TikTok can be added to this list at the bottom of this function,
+ * we will have 2 active platforms and a Primary Chat switcher.
+ * - Remove TikTok from the list without removing the other active platform if we're disabling TikTok itself.
+ */
+ if (platform === 'tiktok') {
+ enabledPlatformsRef.current = enabled
+ ? enabledPlatformsRef.current
+ : enabledPlatformsRef.current.filter(platform => platform !== 'tiktok');
+ } else {
+ /*
+ * Clearing this list ensures that when a new platform is selected, instead of enabling 2 platforms
+ * we switch to 1 enabled platforms that was just toggled.
+ * We will also preserve TikTok as an active platform if it was before.
+ */
+ enabledPlatformsRef.current = enabledPlatformsRef.current.includes('tiktok')
+ ? ['tiktok']
+ : [];
+ }
+ } else {
+ enabledPlatformsRef.current = enabledPlatformsRef.current.filter(p => p !== platform);
+ }
+
+ if (enabled) {
+ enabledPlatformsRef.current.push(platform);
+ }
+
+ // Do not allow disabling the last platform
+ if (!enabledPlatformsRef.current.length) {
+ enabledPlatformsRef.current.push(platform);
+ }
+
emitSwitch();
}
@@ -73,17 +108,20 @@ export function DestinationSwitchers(p: { showSelector?: boolean }) {
emitSwitch(ind, enabled);
}
+ // TODO: find a cleaner way to do this
+ const isPrimary = (platform: TPlatform) =>
+ isPrimaryPlatform(platform) || linkedPlatforms.length === 1;
+
return (
-
+
{linkedPlatforms.map(platform => (
togglePlatform(platform, enabled)}
- isPrimary={isPrimaryPlatform(platform)}
promptConnectTikTok={platform === 'tiktok' && promptConnectTikTok}
- disabled={disableSwitchers && !isEnabled(platform)}
+ isPrimary={isPrimaryPlatform(platform)}
/>
))}
@@ -94,7 +132,6 @@ export function DestinationSwitchers(p: { showSelector?: boolean }) {
onChange={enabled => togglePlatform('tiktok', enabled)}
isPrimary={isPrimaryPlatform('tiktok')}
promptConnectTikTok={promptConnectTikTok}
- disabled={disableSwitchers && !isEnabled('tiktok')}
/>
)}
@@ -102,8 +139,8 @@ export function DestinationSwitchers(p: { showSelector?: boolean }) {
toggleDest(ind, enabled)}
+ enabled={customDestinations[ind].enabled}
+ onChange={enabled => switchCustomDestination(ind, enabled)}
disabled={disableSwitchers && !isEnabled(ind)}
/>
))}
@@ -130,18 +167,17 @@ const DestinationSwitcher = React.forwardRef<{}, IDestinationSwitcherProps>((p,
const switchInputRef = useRef(null);
const containerRef = useRef(null);
const platform = typeof p.destination === 'string' ? (p.destination as TPlatform) : null;
- const { RestreamService, MagicLinkService, NavigationService, WindowsService } = Services;
+ const { RestreamService, MagicLinkService, StreamingService } = Services;
+ const canEnableRestream = RestreamService.views.canEnableRestream;
+ const cannotDisableDestination = p.isPrimary && !canEnableRestream;
- function onClickHandler(ev: MouseEvent) {
- if (p.isPrimary) {
- alertAsync(
- $t(
- 'You cannot disable the platform you used to sign in to Streamlabs Desktop. Please sign in with a different platform to disable streaming to this destination.',
- ),
- );
- return;
- }
+ // Preserving old TikTok functionality, so they can't enable the toggle if TikTok is not
+ // connected.
+ // TODO: this kind of logic should belong on caller, but ideally we would refactor all this
+ const tiktokDisabled =
+ platform === 'tiktok' && !StreamingService.views.isPlatformLinked('tiktok');
+ function onClickHandler(ev: MouseEvent) {
if (p.promptConnectTikTok) {
alertAsync({
type: 'confirm',
@@ -163,21 +199,25 @@ const DestinationSwitcher = React.forwardRef<{}, IDestinationSwitcherProps>((p,
return;
}
- if (RestreamService.views.canEnableRestream || !p.promptConnectTikTok) {
- const enable = !p.enabled;
- p.onChange(enable);
- // always proxy the click to the SwitchInput
- // so it can play a transition animation
- switchInputRef.current?.click();
- // switch the container class without re-rendering to not stop the animation
- if (enable) {
- containerRef.current?.classList.remove(styles.platformDisabled);
- } else {
- containerRef.current?.classList.add(styles.platformDisabled);
- }
+ const enable = !p.enabled;
+ p.onChange(enable);
+ // always proxy the click to the SwitchInput
+ // so it can play a transition animation
+ switchInputRef.current?.click();
+
+ /*
+ * TODO:
+ * this causes inconsistent state when disabling primary platform
+ * after is being re-enabled. Not sure which animation is referring to.
+ */
+ // switch the container class without re-rendering to not stop the animation
+ /*
+ if (enable) {
+ containerRef.current?.classList.remove(styles.platformDisabled);
} else {
- MagicLinkService.actions.linkToPrime('slobs-multistream');
+ containerRef.current?.classList.add(styles.platformDisabled);
}
+ */
}
function addClass() {
@@ -205,12 +245,6 @@ const DestinationSwitcher = React.forwardRef<{}, IDestinationSwitcherProps>((p,
const platformAuthData = UserService.state.auth?.platforms[platform];
const username = platformAuthData?.username ?? '';
- // Preserving old TikTok functionality, so they can't enable the toggle if TikTok is not
- // connected.
- // TODO: this kind of logic should belong on caller, but ideally we would refactor all this
- const tiktokDisabled =
- platform === 'tiktok' && !StreamingService.views.isPlatformLinked('tiktok');
-
return {
title: $t('Stream to %{platformName}', { platformName: service.displayName }),
description: username,
@@ -222,7 +256,7 @@ const DestinationSwitcher = React.forwardRef<{}, IDestinationSwitcherProps>((p,
inputRef={switchInputRef}
value={p.enabled}
name={platform}
- disabled={p.isPrimary || tiktokDisabled}
+ disabled={tiktokDisabled}
uncontrolled
/>
),
diff --git a/app/components-react/windows/go-live/EditStreamWindow.tsx b/app/components-react/windows/go-live/EditStreamWindow.tsx
index c22bffcebb28..c00481e46b91 100644
--- a/app/components-react/windows/go-live/EditStreamWindow.tsx
+++ b/app/components-react/windows/go-live/EditStreamWindow.tsx
@@ -14,6 +14,7 @@ import PlatformSettings from './PlatformSettings';
import Scrollable from '../../shared/Scrollable';
import Spinner from '../../shared/Spinner';
import GoLiveError from './GoLiveError';
+import PrimaryChatSwitcher from './PrimaryChatSwitcher';
export default function EditStreamWindow() {
const { StreamingService, WindowsService } = Services;
@@ -28,6 +29,11 @@ export default function EditStreamWindow() {
prepopulate,
isLoading,
form,
+ enabledPlatforms,
+ hasMultiplePlatforms,
+ isRestreamEnabled,
+ primaryChat,
+ setPrimaryChat,
} = useGoLiveSettingsRoot({ isUpdateMode: true });
const shouldShowChecklist = lifecycle === 'runChecklist';
@@ -83,6 +89,8 @@ export default function EditStreamWindow() {
);
}
+ const shouldShowPrimaryChatSwitcher = isRestreamEnabled && hasMultiplePlatforms;
+
return (