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. 189
      webroot/index.html
  2. 41
      webroot/js/app.js
  3. 132
      webroot/js/message.js
  4. 309
      webroot/styles/layout.css
  5. 2
      webroot/vendor/autolink.js

189
webroot/index.html

@ -6,7 +6,9 @@ @@ -6,7 +6,9 @@
href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css"
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 -->
<link href="//unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet">
<link
@ -14,104 +16,127 @@ @@ -14,104 +16,127 @@
rel="stylesheet"
/>
<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? -->
<script src="vendor/jquery-2.1.4.min.js"></script>
<script src="vendor/autolink.js"></script>
</head>
<div>
<div class="flex">
<div class="w-4/6">
<video
id="video"
class="video-js vjs-theme-fantasy"
preload="auto"
poster="/thumbnail.png"
autoplay
controls
style="width: 100%; height: 600px;"
data-setup='{}'
>
<source src="hls/stream.m3u8" type="application/x-mpegURL"/>
</video>
<div id="app">
{{ streamStatus }} {{ viewerCount }} {{ 'viewer' | plural(viewerCount) }}.
Max {{ sessionMaxViewerCount }} {{ 'viewer' | plural(sessionMaxViewerCount) }},
{{ overallMaxViewerCount }} overall.
</div>
</div>
<body>
<div id="app-container" class="flex no-chat">
<header class="flex">
<h1>
😈 Owncast
</h1>
<div class="w-2/6">
<div
id="messages-container"
class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
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 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 class="text-sm">
<p class="text-700">{{ message.author }}</p>
<p class="text-gray-600"v-html="message.linkedText()"></p>
</div>
</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>
<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"
/>
</header>
<div id="main-content-container" class="flex">
<!-- LEFT CONTAINER SIDE-->
<div class="flex main-cols left-col">
<!-- 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 id="video-container" class="flex shadow-md">
<video
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 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 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 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>
</form>
</div>
</div>
</div>
</div>
<script src="js/message.js"></script>
<script src="js/app.js"></script>
<script src="js/message.js"></script>
<script src="js/app.js"></script>
</body>
</html>

41
webroot/js/app.js

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

132
webroot/js/message.js

@ -6,8 +6,13 @@ class Message { @@ -6,8 +6,13 @@ class Message {
this.id = model.id
}
linkedText() {
return autoLink(this.body, { embed: true })
addNewlines(str) {
return str.replace(/(?:\r\n|\r|\n)/g, '<br />');
}
formatText() {
var linked = autoLink(this.body, { embed: true });
return this.addNewlines(linked);
}
toModel() {
@ -18,4 +23,127 @@ class Message { @@ -18,4 +23,127 @@ class Message {
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

@ -0,0 +1,309 @@ @@ -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 = { @@ -113,7 +113,7 @@ AutoLink.prototype = {
var text = this.options.removeHTTP ? removeHTTP(match) : match
return (
p1 +
'<a href="' +
'<a target="_blank" href="' +
match +
'"' +
this.attrs +

Loading…
Cancel
Save