Browse Source

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
pull/5/head
Gabe Kangas 5 years ago
parent
commit
6d8e8a8849
  1. 123
      webroot/index.html
  2. 39
      webroot/js/app.js
  3. 132
      webroot/js/message.js
  4. 309
      webroot/styles/layout.css
  5. 2
      webroot/vendor/autolink.js

123
webroot/index.html

@ -6,6 +6,8 @@
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">
@ -14,9 +16,6 @@
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>
@ -24,94 +23,120 @@
<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">
<h1>
😈 Owncast
</h1>
<div id="user-options-container" class="flex">
<div id="user-info">
<div id="user-info-display" title="Click to update user name" class="flex">
<img src="https://robohash.org/username123" id="username-avatar" class="rounded-full" />
<span id="username-display">Random Username 123</span>
</div>
<div id="user-info-change">
<input type="text"
id="username-change-input"
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"
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>
</header>
<div id="main-content-container" class="flex">
<!-- LEFT CONTAINER SIDE-->
<div class="flex main-cols left-col">
<div id="video-container" class="flex shadow-md">
<video <video
id="video"
class="video-js vjs-theme-fantasy" class="video-js vjs-theme-fantasy"
id="video"
preload="auto" preload="auto"
poster="/thumbnail.png"
autoplay
controls controls
style="width: 100%; height: 600px;" autoplay
muted
poster="https://goth.land/thumbnail.png"
data-setup='{}' data-setup='{}'
> >
<source src="hls/stream.m3u8" type="application/x-mpegURL"/> <source src="https://goth.land/hls/stream.m3u8" type="application/x-mpegURL"/>
</video> </video>
<div id="app"> </div>
<div id="stream-info">
{{ streamStatus }} {{ viewerCount }} {{ 'viewer' | plural(viewerCount) }}. {{ streamStatus }} {{ viewerCount }} {{ 'viewer' | plural(viewerCount) }}.
Max {{ sessionMaxViewerCount }} {{ 'viewer' | plural(sessionMaxViewerCount) }}, Max {{ sessionMaxViewerCount }} {{ 'viewer' | plural(sessionMaxViewerCount) }},
{{ overallMaxViewerCount }} overall. {{ overallMaxViewerCount }} overall.
</div> </div>
</div> </div>
<div class="w-2/6"> <!-- RIGHT CONTAINER SIDE-->
<div <div class="flex main-cols right-col">
id="messages-container"
class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" <div id="chat-container">
style="height: 60vh; overflow-y: scroll;" <div id="messages-container">
>
<div v-for="(message, index) in messages"> <div v-for="(message, index) in messages">
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"> <div class="message flex">
<div class="flex items-center">
<img <img
v-bind:src="message.image" v-bind:src="message.image"
class="w-10 h-10 rounded-full mr-4 border-black-500" class="message-avatar rounded-full"
style="padding: 5px; background-color: #ececec;"
/> />
<div class="message-content">
<div class="text-sm"> <p class="message-author">{{ message.author }}</p>
<p class="text-700">{{ message.author }}</p> <p class="message-text"v-html="message.formatText()"></p>
<p class="text-gray-600"v-html="message.linkedText()"></p>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<form <div id="message-input-container" class="shadow-md">
id="chatForm" <form id="message-form" class="flex" @submit="submitChatForm">
@submit="submitChatForm"
class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" <input type="hidden" name="inputAuthor" id="self-message-author" v-model="message.author" />
>
<!-- Author --> <!-- Author -->
<label class="control-label" for="inputAuthor">Author</label> <!-- <label class="control-label" for="inputAuthor">Author</label>
<input <input
id="inputAuthor" id="inputAuthor"
type="text" 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" 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" placeholder="Name"
v-model="message.author" v-model="message.author"
/> /> -->
<!-- Body -->
<div>
<label class="control-label" for="inputBody">Message</label>
<div class="controls">
<textarea <textarea
id="inputBody" id="inputBody"
placeholder="Message" placeholder="Message"
v-model="message.body" 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" 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>
</textarea>
</div>
</div>
<div class="control-group"> <div id="message-form-actions" class="flex">
<div class="controls"> <span id="message-form-warning"></span>
<button <button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 rounded"
> > Send
Send
</button> </button>
</div> </div>
</div>
</form> </form>
</div> </div>
</div> </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>

39
webroot/js/app.js

@ -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);
@ -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)

132
webroot/js/message.js

@ -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() {
@ -19,3 +24,126 @@ class Message {
} }
} }
} }
// 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

@ -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;
}
}

2
webroot/vendor/autolink.js vendored

@ -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 +

Loading…
Cancel
Save