Skip to content

Commit 41ada5a

Browse files
authored
Adds link to open sync from youtube page (#3431)
* Adds link to open sync from transfer status Adds a button to open the Odysee sync page directly from the YouTube transfer status page using a deep link. Enables auto-opening YouTube sync deep link Allows the application to automatically launch the YouTube sync deep link when specific URL parameters (`open_in_sync` or `open_app`) are present. This streamlines the user flow by initiating the sync process immediately upon page load for users directed with these parameters. It also removes the auto-open parameters from the URL after the attempt. Adjusts YouTube self-sync card visibility Expands the display conditions for the self-sync alternative card to include users with `autoOpenSync` enabled, improving discoverability for relevant users. Removes the direct "Open in Odysee Sync" button, streamlining the UI and potentially simplifying the user's journey for initiating the sync process. The self-sync token remains available for manual transfer. * Improves deep link security and query param interpretation Enhances `odysee://token/` deep link handling with stricter regex validation and HTML attribute escaping, preventing potential XSS vulnerabilities. Refines query parameter parsing for auto-opening the sync page, treating empty parameter values as truthy and ensuring primary parameters are prioritized.
1 parent 5bd4fee commit 41ada5a

File tree

5 files changed

+202
-55
lines changed

5 files changed

+202
-55
lines changed

ui/component/portals/view.jsx

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ type Props = {
2222
export default function Portals(props: Props) {
2323
const { homepageData, homepageOrder, doSetClientSetting, authenticated, activePortal } = props;
2424
const { portals, categories } = homepageData;
25+
const mainPortal = portals?.mainPortal;
26+
const mainPortals = mainPortal?.portals || [];
2527

2628
const [width, setWidth] = React.useState(0);
2729
const [tileWidth, setTileWidth] = React.useState(0);
@@ -30,23 +32,23 @@ export default function Portals(props: Props) {
3032
const [index, setIndex] = React.useState(1);
3133
const [pause, setPause] = React.useState(false);
3234
const [hover, setHover] = React.useState(undefined);
33-
const rotate = portals?.mainPortal?.portals?.length > tileNum;
35+
const rotate = mainPortals.length > tileNum;
3436

3537
const [kill, setKill] = React.useState(false);
3638
const wrapper = React.useRef(null);
3739

3840
const imageWidth = width >= 1600 ? 1700 : width >= 1150 ? 1150 : width >= 900 ? 900 : width >= 600 ? 600 : 400;
3941

4042
React.useEffect(() => {
41-
if (rotate && portals && width) {
43+
if (rotate && width) {
4244
const interval = setInterval(() => {
4345
if (!pause) {
44-
setIndex(index + 1 <= portals.mainPortal.portals.length - (tileNum - 1) ? index + 1 : 1);
46+
setIndex(index + 1 <= mainPortals.length - (tileNum - 1) ? index + 1 : 1);
4547
}
4648
}, 5000 + 1000);
4749
return () => clearInterval(interval);
4850
}
49-
}, [rotate, portals, tileNum, marginLeft, width, pause, index]);
51+
}, [rotate, mainPortals.length, tileNum, marginLeft, width, pause, index]);
5052

5153
React.useEffect(() => {
5254
if (portals && width) {
@@ -59,14 +61,14 @@ export default function Portals(props: Props) {
5961
if (wrapper.current) {
6062
let wrapperWidth = wrapper.current.offsetWidth + 12;
6163
let tileNum = wrapperWidth > 954 ? 6 : wrapperWidth > 870 ? 5 : wrapperWidth > 470 ? 3 : 2;
62-
if (tileNum === 6 && portals.mainPortal.portals.length < 9) {
63-
tileNum = portals.mainPortal.portals.length;
64+
if (tileNum === 6 && mainPortals.length < 9 && mainPortals.length > 0) {
65+
tileNum = mainPortals.length;
6466
}
6567
setWidth(wrapperWidth);
6668
setTileNum(tileNum);
6769
setTileWidth(wrapperWidth / tileNum);
6870
}
69-
}, [portals]);
71+
}, [mainPortals.length]);
7072
useOnResize(handleResize);
7173

7274
const NON_CATEGORY = Object.freeze({
@@ -113,7 +115,7 @@ export default function Portals(props: Props) {
113115
orderToSave.hidden = ['PORTALS'];
114116
}
115117
} else if (!orderToSave.hidden) {
116-
const SECTIONS = { ...NON_CATEGORY, ...categories };
118+
const SECTIONS = { ...NON_CATEGORY, ...(categories || {}) };
117119
orderToSave = { active: [], hidden: [] };
118120
orderToSave.active = getInitialList('ACTIVE', homepageOrder, SECTIONS);
119121
orderToSave.hidden = getInitialList('HIDDEN', homepageOrder, SECTIONS);
@@ -125,7 +127,7 @@ export default function Portals(props: Props) {
125127
setKill(true);
126128
}
127129

128-
return portals && portals.mainPortal ? (
130+
return mainPortal ? (
129131
<div
130132
id="portals"
131133
className={classnames('portals-wrapper', { kill: kill })}
@@ -134,15 +136,15 @@ export default function Portals(props: Props) {
134136
'url(https://thumbnails.odycdn.com/optimize/s:' +
135137
imageWidth +
136138
':0/quality:95/plain/' +
137-
portals.mainPortal.background +
139+
mainPortal.background +
138140
')',
139141
}}
140142
onMouseEnter={() => setPause(true)}
141143
onMouseLeave={() => setPause(false)}
142144
>
143-
<h1>{portals.mainPortal.description}</h1>
145+
<h1>{mainPortal.description}</h1>
144146
<div className="portal-rotator" style={{ marginLeft: marginLeft }} ref={wrapper}>
145-
{portals.mainPortal.portals.map((portal, i) => {
147+
{mainPortals.map((portal, i) => {
146148
return (
147149
<div
148150
className={classnames('portal-wrapper', { disabled: portal.name === activePortal })}
@@ -172,33 +174,32 @@ export default function Portals(props: Props) {
172174
);
173175
})}
174176
</div>
175-
{portals.mainPortal.portals.length > tileNum && (
177+
{mainPortals.length > tileNum && (
176178
<>
177179
<div
178180
className="portal-browse left"
179-
onClick={() => setIndex(index > 1 ? index - 1 : portals.mainPortal.portals.length - (tileNum - 1))}
181+
onClick={() => setIndex(index > 1 ? index - 1 : mainPortals.length - (tileNum - 1))}
180182
>
181183
182184
</div>
183185
<div
184186
className="portal-browse right"
185-
onClick={() => setIndex(index + (tileNum - 1) < portals.mainPortal.portals.length ? index + 1 : 1)}
187+
onClick={() => setIndex(index + (tileNum - 1) < mainPortals.length ? index + 1 : 1)}
186188
>
187189
188190
</div>
189191
<div className="portal-active-indicator">
190-
{portals &&
191-
portals.mainPortal.portals.map((item, i) => {
192-
return (
193-
i < portals.mainPortal.portals.length - (tileNum - 1) && (
194-
<div
195-
key={i}
196-
className={i + 1 === index ? 'portal-active-indicator-active' : ''}
197-
onClick={() => setIndex(i + 1)}
198-
/>
199-
)
200-
);
201-
})}
192+
{mainPortals.map((item, i) => {
193+
return (
194+
i < mainPortals.length - (tileNum - 1) && (
195+
<div
196+
key={i}
197+
className={i + 1 === index ? 'portal-active-indicator-active' : ''}
198+
onClick={() => setIndex(i + 1)}
199+
/>
200+
)
201+
);
202+
})}
202203
</div>
203204
</>
204205
)}

ui/component/youtubeTransferStatus/style.lazy.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,8 @@
199199
line-height: 1.5;
200200
font-style: italic;
201201
}
202+
203+
.token-display__open-link {
204+
margin-top: var(--spacing-xs);
205+
}
202206
}

ui/component/youtubeTransferStatus/view.jsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// @flow
2-
import { SITE_HELP_EMAIL } from 'config';
2+
import { SITE_HELP_EMAIL, DOMAIN } from 'config';
33
import * as ICONS from 'constants/icons';
44
import * as React from 'react';
55
import classnames from 'classnames';
@@ -22,10 +22,14 @@ type Props = {
2222
videosImported: ?Array<number>, // [currentAmountImported, totalAmountToImport]
2323
alwaysShow: boolean,
2424
addNewChannel?: boolean,
25+
autoOpenSync?: boolean,
2526
doResolveUris: (uris: Array<string>) => void,
2627
experimentalUi: boolean,
2728
};
2829

30+
const AUTO_OPEN_SYNC_PARAM = 'open_in_sync';
31+
const AUTO_OPEN_SYNC_PARAM_ALT = 'open_app';
32+
2933
export default function YoutubeTransferStatus(props: Props) {
3034
const {
3135
youtubeChannels,
@@ -36,6 +40,7 @@ export default function YoutubeTransferStatus(props: Props) {
3640
updateUser,
3741
alwaysShow = false,
3842
addNewChannel,
43+
autoOpenSync = false,
3944
doResolveUris,
4045
experimentalUi,
4146
} = props;
@@ -49,6 +54,19 @@ export default function YoutubeTransferStatus(props: Props) {
4954
const firstAvailableToken = hasChannels
5055
? youtubeChannels.find((channel) => channel.status_token)?.status_token
5156
: null;
57+
const selfSyncDeepLink = firstAvailableToken ? `odysee://token/${firstAvailableToken}` : null;
58+
const selfSyncLauncherUrl = selfSyncDeepLink
59+
? `https://${DOMAIN}/$/spinner?launch=${encodeURIComponent(selfSyncDeepLink)}`
60+
: null;
61+
const showSelfSyncCard = experimentalUi || autoOpenSync;
62+
const hasAutoOpenedRef = React.useRef(false);
63+
64+
function clearAutoOpenParamsFromUrl() {
65+
const url = new URL(window.location.href);
66+
url.searchParams.delete(AUTO_OPEN_SYNC_PARAM);
67+
url.searchParams.delete(AUTO_OPEN_SYNC_PARAM_ALT);
68+
window.history.replaceState(window.history.state, '', `${url.pathname}${url.search}${url.hash}`);
69+
}
5270

5371
// State for token visibility
5472
const [isTokenVisible, setIsTokenVisible] = React.useState(false);
@@ -104,6 +122,20 @@ export default function YoutubeTransferStatus(props: Props) {
104122
}
105123
}, [hasPendingTransfers, checkYoutubeTransfer, updateUser]);
106124

125+
React.useEffect(() => {
126+
if (!autoOpenSync || hasAutoOpenedRef.current || !selfSyncLauncherUrl) {
127+
return;
128+
}
129+
130+
hasAutoOpenedRef.current = true;
131+
clearAutoOpenParamsFromUrl();
132+
133+
const launcherWindow = window.open(selfSyncLauncherUrl, '_blank');
134+
if (!launcherWindow) {
135+
window.location.href = selfSyncLauncherUrl;
136+
}
137+
}, [autoOpenSync, selfSyncLauncherUrl]);
138+
107139
return (
108140
(alwaysShow || (hasChannels && !isYoutubeTransferComplete)) && (
109141
<Card
@@ -294,8 +326,8 @@ export default function YoutubeTransferStatus(props: Props) {
294326
/>
295327
</p>
296328

297-
{/* Self-Sync Alternative - Only show for experimental UI users */}
298-
{experimentalUi && (
329+
{/* Self-Sync Alternative */}
330+
{showSelfSyncCard && (
299331
<div className="card card--self-sync">
300332
<div className="card__header">
301333
<h4>

ui/page/youtubeSync/view.jsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,21 @@ const STATUS_TOKEN_PARAM = 'status_token';
2424
const ERROR_PARAM = 'error';
2525
const ERROR_MESSAGE_PARAM = 'error_message';
2626
const NEW_CHANNEL_PARAM = 'new_channel';
27+
const AUTO_OPEN_SYNC_PARAM = 'open_in_sync';
28+
const AUTO_OPEN_SYNC_PARAM_ALT = 'open_app';
29+
30+
function isTruthyQueryValue(value: ?string): boolean {
31+
if (value === null || value === undefined) {
32+
return false;
33+
}
34+
35+
if (value === '') {
36+
return true;
37+
}
38+
39+
const normalizedValue = value.toLowerCase();
40+
return normalizedValue !== '0' && normalizedValue !== 'false' && normalizedValue !== 'no';
41+
}
2742

2843
type Props = {
2944
youtubeChannels: ?Array<{ transfer_state: string, sync_status: string }>,
@@ -44,6 +59,11 @@ export default function YoutubeSync(props: Props) {
4459
const hasErrorParam = urlParams.get(ERROR_PARAM) === 'true';
4560
const errorMessage = urlParams.get(ERROR_MESSAGE_PARAM);
4661
const newChannelParam = urlParams.get(NEW_CHANNEL_PARAM);
62+
const hasAutoOpenSyncPrimaryParam = urlParams.has(AUTO_OPEN_SYNC_PARAM);
63+
const autoOpenSyncParam = hasAutoOpenSyncPrimaryParam
64+
? urlParams.get(AUTO_OPEN_SYNC_PARAM)
65+
: urlParams.get(AUTO_OPEN_SYNC_PARAM_ALT);
66+
const shouldAutoOpenSync = isTruthyQueryValue(autoOpenSyncParam);
4767
const [channel, setChannel] = React.useState('');
4868
const [language, setLanguage] = React.useState(getDefaultLanguage());
4969
const [nameError, setNameError] = React.useState(undefined);
@@ -121,7 +141,7 @@ export default function YoutubeSync(props: Props) {
121141
<div className="main__channel-creation">
122142
{showYoutubeTransferStatus ? (
123143
<React.Suspense fallback={null}>
124-
<YoutubeTransferStatus alwaysShow addNewChannel={handleNewChannel} />
144+
<YoutubeTransferStatus alwaysShow addNewChannel={handleNewChannel} autoOpenSync={shouldAutoOpenSync} />
125145
</React.Suspense>
126146
) : (
127147
<Card

0 commit comments

Comments
 (0)