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