From bf512e83383a233cad405b1724f67c67cd1d7009 Mon Sep 17 00:00:00 2001 From: Sean Yesmunt Date: Mon, 2 Dec 2019 12:30:08 -0500 Subject: [PATCH] use same code for handling open links on lbry.tv and desktop --- babel.config.js | 2 +- config.js | 2 +- lbrytv/middleware/redirect.js | 20 ++---- ui/component/button/view.jsx | 4 +- ui/component/claimPreview/view.jsx | 4 +- ui/component/externalLink/view.jsx | 2 +- ui/component/fileRender/view.jsx | 4 +- ui/component/navigationHistoryItem/view.jsx | 4 +- ui/component/previewLink/view.jsx | 4 +- ui/component/router/view.jsx | 58 ++++++++--------- ui/component/wunderbar/index.js | 4 +- ui/index.jsx | 45 +++++++------ ui/modal/modalAutoGenerateThumbnail/view.jsx | 4 +- ui/modal/modalOpenExternalResource/view.jsx | 4 +- ui/page/channel/view.jsx | 4 +- ui/redux/actions/app.js | 13 ---- ui/redux/actions/content.js | 4 +- ui/redux/actions/publish.js | 4 +- ui/scss/init/_gui.scss | 4 ++ ui/util/uri.js | 38 ----------- ui/util/url.js | 68 ++++++++++++++++++++ 21 files changed, 156 insertions(+), 140 deletions(-) delete mode 100644 ui/util/uri.js create mode 100644 ui/util/url.js diff --git a/babel.config.js b/babel.config.js index 5697c999a..27e3805dd 100644 --- a/babel.config.js +++ b/babel.config.js @@ -2,7 +2,7 @@ module.exports = api => { api.cache(false); return { - presets: ['@babel/env', '@babel/react', '@babel/flow'], + presets: [['@babel/env', { loose: true, modules: false }], '@babel/react', '@babel/flow'], plugins: [ 'import-glob', '@babel/plugin-transform-runtime', diff --git a/config.js b/config.js index 5dcdaf381..e3f1ef76e 100644 --- a/config.js +++ b/config.js @@ -1,7 +1,7 @@ const config = { WEBPACK_WEB_PORT: 9090, WEBPACK_ELECTRON_PORT: 9091, - WEB_SERVER_PORT: 80, + WEB_SERVER_PORT: 1337, DOMAIN: 'lbry.tv', URL: 'https://lbry.tv', SITE_TITLE: 'lbry.tv', diff --git a/lbrytv/middleware/redirect.js b/lbrytv/middleware/redirect.js index ae6606897..4b4ca0de3 100644 --- a/lbrytv/middleware/redirect.js +++ b/lbrytv/middleware/redirect.js @@ -1,5 +1,7 @@ const config = require('../../config'); const PAGES = require('../../ui/constants/pages'); +const { formatCustomUrl } = require('../../ui/util/url'); +const { parseURI } = require('lbry-redux'); async function redirectMiddleware(ctx, next) { const requestHost = ctx.host; @@ -16,22 +18,14 @@ async function redirectMiddleware(ctx, next) { return; } - if (requestHost === 'open.lbry.com') { - let redirectUrl = config.URL; + if (requestHost === 'open.lbry.com' || requestHost === 'open.lbry.io') { const openQuery = '?src=open'; - const matches = /(\/\?)([a-z]*)(.*)/.exec(url); + let redirectUrl = config.URL + formatCustomUrl(url, openQuery); - if (matches && matches.length) { - [, , page, queryString] = matches; - - // This is a lbry app page. Make sure to add the leading `/$/` - if (page && Object.values(PAGES).includes(page)) { - redirectUrl += '/$/' + page; - } - - redirectUrl += openQuery + queryString; + if (redirectUrl.includes('?')) { + redirectUrl = redirectUrl.replace('?', `${openQuery}&`); } else { - redirectUrl += path + openQuery; + redirectUrl += openQuery; } ctx.redirect(redirectUrl); diff --git a/ui/component/button/view.jsx b/ui/component/button/view.jsx index 8325bbe2d..1c6ac37db 100644 --- a/ui/component/button/view.jsx +++ b/ui/component/button/view.jsx @@ -4,7 +4,7 @@ import React, { forwardRef, useRef } from 'react'; import Icon from 'component/common/icon'; import classnames from 'classnames'; import { NavLink } from 'react-router-dom'; -import { formatLbryUriForWeb } from 'util/uri'; +import { formatLbryUrlForWeb } from 'util/url'; import { OutboundLink } from 'react-ga'; import * as PAGES from 'constants/pages'; import useCombinedRefs from 'effects/use-combined-refs'; @@ -108,7 +108,7 @@ const Button = forwardRef((props: Props, ref: any) => { let path = navigate; if (path) { if (path.startsWith('lbry://')) { - path = formatLbryUriForWeb(path); + path = formatLbryUrlForWeb(path); } else if (!path.startsWith('/')) { // Force a leading slash so new paths aren't appended on to the current path path = `/${path}`; diff --git a/ui/component/claimPreview/view.jsx b/ui/component/claimPreview/view.jsx index 412dfb656..1a4b24fba 100644 --- a/ui/component/claimPreview/view.jsx +++ b/ui/component/claimPreview/view.jsx @@ -5,7 +5,7 @@ import classnames from 'classnames'; import { parseURI, convertToShareLink } from 'lbry-redux'; import { withRouter } from 'react-router-dom'; import { openCopyLinkMenu } from 'util/context-menu'; -import { formatLbryUriForWeb } from 'util/uri'; +import { formatLbryUrlForWeb } from 'util/url'; import { isEmpty } from 'util/object'; import CardMedia from 'component/cardMedia'; import UriIndicator from 'component/uriIndicator'; @@ -149,7 +149,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { if (onClick) { onClick(e); } else if ((isChannel || title) && !pending) { - history.push(formatLbryUriForWeb(claim && claim.canonical_url ? claim.canonical_url : uri)); + history.push(formatLbryUrlForWeb(claim && claim.canonical_url ? claim.canonical_url : uri)); } } diff --git a/ui/component/externalLink/view.jsx b/ui/component/externalLink/view.jsx index fcf5cbf4b..453568da5 100644 --- a/ui/component/externalLink/view.jsx +++ b/ui/component/externalLink/view.jsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { isURIValid } from 'lbry-redux'; import Button from 'component/button'; import ClaimLink from 'component/claimLink'; -import { isLBRYDomain } from 'util/uri'; +import { isLBRYDomain } from 'util/url'; type Props = { href: string, diff --git a/ui/component/fileRender/view.jsx b/ui/component/fileRender/view.jsx index 16b25f412..cb677e8cf 100644 --- a/ui/component/fileRender/view.jsx +++ b/ui/component/fileRender/view.jsx @@ -7,7 +7,7 @@ import ImageViewer from 'component/viewers/imageViewer'; import AppViewer from 'component/viewers/appViewer'; import Button from 'component/button'; import { withRouter } from 'react-router-dom'; -import { formatLbryUriForWeb } from 'util/uri'; +import { formatLbryUrlForWeb } from 'util/url'; // @if TARGET='web' import { generateStreamUrl } from 'util/lbrytv'; // @endif @@ -73,7 +73,7 @@ class FileRender extends React.PureComponent { onEndedCb() { const { autoplay, nextUnplayed, history } = this.props; if (autoplay && nextUnplayed) { - history.push(formatLbryUriForWeb(nextUnplayed)); + history.push(formatLbryUrlForWeb(nextUnplayed)); } } diff --git a/ui/component/navigationHistoryItem/view.jsx b/ui/component/navigationHistoryItem/view.jsx index bd519c1f1..b9851857b 100644 --- a/ui/component/navigationHistoryItem/view.jsx +++ b/ui/component/navigationHistoryItem/view.jsx @@ -5,7 +5,7 @@ import classnames from 'classnames'; import Button from 'component/button'; import { FormField } from 'component/common/form'; import { withRouter } from 'react-router-dom'; -import { formatLbryUriForWeb } from 'util/uri'; +import { formatLbryUrlForWeb } from 'util/url'; type Props = { lastViewed: number, @@ -39,7 +39,7 @@ class NavigationHistoryItem extends React.PureComponent { ({ title } = claim.value); } - const navigatePath = formatLbryUriForWeb(uri); + const navigatePath = formatLbryUrlForWeb(uri); const onClick = onSelect || function() { diff --git a/ui/component/previewLink/view.jsx b/ui/component/previewLink/view.jsx index 47f3ad012..092ca88f8 100644 --- a/ui/component/previewLink/view.jsx +++ b/ui/component/previewLink/view.jsx @@ -4,7 +4,7 @@ import UriIndicator from 'component/uriIndicator'; import TruncatedText from 'component/common/truncated-text'; import MarkdownPreview from 'component/common/markdown-preview'; import { withRouter } from 'react-router-dom'; -import { formatLbryUriForWeb } from 'util/uri'; +import { formatLbryUrlForWeb } from 'util/url'; type Props = { uri: string, @@ -17,7 +17,7 @@ type Props = { class PreviewLink extends React.PureComponent { handleClick = () => { const { uri, history } = this.props; - history.push(formatLbryUriForWeb(uri)); + history.push(formatLbryUrlForWeb(uri)); }; render() { diff --git a/ui/component/router/view.jsx b/ui/component/router/view.jsx index 1a252d474..a7e0a004f 100644 --- a/ui/component/router/view.jsx +++ b/ui/component/router/view.jsx @@ -1,6 +1,6 @@ // @flow import * as PAGES from 'constants/pages'; -import React, { useEffect, Suspense } from 'react'; +import React, { useEffect } from 'react'; import { Route, Redirect, Switch, withRouter } from 'react-router-dom'; import SettingsPage from 'page/settings'; import HelpPage from 'page/help'; @@ -69,37 +69,35 @@ function AppRouter(props: Props) { }, [currentScroll, pathname]); return ( - LODIFJDSLKJFSLDKJFLDJ}> - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - {/* Below need to go at the end to make sure we don't match any of our pages first */} - - - - - + {/* Below need to go at the end to make sure we don't match any of our pages first */} + + + + ); } diff --git a/ui/component/wunderbar/index.js b/ui/component/wunderbar/index.js index 9e4c9fc92..2895ee450 100644 --- a/ui/component/wunderbar/index.js +++ b/ui/component/wunderbar/index.js @@ -11,7 +11,7 @@ import { import analytics from 'analytics'; import Wunderbar from './view'; import { withRouter } from 'react-router-dom'; -import { formatLbryUriForWeb } from 'util/uri'; +import { formatLbryUrlForWeb } from 'util/url'; const select = state => ({ suggestions: selectSearchSuggestions(state), @@ -26,7 +26,7 @@ const perform = (dispatch, ownProps) => ({ analytics.apiLogSearch(); }, onSubmit: uri => { - const path = formatLbryUriForWeb(uri); + const path = formatLbryUrlForWeb(uri); ownProps.history.push(path); dispatch(doUpdateSearchQuery('')); }, diff --git a/ui/index.jsx b/ui/index.jsx index 7dab9afc6..cc6488602 100644 --- a/ui/index.jsx +++ b/ui/index.jsx @@ -13,7 +13,7 @@ import * as MODALS from 'constants/modal_types'; import React, { Fragment, useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; -import { doConditionalAuthNavigate, doDaemonReady, doAutoUpdate, doOpenModal, doHideModal } from 'redux/actions/app'; +import { doDaemonReady, doAutoUpdate, doOpenModal, doHideModal } from 'redux/actions/app'; import { Lbry, doToast, isURIValid, setSearchApi, apiCall } from 'lbry-redux'; import { doSetLanguage, doUpdateIsNightAsync } from 'redux/actions/settings'; import { @@ -28,7 +28,7 @@ import pjson from 'package.json'; import app from './app'; import doLogWarningConsoleMessage from './logWarningConsoleMessage'; import { ConnectedRouter, push } from 'connected-react-router'; -import { formatLbryUriForWeb } from 'util/uri'; +import { formatLbryUrlForWeb, formatCustomUrl } from 'util/url'; import { PersistGate } from 'redux-persist/integration/react'; import analytics from 'analytics'; import { getAuthToken, setAuthToken } from 'util/saved-passwords'; @@ -70,7 +70,6 @@ Lbry.setOverride( const startTime = Date.now(); analytics.startupEvent(); -const APPPAGEURL = 'lbry://?'; // @if TARGET='app' const { autoUpdater } = remote.require('electron-updater'); autoUpdater.logger = remote.require('electron-log'); @@ -157,25 +156,29 @@ rewards.setCallback('claimRewardSuccess', () => { }); // @if TARGET='app' -ipcRenderer.on('open-uri-requested', (event, uri, newSession) => { - if (uri && uri.startsWith('lbry://')) { - if (uri.startsWith('lbry://?verify')) { - app.store.dispatch(doConditionalAuthNavigate(newSession)); - } else if (uri.startsWith(APPPAGEURL)) { - const navpage = uri.replace(APPPAGEURL, '').toLowerCase(); - app.store.dispatch(push(`/$/${navpage}`)); - } else if (isURIValid(uri)) { - const formattedUri = formatLbryUriForWeb(uri); - app.store.dispatch(push(formattedUri)); - analytics.openUrlEvent(formattedUri); - } else { - app.store.dispatch( - doToast({ - message: __('Invalid LBRY URL requested'), - }) - ); - } +ipcRenderer.on('open-uri-requested', (event, url, newSession) => { + function handleError() { + app.store.dispatch( + doToast({ + message: __('Invalid LBRY URL requested'), + }) + ); } + + const path = url.slice('lbry://'.length); + if (path.startsWith('?')) { + const redirectUrl = formatCustomUrl(path); + return app.store.dispatch(push(redirectUrl)); + } + + if (isURIValid(url)) { + const formattedUrl = formatLbryUrlForWeb(url); + analytics.openUrlEvent(formattedUrl); + return app.store.dispatch(push(formattedUrl)); + } + + // If nothing redirected before here the url must be messed up + handleError(); }); ipcRenderer.on('language-set', (event, language) => { diff --git a/ui/modal/modalAutoGenerateThumbnail/view.jsx b/ui/modal/modalAutoGenerateThumbnail/view.jsx index 4da0dcd1f..b7115e079 100644 --- a/ui/modal/modalAutoGenerateThumbnail/view.jsx +++ b/ui/modal/modalAutoGenerateThumbnail/view.jsx @@ -1,7 +1,7 @@ // @flow import React, { useRef } from 'react'; import { Modal } from 'modal/modal'; -import { formatPathForWeb } from 'util/uri'; +import { formatFileSystemPath } from 'util/url'; type Props = { upload: WebFile => void, @@ -15,7 +15,7 @@ function ModalAutoGenerateThumbnail(props: Props) { const playerRef = useRef(); let videoSrc; if (typeof filePath === 'string') { - videoSrc = formatPathForWeb(filePath); + videoSrc = formatFileSystemPath(filePath); } else { videoSrc = URL.createObjectURL(filePath); } diff --git a/ui/modal/modalOpenExternalResource/view.jsx b/ui/modal/modalOpenExternalResource/view.jsx index 1a1b3321d..274b656df 100644 --- a/ui/modal/modalOpenExternalResource/view.jsx +++ b/ui/modal/modalOpenExternalResource/view.jsx @@ -1,7 +1,7 @@ // @flow import React from 'react'; import { Modal } from 'modal/modal'; -import { formatPathForWeb } from 'util/uri'; +import { formatFileSystemPath } from 'util/url'; // @if TARGET='app' import { shell } from 'electron'; // @endif @@ -37,7 +37,7 @@ function ModalOpenExternalResource(props: Props) { if (uri) { window.open(uri); } else if (path) { - window.open(formatPathForWeb(path)); + window.open(formatFileSystemPath(path)); } // @endif diff --git a/ui/page/channel/view.jsx b/ui/page/channel/view.jsx index 7b3a8c21f..308701835 100644 --- a/ui/page/channel/view.jsx +++ b/ui/page/channel/view.jsx @@ -8,7 +8,7 @@ import ShareButton from 'component/shareButton'; import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs'; import { withRouter } from 'react-router'; import Button from 'component/button'; -import { formatLbryUriForWeb } from 'util/uri'; +import { formatLbryUrlForWeb } from 'util/url'; import ChannelContent from 'component/channelContent'; import ChannelAbout from 'component/channelAbout'; import ChannelDiscussion from 'component/channelDiscussion'; @@ -93,7 +93,7 @@ function ChannelPage(props: Props) { const tabIndex = currentView === ABOUT_PAGE || editing ? 1 : currentView === DISCUSSION_PAGE ? 2 : 0; function onTabChange(newTabIndex) { - let url = formatLbryUriForWeb(uri); + let url = formatLbryUrlForWeb(uri); let search = '?'; if (newTabIndex === 0) { diff --git a/ui/redux/actions/app.js b/ui/redux/actions/app.js index 9ab81e5c9..a69ceaeb2 100644 --- a/ui/redux/actions/app.js +++ b/ui/redux/actions/app.js @@ -6,7 +6,6 @@ import { ipcRenderer, remote } from 'electron'; import path from 'path'; import * as ACTIONS from 'constants/action_types'; import * as MODALS from 'constants/modal_types'; -import * as PAGES from 'constants/pages'; import { Lbry, doBalanceSubscribe, @@ -36,7 +35,6 @@ import { } from 'redux/selectors/app'; import { doAuthenticate, doGetSync } from 'lbryinc'; import { lbrySettings as config, version as appVersion } from 'package.json'; -import { push } from 'connected-react-router'; import analytics from 'analytics'; import { doSignOutCleanup, deleteSavedPassword, getSavedPassword } from 'util/saved-passwords'; @@ -404,17 +402,6 @@ export function doClickCommentButton() { }; } -export function doConditionalAuthNavigate(newSession) { - return (dispatch, getState) => { - const state = getState(); - const modal = selectModal(state); - - if (newSession || (modal && modal.id !== MODALS.EMAIL_COLLECTION)) { - dispatch(push(`/$/${PAGES.AUTH}`)); - } - }; -} - export function doToggleSearchExpanded() { return { type: ACTIONS.TOGGLE_SEARCH_EXPANDED, diff --git a/ui/redux/actions/content.js b/ui/redux/actions/content.js index 668ae08b9..c4b3ac688 100644 --- a/ui/redux/actions/content.js +++ b/ui/redux/actions/content.js @@ -25,7 +25,7 @@ import { } from 'lbry-redux'; import { makeSelectCostInfoForUri } from 'lbryinc'; import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings'; -import { formatLbryUriForWeb } from 'util/uri'; +import { formatLbryUrlForWeb } from 'util/url'; const DOWNLOAD_POLL_INTERVAL = 250; @@ -85,7 +85,7 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) { silent: false, }); notif.onclick = () => { - dispatch(push(formatLbryUriForWeb(uri))); + dispatch(push(formatLbryUrlForWeb(uri))); }; } diff --git a/ui/redux/actions/publish.js b/ui/redux/actions/publish.js index b794b4241..76bb71025 100644 --- a/ui/redux/actions/publish.js +++ b/ui/redux/actions/publish.js @@ -6,7 +6,7 @@ import { batchActions, doError, selectMyClaims, doPublish, doCheckPendingPublish import { selectosNotificationsEnabled } from 'redux/selectors/settings'; import { push } from 'connected-react-router'; import analytics from 'analytics'; -import { formatLbryUriForWeb } from 'util/uri'; +import { formatLbryUrlForWeb } from 'util/url'; import { doOpenModal } from './app'; export const doPublishDesktop = (filePath: string) => (dispatch: Dispatch, getState: () => {}) => { @@ -81,7 +81,7 @@ export const doCheckPendingPublishesApp = () => (dispatch: Dispatch, getState: G silent: false, }); notif.onclick = () => { - dispatch(push(formatLbryUriForWeb(claim.permanent_url))); + dispatch(push(formatLbryUrlForWeb(claim.permanent_url))); }; } }; diff --git a/ui/scss/init/_gui.scss b/ui/scss/init/_gui.scss index 22159a994..dd93c07ab 100644 --- a/ui/scss/init/_gui.scss +++ b/ui/scss/init/_gui.scss @@ -6,6 +6,10 @@ color: var(--color-text-selection); } +*:focus { + outline: none; +} + html { @include font-sans; height: 100%; diff --git a/ui/util/uri.js b/ui/util/uri.js deleted file mode 100644 index 353008c2e..000000000 --- a/ui/util/uri.js +++ /dev/null @@ -1,38 +0,0 @@ -// @flow - -const LBRY_INC_DOMAINS = ['lbry.io', 'lbry.com', 'lbry.tv', 'lbry.tech', 'lbry.fund', 'spee.ch']; - -export const formatLbryUriForWeb = (uri: string) => { - return uri.replace('lbry://', '/').replace(/#/g, ':'); -}; - -export const formatPathForWeb = (path: string) => { - if (!path) { - return; - } - - let webUrl = path.replace(/\\/g, '/'); - - if (webUrl[0] !== '/') { - webUrl = `/${webUrl}`; - } - - return encodeURI(`file://${webUrl}`).replace(/[?#]/g, encodeURIComponent); -}; - -export const isLBRYDomain = (uri: string) => { - if (!uri) { - return; - } - - const myURL = new URL(uri); - const hostname = myURL.hostname; - - for (let domain of LBRY_INC_DOMAINS) { - if (hostname.endsWith(domain)) { - return true; - } - } - - return false; -}; diff --git a/ui/util/url.js b/ui/util/url.js new file mode 100644 index 000000000..66ea82831 --- /dev/null +++ b/ui/util/url.js @@ -0,0 +1,68 @@ +const LBRY_INC_DOMAINS = ['lbry.io', 'lbry.com', 'lbry.tv', 'lbry.tech', 'lbry.fund', 'spee.ch']; + +exports.formatLbryUrlForWeb = uri => { + return uri.replace('lbry://', '/').replace(/#/g, ':'); +}; + +exports.formatFileSystemPath = path => { + if (!path) { + return; + } + + let webUrl = path.replace(/\\/g, '/'); + + if (webUrl[0] !== '/') { + webUrl = `/${webUrl}`; + } + + return encodeURI(`file://${webUrl}`).replace(/[?#]/g, encodeURIComponent); +}; + +exports.isLBRYDomain = uri => { + if (!uri) { + return; + } + + const myURL = new URL(uri); + const hostname = myURL.hostname; + + for (let domain of LBRY_INC_DOMAINS) { + if (hostname.endsWith(domain)) { + return true; + } + } + + return false; +}; + +/* + Function that handles page redirects + ex: lbry://?rewards + ex: open.lbry.com/?rewards +*/ +exports.formatCustomUrl = path => { + // Determine if we need to add a leading "/$/" for app pages + const APP_PAGE_REGEX = /(\?)([a-z]*)(.*)/; + const appPageMatches = APP_PAGE_REGEX.exec(path); + + if (appPageMatches && appPageMatches.length) { + // Definitely an app page (or it's formatted like one) + const [, , page, queryString] = appPageMatches; + let actualUrl = '/$/' + page; + + if (queryString) { + if (queryString.startsWith('?')) { + actualUrl += queryString; + } else if (queryString.startsWith('&')) { + // Replace the leading "&" with a "?" because we must have lost the "?" from the page name + // /?rewards&a=b => /$/rewards?a=b + actualUrl += `?${queryString.slice(1)}`; + } + + return actualUrl; + } + } + + // Regular claim url + return path; +};