diff --git a/webroot/img/social-icons.gif b/webroot/img/platform-logos.gif similarity index 100% rename from webroot/img/social-icons.gif rename to webroot/img/platform-logos.gif diff --git a/webroot/js/app-video-only.js b/webroot/js/app-video-only.js index c64e40a38..bba941590 100644 --- a/webroot/js/app-video-only.js +++ b/webroot/js/app-video-only.js @@ -223,7 +223,6 @@ export default class VideoOnly extends Component { } = state; const { logo = TEMP_IMAGE } = configData; - const streamInfoClass = streamOnline ? 'online' : ''; // need? const mainClass = playerActive ? 'online' : ''; @@ -250,10 +249,10 @@ export default class VideoOnly extends Component {
${streamStatusMessage} - ${viewerCount} ${pluralize('viewer', viewerCount)}. + ${viewerCount} ${pluralize('viewer', viewerCount)}.
`); diff --git a/webroot/js/app.js b/webroot/js/app.js index a29b0d3ad..8f9ea0d0f 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -3,7 +3,7 @@ import htm from '/js/web_modules/htm.js'; const html = htm.bind(h); import { OwncastPlayer } from './components/player.js'; -import SocialIconsList from './components/social-icons-list.js'; +import SocialIconsList from './components/platform-logos-list.js'; import UsernameForm from './components/chat/username.js'; import VideoPoster from './components/video-poster.js'; import Chat from './components/chat/chat.js'; @@ -44,6 +44,7 @@ export default class App extends Component { const chatStorage = getLocalStorage(KEY_CHAT_DISPLAYED); this.hasTouchScreen = hasTouchScreen(); + this.windowBlurred = false; this.state = { websocket: new Websocket(), @@ -81,6 +82,8 @@ export default class App extends Component { this.handleUsernameChange = this.handleUsernameChange.bind(this); this.handleFormFocus = this.handleFormFocus.bind(this); this.handleFormBlur = this.handleFormBlur.bind(this); + this.handleWindowBlur = this.handleWindowBlur.bind(this); + this.handleWindowFocus = this.handleWindowFocus.bind(this); this.handleWindowResize = debounce(this.handleWindowResize.bind(this), 250); this.handleOfflineMode = this.handleOfflineMode.bind(this); @@ -102,6 +105,8 @@ export default class App extends Component { componentDidMount() { this.getConfig(); window.addEventListener('resize', this.handleWindowResize); + window.addEventListener('blur', this.handleWindowBlur); + window.addEventListener('focus', this.handleWindowFocus); if (this.hasTouchScreen) { window.addEventListener('orientationchange', this.handleWindowResize); } @@ -123,6 +128,8 @@ export default class App extends Component { clearTimeout(this.disableChatTimer); clearInterval(this.streamDurationTimer); window.removeEventListener('resize', this.handleWindowResize); + window.removeEventListener('blur', this.handleWindowBlur); + window.removeEventListener('focus', this.handleWindowFocus); if (this.hasTouchScreen) { window.removeEventListener('orientationchange', this.handleWindowResize); } @@ -248,6 +255,10 @@ export default class App extends Component { if (this.player.vjsPlayer && this.player.vjsPlayer.paused()) { this.handlePlayerEnded(); } + + if (this.windowBlurred) { + document.title = ` 🔴 ${this.state.configData && this.state.configData.title}`; + } } // play video! @@ -267,6 +278,10 @@ export default class App extends Component { chatInputEnabled: true, streamStatusMessage: MESSAGE_ONLINE, }); + + if (this.windowBlurred) { + document.title = ` 🟢 ${this.state.configData && this.state.configData.title}`; + } } setCurrentStreamDuration() { @@ -335,6 +350,15 @@ export default class App extends Component { }); } + handleWindowBlur() { + this.windowBlurred = true; + } + + handleWindowFocus() { + this.windowBlurred = false; + window.document.title = this.state.configData && this.state.configData.title; + } + render(props, state) { const { chatInputEnabled, @@ -381,7 +405,6 @@ export default class App extends Component { : null; const mainClass = playerActive ? 'online' : ''; - const streamInfoClass = streamOnline ? 'online' : ''; // need? const isPortrait = this.hasTouchScreen && orientation === ORIENTATION_PORTRAIT; const shortHeight = windowHeight <= HEIGHT_SHORT_WIDE && !isPortrait; const singleColMode = windowWidth <= WIDTH_SINGLE_COL && !shortHeight; @@ -462,10 +485,10 @@ export default class App extends Component {
${streamStatusMessage} - ${viewerCount} ${pluralize('viewer', viewerCount)}. + ${viewerCount} ${pluralize('viewer', viewerCount)}.
@@ -514,6 +537,7 @@ export default class App extends Component { websocket=${websocket} username=${username} chatInputEnabled=${chatInputEnabled} + instanceTitle=${title} /> `; diff --git a/webroot/js/components/chat/chat.js b/webroot/js/components/chat/chat.js index eae3fc7cd..a4d5ea6b7 100644 --- a/webroot/js/components/chat/chat.js +++ b/webroot/js/components/chat/chat.js @@ -16,10 +16,10 @@ export default class Chat extends Component { super(props, context); this.state = { - webSocketConnected: true, - messages: [], chatUserNames: [], + messages: [], newMessagesReceived: false, + webSocketConnected: true, }; this.scrollableMessagesContainer = createRef(); @@ -27,16 +27,20 @@ export default class Chat extends Component { this.websocket = null; this.receivedFirstMessages = false; + this.windowBlurred = false; + this.numMessagesSinceBlur = 0; + this.getChatHistory = this.getChatHistory.bind(this); + this.handleNetworkingError = this.handleNetworkingError.bind(this); + this.handleWindowBlur = this.handleWindowBlur.bind(this); + this.handleWindowFocus = this.handleWindowFocus.bind(this); + this.handleWindowResize = debounce(this.handleWindowResize.bind(this), 500); + this.messageListCallback = this.messageListCallback.bind(this); this.receivedWebsocketMessage = this.receivedWebsocketMessage.bind(this); + this.scrollToBottom = this.scrollToBottom.bind(this); + this.submitChat = this.submitChat.bind(this); this.websocketConnected = this.websocketConnected.bind(this); this.websocketDisconnected = this.websocketDisconnected.bind(this); - this.submitChat = this.submitChat.bind(this); - this.submitChat = this.submitChat.bind(this); - this.scrollToBottom = this.scrollToBottom.bind(this); - this.handleWindowResize = debounce(this.handleWindowResize.bind(this), 500); - this.handleNetworkingError = this.handleNetworkingError.bind(this); - this.messageListCallback = this.messageListCallback.bind(this); } componentDidMount() { @@ -45,6 +49,11 @@ export default class Chat extends Component { window.addEventListener('resize', this.handleWindowResize); + if (!this.props.messagesOnly) { + window.addEventListener('blur', this.handleWindowBlur); + window.addEventListener('focus', this.handleWindowFocus); + } + this.messageListObserver = new MutationObserver(this.messageListCallback); this.messageListObserver.observe(this.scrollableMessagesContainer.current, { childList: true }); } @@ -87,6 +96,10 @@ export default class Chat extends Component { } componentWillUnmount() { window.removeEventListener('resize', this.handleWindowResize); + if (!this.props.messagesOnly) { + window.removeEventListener('blur', this.handleWindowBlur); + window.removeEventListener('focus', this.handleWindowFocus); + } this.messageListObserver.disconnect(); } @@ -141,6 +154,7 @@ export default class Chat extends Component { addMessage(message) { const { messages: curMessages } = this.state; + const { messagesOnly } = this.props; // if incoming message has same id as existing message, don't add it const existing = curMessages.filter(function (item) { @@ -157,6 +171,11 @@ export default class Chat extends Component { } this.setState(newState); } + + // if window is blurred and we get a new message, add 1 to title + if (!messagesOnly && message.type === 'CHAT' && this.windowBlurred) { + this.numMessagesSinceBlur += 1; + } } websocketConnected() { @@ -216,6 +235,16 @@ export default class Chat extends Component { this.scrollToBottom(); } + handleWindowBlur() { + this.windowBlurred = true; + } + + handleWindowFocus() { + this.windowBlurred = false; + this.numMessagesSinceBlur = 0; + window.document.title = this.props.instanceTitle; + } + // if the messages list grows in number of child message nodes due to new messages received, scroll to bottom. messageListCallback(mutations) { const numMutations = mutations.length; @@ -234,9 +263,18 @@ export default class Chat extends Component { }); } } + // update document title if window blurred + if (this.numMessagesSinceBlur && !this.props.messagesOnly && this.windowBlurred) { + this.updateDocumentTitle(); + } } }; + updateDocumentTitle() { + const num = this.numMessagesSinceBlur > 10 ? '10+' : this.numMessagesSinceBlur; + window.document.title = `${num} 💬 :: ${this.props.instanceTitle}`; + } + render(props, state) { const { username, messagesOnly, chatInputEnabled } = props; const { messages, chatUserNames, webSocketConnected } = state; diff --git a/webroot/js/components/social-icons-list.js b/webroot/js/components/platform-logos-list.js similarity index 96% rename from webroot/js/components/social-icons-list.js rename to webroot/js/components/platform-logos-list.js index 55156dea8..efb41172a 100644 --- a/webroot/js/components/social-icons-list.js +++ b/webroot/js/components/platform-logos-list.js @@ -2,7 +2,7 @@ import { h, Component } from '/js/web_modules/preact.js'; import htm from '/js/web_modules/htm.js'; const html = htm.bind(h); -import { SOCIAL_PLATFORMS } from '../utils/social.js'; +import { SOCIAL_PLATFORMS } from '../utils/platforms.js'; import { classNames } from '../utils/helpers.js'; function SocialIcon(props) { @@ -60,4 +60,4 @@ export default function (props) { Follow me: ${list} `; -} +} diff --git a/webroot/js/utils/social.js b/webroot/js/utils/platforms.js similarity index 94% rename from webroot/js/utils/social.js rename to webroot/js/utils/platforms.js index 9f42063e5..7bcf0be18 100644 --- a/webroot/js/utils/social.js +++ b/webroot/js/utils/platforms.js @@ -1,4 +1,4 @@ -// x, y pixel psitions of /img/social.gif image. +// x, y pixel positions of /img/platform-logos.gif image. export const SOCIAL_PLATFORMS = { default: { name: "default", diff --git a/webroot/styles/app.css b/webroot/styles/app.css index b5e23888e..65edae579 100644 --- a/webroot/styles/app.css +++ b/webroot/styles/app.css @@ -75,6 +75,12 @@ header { #stream-info span { font-size: .70rem; } +#stream-viewer-count { + display: none; +} +.online #stream-viewer-count { + display: inline; +} /* ************************************************ */ diff --git a/webroot/styles/user-content.css b/webroot/styles/user-content.css index 148f14b0a..ff396d030 100644 --- a/webroot/styles/user-content.css +++ b/webroot/styles/user-content.css @@ -11,7 +11,7 @@ --icon-width: 40px; height: var(--icon-width); width: var(--icon-width); - background-image: url(/img/social-icons.gif); + background-image: url(/img/platform-logos.gif); background-position: calc(var(--imgCol) * var(--icon-width)) calc(var(--imgRow) * var(--icon-width)); transform: scale(.65); }