Browse Source
- set up standalone static page and message related components - start separating out css into smaller more manageable files - start separating out utils into smaller modular files - renaming some files for consistencypull/120/head
11 changed files with 578 additions and 27 deletions
@ -0,0 +1,117 @@ |
|||||||
|
import { h, Component, render } from 'https://unpkg.com/preact?module'; |
||||||
|
import htm from 'https://unpkg.com/htm?module'; |
||||||
|
// Initialize htm with Preact
|
||||||
|
const html = htm.bind(h); |
||||||
|
|
||||||
|
import SOCKET_MESSAGE_TYPES from '../utils/socketMessageTypes.js'; |
||||||
|
|
||||||
|
|
||||||
|
export default class Chat extends Component { |
||||||
|
constructor(props, context) { |
||||||
|
super(props, context); |
||||||
|
|
||||||
|
this.messageCharCount = 0; |
||||||
|
this.maxMessageLength = 500; |
||||||
|
this.maxMessageBuffer = 20; |
||||||
|
|
||||||
|
this.state = { |
||||||
|
inputEnabled: false, |
||||||
|
messages: [], |
||||||
|
|
||||||
|
chatUserNames: [], |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
componentDidMount() { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
componentDidUpdate(prevProps) { |
||||||
|
const { username: prevName } = prevProps; |
||||||
|
const { username, userAvatarImage } = this.props; |
||||||
|
// if username updated, send a message
|
||||||
|
if (prevName !== username) { |
||||||
|
this.sendUsernameChange(prevName, username, userAvatarImage); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
sendUsernameChange(oldName, newName, image) { |
||||||
|
const nameChange = { |
||||||
|
type: SOCKET_MESSAGE_TYPES.NAME_CHANGE, |
||||||
|
oldName: oldName, |
||||||
|
newName: newName, |
||||||
|
image: image, |
||||||
|
}; |
||||||
|
this.send(nameChange); |
||||||
|
} |
||||||
|
|
||||||
|
render() { |
||||||
|
const { username, userAvatarImage } = this.state; |
||||||
|
return ( |
||||||
|
html` |
||||||
|
<section id="chat-container-wrap" class="flex"> |
||||||
|
<div id="chat-container" class="bg-gray-800"> |
||||||
|
<div id="messages-container"> |
||||||
|
messages... |
||||||
|
<!-- <div v-for="message in messages" v-cloak> |
||||||
|
<div class="message flex" v-if="message.type === 'CHAT'"> |
||||||
|
<div class="message-avatar rounded-full flex items-center justify-center" v-bind:style="{ backgroundColor: message.userColor() }"> |
||||||
|
<img |
||||||
|
v-bind:src="message.image" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div class="message-content"> |
||||||
|
<p class="message-author text-white font-bold">{{ message.author }}</p> |
||||||
|
<p class="message-text text-gray-400 font-thin " v-html="message.formatText()"></p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="message flex" v-else-if="message.type === 'NAME_CHANGE'"> |
||||||
|
<img |
||||||
|
class="mr-2" |
||||||
|
width="30px" |
||||||
|
v-bind:src="message.image" |
||||||
|
/> |
||||||
|
<div class="text-white text-center"> |
||||||
|
<span class="font-bold">{{ message.oldName }}</span> is now known as <span class="font-bold">{{ message.newName }}</span>. |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> --> |
||||||
|
</div> |
||||||
|
|
||||||
|
|
||||||
|
<div id="message-input-container" class="shadow-md bg-gray-900 border-t border-gray-700 border-solid"> |
||||||
|
<form id="message-form" class="flex"> |
||||||
|
|
||||||
|
<input type="hidden" name="inputAuthor" id="self-message-author" value=${username} /> |
||||||
|
|
||||||
|
<textarea |
||||||
|
disabled |
||||||
|
id="message-body-form" |
||||||
|
placeholder="Message" |
||||||
|
class="appearance-none block w-full bg-gray-200 text-gray-700 border border-black-500 rounded py-2 px-2 my-2 focus:bg-white" |
||||||
|
></textarea> |
||||||
|
|
||||||
|
<div id="message-form-actions" class="flex"> |
||||||
|
<span id="message-form-warning" class="text-red-600 text-xs"></span> |
||||||
|
<button |
||||||
|
id="button-submit-message" |
||||||
|
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 rounded" |
||||||
|
> Chat |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
`);
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,52 @@ |
|||||||
|
import { h, Component, createRef } from 'https://unpkg.com/preact?module'; |
||||||
|
import htm from 'https://unpkg.com/htm?module'; |
||||||
|
// Initialize htm with Preact
|
||||||
|
const html = htm.bind(h); |
||||||
|
|
||||||
|
import {messageBubbleColorForString } from '../utils/user-colors.js'; |
||||||
|
|
||||||
|
export default class Message extends Component { |
||||||
|
constructor(props, context) { |
||||||
|
super(props, context); |
||||||
|
|
||||||
|
this.state = { |
||||||
|
displayForm: false, |
||||||
|
}; |
||||||
|
|
||||||
|
this.handleKeydown = this.handleKeydown.bind(this); |
||||||
|
this.handleDisplayForm = this.handleDisplayForm.bind(this); |
||||||
|
this.handleHideForm = this.handleHideForm.bind(this); |
||||||
|
this.handleUpdateUsername = this.handleUpdateUsername.bind(this); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
render(props) { |
||||||
|
const { message, type } = props; |
||||||
|
const { image, author, text } = message; |
||||||
|
|
||||||
|
const styles = { |
||||||
|
info: { |
||||||
|
display: displayForm || narrowSpace ? 'none' : 'flex', |
||||||
|
}, |
||||||
|
form: { |
||||||
|
display: displayForm ? 'flex' : 'none', |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
html` |
||||||
|
<div class="message flex"> |
||||||
|
<div class="message-avatar rounded-full flex items-center justify-center" v-bind:style="{ backgroundColor: message.userColor() }"> |
||||||
|
<img |
||||||
|
v-bind:src="message.image" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div class="message-content"> |
||||||
|
<p class="message-author text-white font-bold">{{ message.author }}</p> |
||||||
|
<p class="message-text text-gray-400 font-thin " v-html="message.formatText()"></p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`);
|
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,54 @@ |
|||||||
|
import { html, Component } from "https://unpkg.com/htm/preact/index.mjs?module"; |
||||||
|
|
||||||
|
// import { h, Component, render } from 'https://unpkg.com/preact?module';
|
||||||
|
// import htm from 'https://unpkg.com/htm?module';
|
||||||
|
// Initialize htm with Preact
|
||||||
|
// const html = htm.bind(h);
|
||||||
|
|
||||||
|
import UserInfo from './user-info.js'; |
||||||
|
import Chat from './chat.js'; |
||||||
|
|
||||||
|
import { getLocalStorage, generateAvatar, generateUsername } from '../utils.js'; |
||||||
|
import { KEY_USERNAME, KEY_AVATAR } from '../utils/chat.js'; |
||||||
|
|
||||||
|
export class StandaloneChat extends Component { |
||||||
|
constructor(props, context) { |
||||||
|
super(props, context); |
||||||
|
|
||||||
|
this.state = { |
||||||
|
chatEnabled: true, // always true for standalone chat
|
||||||
|
username: getLocalStorage(KEY_USERNAME) || generateUsername(), |
||||||
|
userAvatarImage: getLocalStorage(KEY_AVATAR) || generateAvatar(`${this.username}${Date.now()}`), |
||||||
|
}; |
||||||
|
|
||||||
|
this.handleUsernameChange = this.handleUsernameChange.bind(this); |
||||||
|
} |
||||||
|
|
||||||
|
handleUsernameChange(newName, newAvatar) { |
||||||
|
this.setState({ |
||||||
|
username: newName, |
||||||
|
userAvatarImage: newAvatar, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
handleChatToggle() { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
render(props, state) { |
||||||
|
const { username, userAvatarImage } = state; |
||||||
|
return ( |
||||||
|
html` |
||||||
|
<div class="flex"> |
||||||
|
<${UserInfo} |
||||||
|
username=${username} |
||||||
|
userAvatarImage=${userAvatarImage} |
||||||
|
handleUsernameChange=${this.handleUsernameChange} |
||||||
|
handleChatToggle=${this.handleChatToggle} |
||||||
|
/> |
||||||
|
<${Chat} username=${username} userAvatarImage=${userAvatarImage} chatEnabled /> |
||||||
|
</div> |
||||||
|
`);
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,108 @@ |
|||||||
|
import { h, Component, createRef } from 'https://unpkg.com/preact?module'; |
||||||
|
import htm from 'https://unpkg.com/htm?module'; |
||||||
|
// Initialize htm with Preact
|
||||||
|
const html = htm.bind(h); |
||||||
|
|
||||||
|
import { generateAvatar, setLocalStorage } from '../utils.js'; |
||||||
|
import { KEY_USERNAME, KEY_AVATAR } from '../utils/chat.js'; |
||||||
|
|
||||||
|
|
||||||
|
export default class UserInfo extends Component { |
||||||
|
constructor(props, context) { |
||||||
|
super(props, context); |
||||||
|
|
||||||
|
this.state = { |
||||||
|
displayForm: false, |
||||||
|
}; |
||||||
|
|
||||||
|
this.textInput = createRef(); |
||||||
|
|
||||||
|
this.handleKeydown = this.handleKeydown.bind(this); |
||||||
|
this.handleDisplayForm = this.handleDisplayForm.bind(this); |
||||||
|
this.handleHideForm = this.handleHideForm.bind(this); |
||||||
|
this.handleUpdateUsername = this.handleUpdateUsername.bind(this); |
||||||
|
} |
||||||
|
|
||||||
|
handleDisplayForm() { |
||||||
|
this.setState({ |
||||||
|
displayForm: true, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
handleHideForm() { |
||||||
|
this.setState({ |
||||||
|
displayForm: false, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
handleKeydown(event) { |
||||||
|
if (event.keyCode === 13) { // enter
|
||||||
|
this.handleUpdateUsername(); |
||||||
|
} else if (event.keyCode === 27) { // esc
|
||||||
|
this.handleHideForm(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
handleUpdateUsername() { |
||||||
|
const { username: curName, handleUsernameChange } = this.props; |
||||||
|
let newName = this.textInput.current.value; |
||||||
|
newName = newName.trim(); |
||||||
|
if (newName !== '' && newName !== curName) { |
||||||
|
const newAvatar = generateAvatar(`${newName}${Date.now()}`); |
||||||
|
setLocalStorage(KEY_USERNAME, newName); |
||||||
|
setLocalStorage(KEY_AVATAR, newAvatar); |
||||||
|
if (handleUsernameChange) { |
||||||
|
handleUsernameChange(newName, newAvatar); |
||||||
|
} |
||||||
|
this.handleHideForm(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
render(props, state) { |
||||||
|
const { username, userAvatarImage, handleChatToggle } = props; |
||||||
|
const { displayForm } = state; |
||||||
|
|
||||||
|
const narrowSpace = document.body.clientWidth < 640; |
||||||
|
const styles = { |
||||||
|
info: { |
||||||
|
display: displayForm || narrowSpace ? 'none' : 'flex', |
||||||
|
}, |
||||||
|
form: { |
||||||
|
display: displayForm ? 'flex' : 'none', |
||||||
|
}, |
||||||
|
}; |
||||||
|
if (narrowSpace) { |
||||||
|
styles.form.display = 'inline-block'; |
||||||
|
} |
||||||
|
return ( |
||||||
|
html` |
||||||
|
<div id="user-options-container" class="flex"> |
||||||
|
<div id="user-info"> |
||||||
|
<div id="user-info-display" style=${styles.info} title="Click to update user name" class="flex" onClick=${this.handleDisplayForm}> |
||||||
|
<img |
||||||
|
src=${userAvatarImage} |
||||||
|
alt="" |
||||||
|
class="rounded-full bg-black bg-opacity-50 border border-solid border-gray-700" |
||||||
|
/> |
||||||
|
<span class="text-indigo-600">${username}</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div id="user-info-change" style=${styles.form}> |
||||||
|
<input type="text" |
||||||
|
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" |
||||||
|
maxlength="100" |
||||||
|
placeholder="Update username" |
||||||
|
value=${username} |
||||||
|
onKeydown=${this.handleKeydown} |
||||||
|
ref=${this.textInput} |
||||||
|
> |
||||||
|
<button onClick=${this.handleUpdateUsername} class="bg-blue-500 hover:bg-blue-700 text-white py-1 px-1 rounded user-btn">Update</button> |
||||||
|
<button onClick=${this.handleHideForm} class="bg-gray-900 hover:bg-gray-800 py-1 px-2 rounded user-btn text-white text-opacity-50" title="cancel">X</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<button type="button" onClick=${handleChatToggle} class="flex bg-gray-800 hover:bg-gray-700">💬</button> |
||||||
|
</div> |
||||||
|
`);
|
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
export const KEY_USERNAME = 'owncast_username'; |
||||||
|
export const KEY_AVATAR = 'owncast_avatar'; |
||||||
|
export const KEY_CHAT_DISPLAYED = 'owncast_chat'; |
||||||
|
export const KEY_CHAT_FIRST_MESSAGE_SENT = 'owncast_first_message_sent'; |
||||||
|
export const CHAT_INITIAL_PLACEHOLDER_TEXT = 'Type here to chat, no account necessary.'; |
||||||
|
export const CHAT_PLACEHOLDER_TEXT = 'Message'; |
||||||
|
export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
|
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8" /> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/> |
||||||
|
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" /> |
||||||
|
|
||||||
|
</head> |
||||||
|
|
||||||
|
<body class="bg-gray-300 text-gray-800"> |
||||||
|
|
||||||
|
<div id="chat-container"></div> |
||||||
|
|
||||||
|
<script type="module"> |
||||||
|
import { render, html } from "https://unpkg.com/htm/preact/index.mjs?module"; |
||||||
|
import { StandaloneChat } from './js/chat/standalone.js'; |
||||||
|
(function () { |
||||||
|
render(html`<${StandaloneChat} />`, document.getElementById("chat-container")); |
||||||
|
})(); |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,87 @@ |
|||||||
|
|
||||||
|
#messages-container { |
||||||
|
overflow: auto; |
||||||
|
padding: 1em 0; |
||||||
|
} |
||||||
|
#message-input-container { |
||||||
|
width: 100%; |
||||||
|
padding: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
#message-form { |
||||||
|
flex-direction: column; |
||||||
|
align-items: flex-end; |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
#message-body-form { |
||||||
|
font-size: 1em; |
||||||
|
height: 60px; |
||||||
|
} |
||||||
|
#message-body-form:disabled{ |
||||||
|
opacity: .5; |
||||||
|
} |
||||||
|
#message-body-form img { |
||||||
|
display: inline; |
||||||
|
padding-left: 5px; |
||||||
|
padding-right: 5px; |
||||||
|
} |
||||||
|
|
||||||
|
#message-body-form .emoji { |
||||||
|
width: 40px; |
||||||
|
} |
||||||
|
|
||||||
|
#message-form-actions { |
||||||
|
flex-direction: row; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.message-text img { |
||||||
|
display: inline; |
||||||
|
padding-left: 5px; |
||||||
|
padding-right: 5px; |
||||||
|
} |
||||||
|
|
||||||
|
.message-text .emoji { |
||||||
|
width: 60px; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.message { |
||||||
|
padding: .85em; |
||||||
|
align-items: flex-start; |
||||||
|
} |
||||||
|
.message-avatar { |
||||||
|
margin-right: .75em; |
||||||
|
} |
||||||
|
.message-avatar img { |
||||||
|
max-width: unset; |
||||||
|
height: 3.0em; |
||||||
|
width: 3.0em; |
||||||
|
padding: 5px; |
||||||
|
} |
||||||
|
|
||||||
|
.message-content { |
||||||
|
font-size: .85em; |
||||||
|
max-width: 85%; |
||||||
|
word-wrap: break-word; |
||||||
|
} |
||||||
|
.message-content a { |
||||||
|
color: #7F9CF5; /* indigo-400 */ |
||||||
|
} |
||||||
|
.message-content a:hover { |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
|
||||||
|
.message-text iframe { |
||||||
|
width: 100%; |
||||||
|
height: 170px; |
||||||
|
border-radius: 15px; |
||||||
|
} |
||||||
|
|
||||||
|
/* Emoji picker */ |
||||||
|
#emoji-button { |
||||||
|
margin: 0 .5em; |
||||||
|
font-size: 1.5em |
||||||
|
} |
||||||
@ -0,0 +1,102 @@ |
|||||||
|
.extra-user-content { |
||||||
|
padding: 1em 3em 3em 3em; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.extra-user-content ol { |
||||||
|
list-style: decimal; |
||||||
|
} |
||||||
|
|
||||||
|
.extra-user-content ul { |
||||||
|
list-style: unset; |
||||||
|
} |
||||||
|
|
||||||
|
.extra-user-content h1, .extra-user-content h2, .extra-user-content h3, .extra-user-content h4 { |
||||||
|
color: #111111; |
||||||
|
font-weight: 400; } |
||||||
|
|
||||||
|
.extra-user-content h1, .extra-user-content h2, .extra-user-content h3, .extra-user-content h4, .extra-user-content h5, .extra-user-content p { |
||||||
|
margin-bottom: 24px; |
||||||
|
padding: 0; } |
||||||
|
|
||||||
|
.extra-user-content h1 { |
||||||
|
font-size: 48px; } |
||||||
|
|
||||||
|
.extra-user-content h2 { |
||||||
|
font-size: 36px; |
||||||
|
margin: 24px 0 6px; } |
||||||
|
|
||||||
|
.extra-user-content h3 { |
||||||
|
font-size: 24px; } |
||||||
|
|
||||||
|
.extra-user-content h4 { |
||||||
|
font-size: 21px; } |
||||||
|
|
||||||
|
.extra-user-content h5 { |
||||||
|
font-size: 18px; } |
||||||
|
|
||||||
|
.extra-user-content a { |
||||||
|
color: #0099ff; |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
vertical-align: baseline; } |
||||||
|
|
||||||
|
.extra-user-content ul, .extra-user-content ol { |
||||||
|
padding: 0; |
||||||
|
margin: 0; } |
||||||
|
|
||||||
|
.extra-user-content li { |
||||||
|
line-height: 24px; } |
||||||
|
|
||||||
|
.extra-user-content li ul, .extra-user-content li ul { |
||||||
|
margin-left: 24px; } |
||||||
|
|
||||||
|
.extra-user-content p, .extra-user-content ul, .extra-user-content ol { |
||||||
|
font-size: 16px; |
||||||
|
line-height: 24px; |
||||||
|
} |
||||||
|
|
||||||
|
.extra-user-content pre { |
||||||
|
padding: 0px 24px; |
||||||
|
max-width: 800px; |
||||||
|
white-space: pre-wrap; } |
||||||
|
|
||||||
|
.extra-user-content code { |
||||||
|
font-family: Consolas, Monaco, Andale Mono, monospace; |
||||||
|
line-height: 1.5; |
||||||
|
font-size: 13px; } |
||||||
|
|
||||||
|
.extra-user-content aside { |
||||||
|
display: block; |
||||||
|
float: right; |
||||||
|
width: 390px; } |
||||||
|
|
||||||
|
.extra-user-content blockquote { |
||||||
|
margin: 1em 2em; |
||||||
|
max-width: 476px; } |
||||||
|
|
||||||
|
.extra-user-content blockquote p { |
||||||
|
color: #666; |
||||||
|
max-width: 460px; } |
||||||
|
|
||||||
|
.extra-user-content hr { |
||||||
|
width: 540px; |
||||||
|
text-align: left; |
||||||
|
margin: 0 auto 0 0; |
||||||
|
color: #999; } |
||||||
|
|
||||||
|
.extra-user-content table { |
||||||
|
border-collapse: collapse; |
||||||
|
margin: 1em 1em; |
||||||
|
border: 1px solid #CCC; } |
||||||
|
|
||||||
|
.extra-user-content table thead { |
||||||
|
background-color: #EEE; } |
||||||
|
|
||||||
|
.extra-user-content table thead td { |
||||||
|
color: #666; } |
||||||
|
|
||||||
|
.extra-user-content table td { |
||||||
|
padding: 0.5em 1em; |
||||||
|
border: 1px solid #CCC; } |
||||||
Loading…
Reference in new issue