From ead85f867a78de793e22b04e0d05ab9b97ad5e8c Mon Sep 17 00:00:00 2001 From: samuelmaddock Date: Sun, 7 Mar 2021 17:23:28 -0500 Subject: [PATCH] refactor: move HomeScreen to React component --- .../metastream-app/src/assets/homescreen.html | 214 ------------------ .../metastream-app/src/assets/webview.html | 10 + .../metastream-app/src/components/Popup.tsx | 47 +--- .../metastream-app/src/components/Portal.tsx | 56 +++++ .../metastream-app/src/components/Webview.tsx | 17 +- .../src/components/browser/HomeScreen.css | 178 +++++++++++++++ .../src/components/browser/HomeScreen.tsx | 161 +++++++++++++ .../src/components/browser/WebBrowser.tsx | 47 ++-- .../metastream-app/src/constants/storage.ts | 3 +- packages/metastream-app/src/index.html | 6 +- .../src/styles/resource.global.css | 50 ---- packages/metastream-app/src/utils/appUrl.ts | 6 +- 12 files changed, 456 insertions(+), 339 deletions(-) delete mode 100644 packages/metastream-app/src/assets/homescreen.html create mode 100644 packages/metastream-app/src/assets/webview.html create mode 100644 packages/metastream-app/src/components/Portal.tsx create mode 100644 packages/metastream-app/src/components/browser/HomeScreen.css create mode 100644 packages/metastream-app/src/components/browser/HomeScreen.tsx diff --git a/packages/metastream-app/src/assets/homescreen.html b/packages/metastream-app/src/assets/homescreen.html deleted file mode 100644 index d4d29f57..00000000 --- a/packages/metastream-app/src/assets/homescreen.html +++ /dev/null @@ -1,214 +0,0 @@ - - - - - Home - - - - - - -
- -
-

- From here you can browse the web to find media to share in the session. -

- -
    -
  • - 🌐 Metastream supports many websites! Don't see one in - the list? Try adding the link anyway! -
  • -
  • - 👥 Everyone in the session will need an account or subscription if required by a website. -
  • -
  • - 🐛 Is a website not working how you'd expect? Help us out by - reporting the issue on GitHub. -
  • -
-
-
-
-
- -
- - -
-
-
- - - diff --git a/packages/metastream-app/src/assets/webview.html b/packages/metastream-app/src/assets/webview.html new file mode 100644 index 00000000..2f67e8a6 --- /dev/null +++ b/packages/metastream-app/src/assets/webview.html @@ -0,0 +1,10 @@ + + + + + Metastream Webview + + + + + diff --git a/packages/metastream-app/src/components/Popup.tsx b/packages/metastream-app/src/components/Popup.tsx index 56851c6d..be9425a0 100644 --- a/packages/metastream-app/src/components/Popup.tsx +++ b/packages/metastream-app/src/components/Popup.tsx @@ -4,10 +4,10 @@ import { HighlightButton } from './common/button' import { t } from 'locale' import { Trans } from 'react-i18next' import { isFirefox } from 'utils/browser' -import { createPortal } from 'react-dom' import { Remote } from './remote' import { dispatchExtensionMessage } from 'utils/extension' import { ASSETS_PATH } from 'utils/appUrl' +import { Portal } from './Portal' const POPUP_WIDTH = 336 // px @@ -111,7 +111,6 @@ export class PopupWindow extends Component { } private intervalId?: number - private stylesheetObserver?: MutationObserver componentWillUnmount() { this.closeWindows() @@ -140,11 +139,6 @@ export class PopupWindow extends Component { PopupWindow.mediaWindowRef = null } - if (this.stylesheetObserver) { - this.stylesheetObserver.disconnect() - this.stylesheetObserver = undefined - } - clearInterval(this.intervalId) this.intervalId = undefined @@ -186,39 +180,6 @@ export class PopupWindow extends Component { remotePopupReady: Boolean(PopupWindow.remoteWindowRef && !PopupWindow.remoteWindowRef.closed), mediaPopupReady: Boolean(PopupWindow.mediaWindowRef && !PopupWindow.mediaWindowRef.closed) }) - - if (this.stylesheetObserver) { - this.stylesheetObserver.disconnect() - } - - this.stylesheetObserver = new MutationObserver(this.onHeadMutation) - this.stylesheetObserver.observe(document.head, { childList: true }) - - this.copyStyleSheets() - } - - private onHeadMutation: MutationCallback = list => { - const shouldCopyStyles = list.some(record => record.type === 'childList') - if (shouldCopyStyles) { - this.copyStyleSheets() - } - } - - private copyStyleSheets() { - const remoteDocument = PopupWindow.remoteWindowRef && PopupWindow.remoteWindowRef.document - if (remoteDocument) { - // remove existing stylesheets - Array.from(remoteDocument.styleSheets).forEach(stylesheet => { - if (stylesheet.ownerNode) stylesheet.ownerNode.remove() - }) - - // add all stylesheets from main document - Array.from(document.styleSheets).forEach(stylesheet => { - if (stylesheet.ownerNode) { - remoteDocument.head.appendChild(stylesheet.ownerNode.cloneNode(true)) - } - }) - } } private openWindows = () => { @@ -322,7 +283,11 @@ export class PopupWindow extends Component { const root = remoteDocument.getElementById('root') if (!root) return - return createPortal(, root) + return ( + + + + ) } private renderPopupIcon() { diff --git a/packages/metastream-app/src/components/Portal.tsx b/packages/metastream-app/src/components/Portal.tsx new file mode 100644 index 00000000..3aedd4c1 --- /dev/null +++ b/packages/metastream-app/src/components/Portal.tsx @@ -0,0 +1,56 @@ +import React, { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' + +interface Props { + container: HTMLElement + children: React.ReactNode +} + +export const Portal = ({ children, container }: Props) => { + const [isReady, setIsReady] = useState(false) + + const copyStyleSheets = () => { + const remoteDocument = container.ownerDocument + if (remoteDocument) { + // remove existing stylesheets + Array.from(remoteDocument.styleSheets).forEach(stylesheet => { + if (stylesheet.ownerNode) stylesheet.ownerNode.remove() + }) + + // add all stylesheets from main document + Array.from(document.styleSheets).forEach(stylesheet => { + if (stylesheet.ownerNode) { + remoteDocument.head.appendChild(stylesheet.ownerNode.cloneNode(true)) + } + }) + } + } + + useEffect(function componentDidMount() { + const stylesheetObserver = new MutationObserver(list => { + const shouldCopyStyles = list.some(record => record.type === 'childList') + if (shouldCopyStyles) { + copyStyleSheets() + } + }) + + stylesheetObserver.observe(document.head, { childList: true }) + + copyStyleSheets() + + // Need to wait a bit for stylesheets to load to prevent flashing content. + let timeoutId = setTimeout(() => { + setIsReady(true) + }, 60) + + return function componentWillUnmount() { + stylesheetObserver.disconnect() + + if (timeoutId) { + clearTimeout(timeoutId) + } + } + }, []) + + return createPortal(isReady ? children : <>, container) +} diff --git a/packages/metastream-app/src/components/Webview.tsx b/packages/metastream-app/src/components/Webview.tsx index 06dd59c1..fa44a6a0 100644 --- a/packages/metastream-app/src/components/Webview.tsx +++ b/packages/metastream-app/src/components/Webview.tsx @@ -24,9 +24,9 @@ interface Props { /** Whether this webview should use a popup window. */ popup?: boolean onClosePopup?: Function - onMessage?: (event: MessageEvent) => void backgroundImage?: string onActivity?: (eventName: string) => void + onNavigate?: (url: string) => void } interface State { @@ -87,12 +87,6 @@ export class Webview extends Component { } private onMessage(event: MessageEvent) { - if (this.iframe && this.iframe.contentWindow === event.source) { - if (this.props.onMessage) { - this.props.onMessage(event) - } - } - const { data } = event if (typeof data !== 'object' || typeof data.type !== 'string') return @@ -203,6 +197,9 @@ export class Webview extends Component { private didNavigate = () => { this.clearNavigateTimeout() + if (this.props.onNavigate) { + this.props.onNavigate(this.url) + } } componentWillReceiveProps(nextProps: Props) { @@ -228,10 +225,10 @@ export class Webview extends Component { className, popup, onClosePopup, - onMessage, backgroundImage, onActivity, mediaSrc, + onNavigate, ...rest } = this.props @@ -329,4 +326,8 @@ export class Webview extends Component { reloadIgnoringCache() { this.dispatchRemoteEvent('reload', true) } + + getIFrame() { + return this.iframe + } } diff --git a/packages/metastream-app/src/components/browser/HomeScreen.css b/packages/metastream-app/src/components/browser/HomeScreen.css new file mode 100644 index 00000000..e6af9a37 --- /dev/null +++ b/packages/metastream-app/src/components/browser/HomeScreen.css @@ -0,0 +1,178 @@ +.container { + --color-btn: rgba(255, 255, 255, 0.22); + --color-btn-text: rgba(255, 255, 255, 0.88); + --column-width: 47rem; + --tips-height: 3rem; + height: 100%; + overflow: auto; +} + +.main { + min-height: calc(100vh - var(--tips-height)); + padding: 0 30px; + padding-top: var(--tips-height); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.column { + width: 100%; + max-width: var(--column-width); + padding-left: 1rem; + padding-right: 1rem; + margin: 0 auto; +} + +.row { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.gridContainer { + display: grid; + grid-gap: 30px; + grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); + grid-auto-flow: dense; +} + +.tips { + position: absolute; + z-index: 1; + width: 100%; + height: var(--tips-height); + overflow: hidden; + border-bottom: 1px solid var(--color-transparent-light-30); + transition: background-color 100ms ease-out; +} + +.tipsExpanded { + height: auto; +} + +.tips:hover, +.tipsExpanded { + backdrop-filter: blur(10px); + background-color: var(--color-transparent-light-30); +} + +/* fix bad contrast in firefox where backdrop blur isn't supported */ +@-moz-document url-prefix() { + .tipsExpanded { + background-color: rgba(0, 0, 0, 0.4); + } +} + +.tipsExpanded svg { + transform: rotate(180deg); +} + +.tipsButton { + width: 100%; + height: var(--tips-height); +} + +.tipsButton .column { + text-align: left; + display: flex; + justify-content: space-between; + align-items: center; +} + +.emojiList { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.emojiList li { + padding-left: 2rem; + position: relative; + margin-top: 1rem; +} + +.emojiList li:first-child { + margin-top: 0; +} +.emojiBullet { + position: absolute; + top: 0; + left: 0; + width: 2rem; + display: inline-block; + text-align: center; +} + +.button { + background: var(--color-btn); + color: var(--color-btn-text); + transition: color 120ms ease-out, background-color 120ms ease-out; + letter-spacing: 1px; + border-radius: 4px; + backdrop-filter: blur(10px); +} + +.button:hover { + background: var(--color-highlight); + color: #fff; +} + +.socialLink { + text-decoration: none; + + display: flex; + justify-content: center; + align-items: center; + + height: 4rem; + padding: 0.75rem 1rem; + + font-size: 1.5rem; + text-align: center; +} + +.text-container { + flex: 1 0 auto; + flex-direction: column; + display: flex; + justify-content: center; + align-items: center; + max-width: var(--column-width); + text-align: center; + font-size: 1.125rem; + padding: 1rem 0; +} + +.inputContainer { + display: flex; + margin-top: 2rem; + min-width: 12rem; + max-width: 100%; +} + +.button, +.inputContainer input { + padding: 0.5rem 1rem; + line-height: 1.5rem; +} + +.inputContainer .button { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.inputContainer input { + background: none; + flex: 1 1 auto; + border: none; + color: #fff; + border: 1px solid var(--color-btn); + border-radius: 4px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.uppercase { + text-transform: uppercase; +} diff --git a/packages/metastream-app/src/components/browser/HomeScreen.tsx b/packages/metastream-app/src/components/browser/HomeScreen.tsx new file mode 100644 index 00000000..54e1df0c --- /dev/null +++ b/packages/metastream-app/src/components/browser/HomeScreen.tsx @@ -0,0 +1,161 @@ +import React, { useState, useRef } from 'react' +import cx from 'classnames' + +import styles from './HomeScreen.css' +import { StorageKey } from 'constants/storage' + +interface Props { + onRequestUrl: (url: string) => void +} + +export const HomeScreen = (props: Props) => { + const [showTips, setShowTips] = useState(!localStorage.getItem(StorageKey.TipsDismissed)) + const urlInputRef = useRef(null) + + const externalHref = (href: string) => `./external.html?href=${encodeURIComponent(href)}` + + return ( +
+
+ +
+

+ From here you can browse the web to find media to share in the session. +

+ +
    +
  • + 🌐 Metastream supports many websites! + Don't see one in the list? Try adding the link anyway! +
  • +
  • + 👥 Everyone in the session will need an + account or subscription if required by a website. +
  • +
  • + 🐛 Is a website not working how you'd + expect? Help us out by{' '} + + reporting the issue on GitHub. + +
  • +
+
+
+
+
+ +
+ + +
+
+
+
+ ) +} diff --git a/packages/metastream-app/src/components/browser/WebBrowser.tsx b/packages/metastream-app/src/components/browser/WebBrowser.tsx index ab7a62c2..2ef6e087 100644 --- a/packages/metastream-app/src/components/browser/WebBrowser.tsx +++ b/packages/metastream-app/src/components/browser/WebBrowser.tsx @@ -1,18 +1,18 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import cx from 'classnames' -import shortid from 'shortid' import styles from './WebBrowser.css' import { WebControls } from 'components/browser/Controls' -import { assetUrl } from 'utils/appUrl' +import { assetUrl, absoluteUrl } from 'utils/appUrl' import { IReactReduxProps } from 'types/redux-thunk' import { Webview } from 'components/Webview' import { sendMediaRequest } from 'lobby/actions/media-request' import { DonateBar } from 'components/account/DonateBar' +import { HomeScreen } from './Homescreen' +import { Portal } from 'components/Portal' -const NONCE = shortid() -const DEFAULT_URL = `${assetUrl('homescreen.html')}?nonce=${NONCE}` +const DEFAULT_URL = absoluteUrl(`${assetUrl('webview.html')}`) interface IProps { className?: string @@ -22,7 +22,13 @@ interface IProps { type PrivateProps = IProps & IReactReduxProps -export class _WebBrowser extends Component { +interface State { + showHomescreen?: boolean +} + +export class _WebBrowser extends Component { + state: State = {} + private webview?: Webview | null private controls?: WebControls | null @@ -100,6 +106,7 @@ export class _WebBrowser extends Component { /> {this.renderContent()} + {this.state.showHomescreen && this.renderHomescreen()} ) } @@ -110,24 +117,28 @@ export class _WebBrowser extends Component { componentRef={this.setupWebview} src={this.initialUrl} className={styles.content} - onMessage={event => { - const { data } = event - if ( - typeof data !== 'object' || - typeof data.type !== 'string' || - typeof data.payload !== 'object' - ) { - return - } - - const { type, payload } = data - if (type === 'add-to-session' && payload.nonce === NONCE) { - this.requestUrl(payload.url, 'homescreen') + onNavigate={url => { + const showHomescreen = url === DEFAULT_URL + if (showHomescreen) { + this.setState({ showHomescreen: false }) // force remount + this.setState({ showHomescreen }) } }} /> ) } + + private renderHomescreen() { + const iframe = this.webview && this.webview.getIFrame() + const wvDoc = iframe && iframe.contentDocument && iframe.contentDocument.body + if (!wvDoc) return null + + return ( + + this.requestUrl(url, 'homescreen')} /> + + ) + } } export const WebBrowser = connect()(_WebBrowser) diff --git a/packages/metastream-app/src/constants/storage.ts b/packages/metastream-app/src/constants/storage.ts index 1ad657eb..d62c4271 100644 --- a/packages/metastream-app/src/constants/storage.ts +++ b/packages/metastream-app/src/constants/storage.ts @@ -2,5 +2,6 @@ export const enum StorageKey { RequestCount = 'requestCount', AutoplayNotice = 'autoplayNotice', HasInteracted = 'hasInteracted', - Login = 'login' + Login = 'login', + TipsDismissed = 'tipsDismissed' } diff --git a/packages/metastream-app/src/index.html b/packages/metastream-app/src/index.html index b5669131..57510ff7 100644 --- a/packages/metastream-app/src/index.html +++ b/packages/metastream-app/src/index.html @@ -1,5 +1,5 @@ - + @@ -10,8 +10,8 @@ -