0

Merge branch 'web-layout'

* web-layout:
  Show max viewers
  Move to videojs and point to remote video on goth.land
  form functionailties
  progress. implement chat toggling
  fix msg container
  use app file from web-layout
  style message items
  Guard against the infinite that can take place when the ws server goes unavailable
  use css vars
  initial chat form layout
  mobile considerations
  add file
  initial layout
  Support local development of index.html
This commit is contained in:
Gabe Kangas 2020-06-13 22:46:06 -07:00
commit 6d8e8a8849
5 changed files with 570 additions and 107 deletions

View File

@ -6,7 +6,9 @@
href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css"
rel="stylesheet" rel="stylesheet"
/> />
<link href="./styles/layout.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- unpkg : use the latest version of Video.js --> <!-- unpkg : use the latest version of Video.js -->
<link href="//unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet"> <link href="//unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet">
<link <link
@ -14,104 +16,127 @@
rel="stylesheet" rel="stylesheet"
/> />
<script src="//unpkg.com/video.js/dist/video.min.js"></script> <script src="//unpkg.com/video.js/dist/video.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="//vjs.zencdn.net/7.8.2/video.min.js"></script>
<!-- Used for animating the scrolling of the chat div. Can that be done other ways? --> <!-- Used for animating the scrolling of the chat div. Can that be done other ways? -->
<script src="vendor/jquery-2.1.4.min.js"></script> <script src="vendor/jquery-2.1.4.min.js"></script>
<script src="vendor/autolink.js"></script> <script src="vendor/autolink.js"></script>
</head> </head>
<div> <body>
<div class="flex"> <div id="app-container" class="flex no-chat">
<div class="w-4/6"> <header class="flex">
<video <h1>
id="video" 😈 Owncast
class="video-js vjs-theme-fantasy" </h1>
preload="auto"
poster="/thumbnail.png" <div id="user-options-container" class="flex">
autoplay <div id="user-info">
controls <div id="user-info-display" title="Click to update user name" class="flex">
style="width: 100%; height: 600px;" <img src="https://robohash.org/username123" id="username-avatar" class="rounded-full" />
data-setup='{}' <span id="username-display">Random Username 123</span>
> </div>
<source src="hls/stream.m3u8" type="application/x-mpegURL"/>
</video> <div id="user-info-change">
<div id="app"> <input type="text"
{{ streamStatus }} {{ viewerCount }} {{ 'viewer' | plural(viewerCount) }}. id="username-change-input"
Max {{ sessionMaxViewerCount }} {{ 'viewer' | plural(sessionMaxViewerCount) }}, class="appearance-none block w-full bg-gray-200 text-gray-700 border border-black-500 rounded py-1 px-1 leading-tight focus:bg-white"
{{ overallMaxViewerCount }} overall. value="Random Username 123"
maxlength="100"
placeholder="Update username"
>
<button id="button-update-username" class="bg-blue-500 hover:bg-blue-700 text-white py-1 px-1 rounded user-btn">Update</button>
<button id="button-cancel-change" class="bg-gray-900 hover:bg-gray-800 py-1 px-2 rounded user-btn" title="cancel">X</button>
</div>
</div>
<div id="chat-toggle" class="flex">💬</div>
</div> </div>
</div>
<div class="w-2/6"> </header>
<div <div id="main-content-container" class="flex">
id="messages-container" <!-- LEFT CONTAINER SIDE-->
class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" <div class="flex main-cols left-col">
style="height: 60vh; overflow-y: scroll;"
>
<div v-for="(message, index) in messages">
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<div class="flex items-center">
<img
v-bind:src="message.image"
class="w-10 h-10 rounded-full mr-4 border-black-500"
style="padding: 5px; background-color: #ececec;"
/>
<div class="text-sm"> <div id="video-container" class="flex shadow-md">
<p class="text-700">{{ message.author }}</p> <video
<p class="text-gray-600"v-html="message.linkedText()"></p> class="video-js vjs-theme-fantasy"
id="video"
preload="auto"
controls
autoplay
muted
poster="https://goth.land/thumbnail.png"
data-setup='{}'
>
<source src="https://goth.land/hls/stream.m3u8" type="application/x-mpegURL"/>
</video>
</div>
<div id="stream-info">
{{ streamStatus }} {{ viewerCount }} {{ 'viewer' | plural(viewerCount) }}.
Max {{ sessionMaxViewerCount }} {{ 'viewer' | plural(sessionMaxViewerCount) }},
{{ overallMaxViewerCount }} overall.
</div>
</div>
<!-- RIGHT CONTAINER SIDE-->
<div class="flex main-cols right-col">
<div id="chat-container">
<div id="messages-container">
<div v-for="(message, index) in messages">
<div class="message flex">
<img
v-bind:src="message.image"
class="message-avatar rounded-full"
/>
<div class="message-content">
<p class="message-author">{{ message.author }}</p>
<p class="message-text"v-html="message.formatText()"></p>
</div>
</div> </div>
</div> </div>
</div> </div>
<div id="message-input-container" class="shadow-md">
<form id="message-form" class="flex" @submit="submitChatForm">
<input type="hidden" name="inputAuthor" id="self-message-author" v-model="message.author" />
<!-- Author -->
<!-- <label class="control-label" for="inputAuthor">Author</label>
<input
id="inputAuthor"
type="text"
class="appearance-none bg-gray-200 text-gray-700 border border-black-500 rounded py-3 px-4 mb-3 leading-tight focus:outline-none focus:bg-white"
placeholder="Name"
v-model="message.author"
/> -->
<textarea
id="inputBody"
placeholder="Message"
v-model="message.body"
class="appearance-none block w-full bg-gray-200 text-gray-700 border border-black-500 rounded py-2 px-2 my-2 leading-tight focus:bg-white"
></textarea>
<div id="message-form-actions" class="flex">
<span id="message-form-warning"></span>
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 rounded"
> Send
</button>
</div>
</form>
</div>
</div> </div>
</div> </div>
<form
id="chatForm"
@submit="submitChatForm"
class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
>
<!-- Author -->
<label class="control-label" for="inputAuthor">Author</label>
<input
id="inputAuthor"
type="text"
class="appearance-none bg-gray-200 text-gray-700 border border-black-500 rounded py-3 px-4 mb-3 leading-tight focus:outline-none focus:bg-white"
placeholder="Name"
v-model="message.author"
/>
<!-- Body -->
<div>
<label class="control-label" for="inputBody">Message</label>
<div class="controls">
<textarea
id="inputBody"
placeholder="Message"
v-model="message.body"
class="appearance-none block w-full bg-gray-200 text-gray-700 border border-black-500 rounded py-3 px-4 mb-3 leading-tight focus:outline-none focus:bg-white"
>
</textarea>
</div>
</div>
<div class="control-group">
<div class="controls">
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Send
</button>
</div>
</div>
</form>
</div> </div>
</div> </div>
</div>
<script src="js/message.js"></script> <script src="js/message.js"></script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
</body>
</html>

View File

@ -1,19 +1,17 @@
function setupApp() { function setupApp() {
Vue.filter('plural', function (string, count) { Vue.filter('plural', function (string, count) {
if (count === 1) { if (count === 1) {
return string return string;
} else { } else {
return string + "s" return string + "s";
} }
}) })
window.app = new Vue({ window.app = new Vue({
el: "#app", el: "#stream-info",
data: { data: {
streamStatus: "", streamStatus: "",
viewerCount: 0, viewerCount: 0,
sessionMaxViewerCount: 0,
overallMaxViewerCount: 0
}, },
}); });
@ -28,27 +26,30 @@ function setupApp() {
el: "#chatForm", el: "#chatForm",
data: { data: {
message: { message: {
author: localStorage.author || "Viewer" + (Math.floor(Math.random() * 42) + 1), author: "",//localStorage.author || "Viewer" + (Math.floor(Math.random() * 42) + 1),
body: "" body: ""
} }
}, },
methods: { methods: {
submitChatForm: function (e) { submitChatForm: function (e) {
const message = new Message(this.message) const message = new Message(this.message);
message.id = uuidv4() message.id = uuidv4();
localStorage.author = message.author localStorage.author = message.author;
const messageJSON = JSON.stringify(message) const messageJSON = JSON.stringify(message);
window.ws.send(messageJSON) window.ws.send(messageJSON);
e.preventDefault() e.preventDefault();
this.message.body = "" this.message.body = "";
} }
} }
}); });
var appMessagingMisc = new Messaging();
appMessagingMisc.init();
} }
async function getStatus() { async function getStatus() {
const url = "/status"; let url = "https://util.real-ity.com:8042/status";
try { try {
const response = await fetch(url); const response = await fetch(url);
@ -60,7 +61,7 @@ async function getStatus() {
app.viewerCount = status.viewerCount app.viewerCount = status.viewerCount
app.sessionMaxViewerCount = status.sessionMaxViewerCount app.sessionMaxViewerCount = status.sessionMaxViewerCount
app.overallMaxViewerCount = status.overallMaxViewerCount app.overallMaxViewerCount = status.overallMaxViewerCount
} catch (e) { } catch (e) {
app.streamStatus = "Stream server is offline." app.streamStatus = "Stream server is offline."
app.viewerCount = 0 app.viewerCount = 0
@ -68,12 +69,12 @@ async function getStatus() {
} }
var websocketReconnectTimer var websocketReconnectTimer;
function setupWebsocket() { function setupWebsocket() {
clearTimeout(websocketReconnectTimer) clearTimeout(websocketReconnectTimer)
const protocol = location.protocol == "https:" ? "wss" : "ws" const protocol = location.protocol == "https:" ? "wss" : "ws"
var ws = new WebSocket(protocol + "://" + location.host + "/entry") var ws = new WebSocket("wss://util.real-ity.com:8042/entry")
ws.onmessage = (e) => { ws.onmessage = (e) => {
const model = JSON.parse(e.data) const model = JSON.parse(e.data)
@ -108,10 +109,10 @@ function setupWebsocket() {
setupApp() setupApp()
getStatus() getStatus()
setupWebsocket() setupWebsocket()
setInterval(getStatus, 5000) // setInterval(getStatus, 5000)
function scrollSmoothToBottom(id) { function scrollSmoothToBottom(id) {
const div = document.getElementById(id) const div = document.getElementById(id);
$('#' + id).animate({ $('#' + id).animate({
scrollTop: div.scrollHeight - div.clientHeight scrollTop: div.scrollHeight - div.clientHeight
}, 500) }, 500)

View File

@ -6,8 +6,13 @@ class Message {
this.id = model.id this.id = model.id
} }
linkedText() { addNewlines(str) {
return autoLink(this.body, { embed: true }) return str.replace(/(?:\r\n|\r|\n)/g, '<br />');
}
formatText() {
var linked = autoLink(this.body, { embed: true });
return this.addNewlines(linked);
} }
toModel() { toModel() {
@ -18,4 +23,127 @@ class Message {
id: this.id id: this.id
} }
} }
}
// convert newlines to <br>s
class Messaging {
constructor() {
this.chatDisplayed = false;
this.username = "";
this.avatarSource = "https://robohash.org/";
this.messageCharCount = 0;
this.maxMessageLength = 500;
this.maxMessageBuffer = 20;
this.tagChatToggle = document.querySelector("#chat-toggle");
this.tagUserInfoDisplay = document.querySelector("#user-info-display");
this.tagUserInfoChanger = document.querySelector("#user-info-change");
this.tagUsernameDisplay = document.querySelector("#username-display");
this.imgUsernameAvatar = document.querySelector("#username-avatar");
this.tagMessageAuthor = document.querySelector("#self-message-author");
this.tagMessageFormWarning = document.querySelector("#message-form-warning");
this.tagAppContainer = document.querySelector("#app-container");
this.inputChangeUserName = document.querySelector("#username-change-input");
this.btnUpdateUserName = document.querySelector("#button-update-username");
this.btnCancelUpdateUsername = document.querySelector("#button-cancel-change");
this.formMessageInput = document.querySelector("#inputBody");
}
init() {
this.tagChatToggle.addEventListener("click", this.handleChatToggle);
this.tagUsernameDisplay.addEventListener("click", this.handleShowChangeNameForm);
this.btnUpdateUserName.addEventListener("click", this.handleUpdateUsername);
this.btnCancelUpdateUsername.addEventListener("click", this.handleHideChangeNameForm);
this.inputChangeUserName.addEventListener("keydown", this.handleUsernameKeydown);
this.formMessageInput.addEventListener("keydown", this.handleMessageInputKeydown);
}
handleChatToggle = () => {
if (this.chatDisplayed) {
this.tagAppContainer.className = "flex no-chat";
this.chatDisplayed = false;
} else {
this.tagAppContainer.className = "flex";
this.chatDisplayed = true;
}
}
handleShowChangeNameForm = () => {
this.tagUserInfoDisplay.style.display = "none";
this.tagUserInfoChanger.style.display = "flex";
}
handleHideChangeNameForm = () => {
this.tagUserInfoDisplay.style.display = "flex";
this.tagUserInfoChanger.style.display = "none";
}
handleUpdateUsername = () => {
var newValue = this.inputChangeUserName.value;
newValue = newValue.trim();
// do other string cleanup?
if (newValue) {
this.userName = newValue;
this.inputChangeUserName.value = newValue;
this.tagMessageAuthor.innerText = newValue;
this.tagUsernameDisplay.innerText = newValue;
this.imgUsernameAvatar.src = this.avatarSource + newValue;
}
this.handleHideChangeNameForm();
}
handleUsernameKeydown = event => {
if (event.keyCode === 13) { // enter
this.handleUpdateUsername();
} else if (event.keyCode === 27) { // esc
this.handleHideChangeNameForm();
}
}
handleMessageInputKeydown = event => {
var okCodes = [37,38,39,40,16,91,18,46,8];
var value = this.formMessageInput.value.trim();
var numCharsLeft = this.maxMessageLength - value.length;
if (event.keyCode === 13) { // enter
if (!this.prepNewLine) {
// submit()
event.preventDefault();
// clear out things.
this.formMessageInput.value = "";
this.tagMessageFormWarning.innerText = "";
return;
}
this.prepNewLine = false;
} else {
this.prepNewLine = false;
}
if (event.keyCode === 16 || event.keyCode === 17) { // ctrl, shift
this.prepNewLine = true;
}
if (numCharsLeft <= this.maxMessageBuffer) {
this.tagMessageFormWarning.innerText = numCharsLeft + " chars left";
if (numCharsLeft <= 0 && !okCodes.includes(event.keyCode)) {
event.preventDefault();
return;
}
} else {
this.tagMessageFormWarning.innerText = "";
}
}
} }

309
webroot/styles/layout.css Normal file
View File

@ -0,0 +1,309 @@
/* variables */
:root {
--header-height: 3em;
--right-col-width: 24em;
--chat-bg-color: rgba(11,0,33,.95);
--header-bg-color: rgba(20,0,40,1);
}
body {
font-size: 14px;
background-color: #666;
}
::-webkit-scrollbar {
width: 0px;
background: transparent;
}
#app-container {
width: 100%;
flex-direction: column;
justify-content: flex-start;
position: relative;
color: white;
}
header {
position: fixed;
width: 100%;
height: var(--header-height);
top: 0;
left: 0;
background-color: var(--header-bg-color);
z-index: 10;
flex-direction: row;
justify-content: space-between;
}
header h1 {
font-size: 1.25em;
font-weight: 100;
letter-spacing: 1.2;
text-transform: uppercase;
color: #ddd;
padding: .5em;
white-space: nowrap;
}
#chat-toggle {
cursor: pointer;
background-color: #555;
text-align: center;
height: 100%;
width: 3em;
justify-content: center;
align-items: center;
}
#chat-toggle:hover {
background-color: #666;
}
/* ************************************************8 */
#user-options-container {
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
#user-info-display {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
cursor: pointer;
padding: .5em 1em;
}
#username-avatar {
height: 1.75em;
width: 1.75em;
margin-right: .5em;
border: 1px solid rgba(255,255,255,.25)
}
#username-display {
font-weight: bold;
font-size: .75em;
color: #516FEB
}
#user-info-display:hover {
transition: opacity .2s;
opacity: .75;
}
#user-info-change {
display: none;
justify-content: flex-end;
align-items: center;
padding: .25em;
}
#username-change-input {
font-size: .75em;
}
#button-update-username {
font-size: .65em;
text-transform: uppercase;
height: 2.5em;
}
#button-cancel-change {
color: rgba(255,255,255,.5);
cursor: pointer;
height: 2.5em;
font-size: .65em;
}
.user-btn {
margin: 0 .25em;
}
/* ************************************************8 */
#main-content-container {
width: 100%;
flex-direction: row;
position: relative;
margin-top: var(--header-height);
}
.main-cols {
flex-direction: column;
justify-content: flex-start;
position: relative;
}
.left-col {
width: calc(100vw - var(--right-col-width));
}
/* ************************************************8 */
#video-container {
width: 100%;
height: auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
#video-container video {
width: 100%;
display: block;
}
#stream-info {
padding: .5em;
text-align: center;
font-family: monospace;
font-size: .75em;
background-color: rgba(0,0,0,.5);
border-bottom: 1px solid black;
}
/* ************************************************8 */
#chat-container {
position: fixed;
z-index: 9;
right: 0;
height: 100%;
width: var(--right-col-width);
background-color: var(--chat-bg-color);
height: calc(100vh - var(--header-height));
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
#messages-container {
overflow: auto;
padding: 1em 0;
}
#message-input-container {
width: 100%;
border-top: 1px solid #eee;
padding: 1em;
background-color: #334;
}
#message-form {
flex-direction: column;
align-items: flex-end;
margin-bottom: 0;
}
#message-form-actions {
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
}
#message-form-warning {
font-size: .75em;
color: red;
}
/* ************************************************8 */
.message {
padding: .85em;
align-items: flex-start;
}
.message-avatar {
height: 2.5em;
width: 2.5em;
margin-right: .75em;
background-color: rgba(0,0,0, .75);
}
.message-content {
font-size: .85em;
}
.message-content a {
color: #6699cc;
}
.message-content a:hover {
text-decoration: underline;
}
.message-author {
font-weight: 600;
}
.message-text {
color: #ccc;
font-weight: 100;
}
/* ************************************************8 */
.no-chat .left-col {
width: 100vw;
}
.no-chat .right-col {
display: none;
}
.no-chat #chat-toggle {
opacity: .5;
}
/* ************************************************8 */
@media screen and (max-width: 860px) {
:root {
--right-col-width: 20em;
}
#chat-container {
width: var(--right-col-width);
}
.left-col {
width: calc(100vw - var(--right-col-width));
}
}
@media screen and (max-width: 640px ) and (orientation: portrait) {
#main-content-container {
flex-direction: column;
justify-content: space-between;
height: calc(100vh - var(--header-height));
}
.main-cols {
width: 100vw;
}
.left-col {
flex-direction: column;
justify-content: stretch;
}
.right-col {
overflow: hidden;
}
#info {
display: none;
overflow: auto;
height: auto;
}
#chat-container {
width: 100%;
height: 100%;
position: relative;
height: auto;
}
.no-chat .left-col {
height: 100%;
}
.no-chat .right-col {
display: none;
}
.no-chat #info {
display: block;
}
}

View File

@ -113,7 +113,7 @@ AutoLink.prototype = {
var text = this.options.removeHTTP ? removeHTTP(match) : match var text = this.options.removeHTTP ? removeHTTP(match) : match
return ( return (
p1 + p1 +
'<a href="' + '<a target="_blank" href="' +
match + match +
'"' + '"' +
this.attrs + this.attrs +