From 136a5759734a0fe8c89c789ad15c4e2349c98e84 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Thu, 20 Aug 2020 12:59:07 -0700 Subject: [PATCH] wip.. initial setup for preact integration into main app --- webroot/index2.html | 76 +++++++++ webroot/js/app2.js | 310 ++++++++++++++++++++++++++++++++++ webroot/js/chat/chat-input.js | 2 +- webroot/js/chat/standalone.js | 1 + webroot/js/utils.js | 2 + 5 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 webroot/index2.html create mode 100644 webroot/js/app2.js diff --git a/webroot/index2.html b/webroot/index2.html new file mode 100644 index 000000000..5ef518b28 --- /dev/null +++ b/webroot/index2.html @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + diff --git a/webroot/js/app2.js b/webroot/js/app2.js new file mode 100644 index 000000000..8ee9c1aaf --- /dev/null +++ b/webroot/js/app2.js @@ -0,0 +1,310 @@ +import { h, Component, Fragment } from 'https://unpkg.com/preact?module'; +import htm from 'https://unpkg.com/htm?module'; +const html = htm.bind(h); + + +import UsernameForm from './chat/username.js'; +import Chat from './chat/chat.js'; +import Websocket from './websocket.js'; + +import { getLocalStorage, generateAvatar, generateUsername, URL_OWNCAST, URL_CONFIG, URL_STATUS, addNewlines } from './utils.js'; +import { KEY_USERNAME, KEY_AVATAR, } from './utils/chat.js'; + +export default class App extends Component { + constructor(props, context) { + super(props, context); + + this.state = { + websocket: new Websocket(), + chatEnabled: true, // always true for standalone chat + username: getLocalStorage(KEY_USERNAME) || generateUsername(), + userAvatarImage: getLocalStorage(KEY_AVATAR) || generateAvatar(`${this.username}${Date.now()}`), + + streamStatus: null, + player: null, + configData: {}, + }; + + // timers + this.playerRestartTimer = null; + this.offlineTimer = null; + this.statusTimer = null; + this.disableChatTimer = null; + this.streamDurationTimer = null; + + this.handleUsernameChange = this.handleUsernameChange.bind(this); + this.getConfig = this.getConfig.bind(this); + this.getStreamStatus = this.getStreamStatus.bind(this); + this.getExtraUserContent = this.getExtraUserContent.bind(this); + + } + + componentDidMount() { + this.getConfig(); + + // DO LATER.. + // this.player = new OwncastPlayer(); + // this.player.setupPlayerCallbacks({ + // onReady: this.handlePlayerReady, + // onPlaying: this.handlePlayerPlaying, + // onEnded: this.handlePlayerEnded, + // onError: this.handlePlayerError, + // }); + // this.player.init(); + } + + // fetch /config data + getConfig() { + fetch(URL_CONFIG) + .then(response => { + if (!response.ok) { + throw new Error(`Network response was not ok ${response.ok}`); + } + return response.json(); + }) + .then(json => { + this.setConfigData(json); + }) + .catch(error => { + this.handleNetworkingError(`Fetch config: ${error}`); + }); + } + + // fetch stream status + getStreamStatus() { + fetch(URL_STATUS) + .then(response => { + if (!response.ok) { + throw new Error(`Network response was not ok ${response.ok}`); + } + return response.json(); + }) + .then(json => { + this.updateStreamStatus(json); + }) + .catch(error => { + this.handleOfflineMode(); + this.handleNetworkingError(`Stream status: ${error}`); + }); + } + + // fetch content.md + getExtraUserContent(path) { + fetch(path) + .then(response => { + if (!response.ok) { + throw new Error(`Network response was not ok ${response.ok}`); + } + return response.text(); + }) + .then(text => { + const descriptionHTML = new showdown.Converter().makeHtml(text); + this.vueApp.extraUserContent = descriptionHTML; + }) + .catch(error => { + this.handleNetworkingError(`Fetch extra content: ${error}`); + }); + } + + + setConfigData(data = {}) { + const { title, extraUserInfoFileName, summary } = data; + + window.document.title = title; + if (extraUserInfoFileName) { + this.getExtraUserContent(extraUserInfoFileName); + } + + this.setState({ + configData: { + ...data, + summary: summary && addNewlines(summary), + }, + }); + } + + // handle UI things from stream status result + updateStreamStatus(status = {}) { + if (!status) { + return; + } + // update UI + this.vueApp.viewerCount = status.viewerCount; + this.vueApp.sessionMaxViewerCount = status.sessionMaxViewerCount; + this.vueApp.overallMaxViewerCount = status.overallMaxViewerCount; + + this.lastDisconnectTime = status.lastDisconnectTime; + + if (!this.streamStatus) { + // display offline mode the first time we get status, and it's offline. + if (!status.online) { + this.handleOfflineMode(); + } else { + this.handleOnlineMode(); + } + } else { + if (status.online && !this.streamStatus.online) { + // stream has just come online. + this.handleOnlineMode(); + } else if (!status.online && this.streamStatus.online) { + // stream has just flipped offline. + this.handleOfflineMode(); + } + } + + // keep a local copy + this.streamStatus = status; + + if (status.online) { + // only do this if video is paused, so no unnecessary img fetches + if (this.player.vjsPlayer && this.player.vjsPlayer.paused()) { + this.player.setPoster(); + } + } + } + + // stop status timer and disable chat after some time. + handleOfflineMode() { + this.vueApp.isOnline = false; + clearInterval(this.streamDurationTimer); + this.vueApp.streamStatus = MESSAGE_OFFLINE; + if (this.streamStatus) { + const remainingChatTime = TIMER_DISABLE_CHAT_AFTER_OFFLINE - (Date.now() - new Date(this.lastDisconnectTime)); + const countdown = (remainingChatTime < 0) ? 0 : remainingChatTime; + this.disableChatTimer = setTimeout(this.messagingInterface.disableChat, countdown); + } + } + + // play video! + handleOnlineMode() { + this.vueApp.playerOn = true; + this.vueApp.isOnline = true; + this.vueApp.streamStatus = MESSAGE_ONLINE; + + this.player.startPlayer(); + clearTimeout(this.disableChatTimer); + this.disableChatTimer = null; + this.messagingInterface.enableChat(); + + this.streamDurationTimer = + setInterval(this.setCurrentStreamDuration, TIMER_STREAM_DURATION_COUNTER); + } + + + handleUsernameChange(newName, newAvatar) { + this.setState({ + username: newName, + userAvatarImage: newAvatar, + }); + } + + handleChatToggle() { + const { chatEnabled: curChatEnabled } = this.state; + this.setState({ + chatEnabled: !curChatEnabled, + }); + } + + handleNetworkingError(error) { + console.log(`>>> App Error: ${error}`); + } + + render(props, state) { + const { username, userAvatarImage, websocket, configData } = state; + const { + version: appVersion, + logo = {}, + socialHandles, + name: streamnerName, + summary, + tags, + title, + } = configData; + const { small: smallLogo, large: largeLogo } = logo; + + const bgLogo = { backgroundImage: `url(${smallLogo})` }; + const bgLogoLarge = { backgroundImage: `url(${largeLogo})` }; + + // not needed for standalone, just messages only. remove later. + return ( + html` +
+
+
+

+ + + + ${title} +

+ + <${UsernameForm} + username=${username} + userAvatarImage=${userAvatarImage} + handleUsernameChange=${this.handleUsernameChange} + handleChatToggle=${this.handleChatToggle} + /> + +
+
+ +
+
+ +
+ + +
+ {{ streamStatus }} + {{ viewerCount }} {{ 'viewer' | plural(viewerCount) }}. + Max {{ sessionMaxViewerCount }} {{ 'viewer' | plural(sessionMaxViewerCount) }}. + {{ overallMaxViewerCount }} overall. +
+
+ +
+ {{streamerName}} + +
{{extraUserContent}}
+
+ + +
+ + <${Chat} + websocket=${websocket} + username=${username} + userAvatarImage=${userAvatarImage} + chatEnabled + /> + + + `); + } +} + diff --git a/webroot/js/chat/chat-input.js b/webroot/js/chat/chat-input.js index b6f3bf189..7d9fed2ca 100644 --- a/webroot/js/chat/chat-input.js +++ b/webroot/js/chat/chat-input.js @@ -111,7 +111,7 @@ export default class ChatInput extends Component { return false; } - const partial = inputHTML.substring(at + 1, position).trim(); + let partial = inputHTML.substring(at + 1, position).trim(); if (partial === this.suggestion) { partial = this.partial; diff --git a/webroot/js/chat/standalone.js b/webroot/js/chat/standalone.js index 90d203e62..14f0fd951 100644 --- a/webroot/js/chat/standalone.js +++ b/webroot/js/chat/standalone.js @@ -54,6 +54,7 @@ export default class StandaloneChat extends Component { `); } + // not needed for standalone, just messages only. remove later. return ( html` <${Fragment}> diff --git a/webroot/js/utils.js b/webroot/js/utils.js index 26da002e1..238bd8f58 100644 --- a/webroot/js/utils.js +++ b/webroot/js/utils.js @@ -1,6 +1,8 @@ export const URL_STATUS = `/status`; export const URL_CHAT_HISTORY = `/chat`; export const URL_CUSTOM_EMOJIS = `/emoji`; +export const URL_CONFIG = `/config`; + // TODO: This directory is customizable in the config. So we should expose this via the config API. export const URL_STREAM = `/hls/stream.m3u8`; export const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`;