chat fixes v3 or 5 or 123 (#168)

* only consider short-heights when not smallscreen; hide status bar when small screen, but leave shadow;

* fix max char counting bugs with paste, yet be still be able to use modifier keys even when max chars reached

* rmeove 'chat' button; move into textarea

* use image for emoji picker for sizing consitency

* cleanup unused things

* - totally unecessary emoji picker style improvements
- totally necessary doctype added to emoji picker so it shows up more stable-y on mobile views

* more stable layout positioning for chat panel without hacky margins, so that the bottom of the message list will always be on top of the form input, and not behind it at any point.

* hide header on touch screens when screns are small and screen height is short (possibly when keyboard is up), so that there's more visibliity to see messages. this only works on chrome, not ios safari right now, due to the position: fixed of things.

* move char counting to keyup instead

* address message text horiz overflow (#157)

* dont jumpToBottom if user has scrolled about 200px from the bottom (#101)

* scroll to bottom on resize too

* cleanup

* revert test bool

* typo

* re-readjust short-wide case again

* - add focus to input field after emoji is selected, put cursor at end
- instead of smooth scrolling to bottom, just jump there.
This commit is contained in:
gingervitis
2020-09-21 20:11:09 -07:00
committed by GitHub
parent f2f5993e22
commit 661eedc03a
11 changed files with 232 additions and 130 deletions

BIN
webroot/img/smiley.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,3 +1,4 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -46,7 +47,7 @@
<link rel="preconnect" href="https://unpkg.com/htm?module" /> <link rel="preconnect" href="https://unpkg.com/htm?module" />
</head> </head>
<body class="bg-gray-300 text-gray-800"> <body class="scrollbar-hidden bg-gray-300 text-gray-800">
<div id="app"></div> <div id="app"></div>
<script type="module"> <script type="module">

View File

@@ -7,7 +7,7 @@ import SocialIcon from './components/social.js';
import UsernameForm from './components/chat/username.js'; import UsernameForm from './components/chat/username.js';
import Chat from './components/chat/chat.js'; import Chat from './components/chat/chat.js';
import Websocket from './utils/websocket.js'; import Websocket from './utils/websocket.js';
import { secondsToHMMSS } from './utils/helpers.js'; import { secondsToHMMSS, hasTouchScreen } from './utils/helpers.js';
import { import {
addNewlines, addNewlines,
@@ -69,6 +69,8 @@ export default class App extends Component {
windowHeight: window.innerHeight, windowHeight: window.innerHeight,
}; };
this.hasTouchScreen = hasTouchScreen();
// timers // timers
this.playerRestartTimer = null; this.playerRestartTimer = null;
this.offlineTimer = null; this.offlineTimer = null;
@@ -79,7 +81,7 @@ export default class App extends Component {
// misc dom events // misc dom events
this.handleChatPanelToggle = this.handleChatPanelToggle.bind(this); this.handleChatPanelToggle = this.handleChatPanelToggle.bind(this);
this.handleUsernameChange = this.handleUsernameChange.bind(this); this.handleUsernameChange = this.handleUsernameChange.bind(this);
this.handleWindowResize = debounce(this.handleWindowResize.bind(this), 400); this.handleWindowResize = debounce(this.handleWindowResize.bind(this), 250);
this.handleOfflineMode = this.handleOfflineMode.bind(this); this.handleOfflineMode = this.handleOfflineMode.bind(this);
this.handleOnlineMode = this.handleOnlineMode.bind(this); this.handleOnlineMode = this.handleOnlineMode.bind(this);
@@ -101,7 +103,9 @@ export default class App extends Component {
componentDidMount() { componentDidMount() {
this.getConfig(); this.getConfig();
window.addEventListener('resize', this.handleWindowResize); window.addEventListener('resize', this.handleWindowResize);
if (this.hasTouchScreen) {
window.addEventListener('orientationchange', this.handleWindowResize);
}
this.player = new OwncastPlayer(); this.player = new OwncastPlayer();
this.player.setupPlayerCallbacks({ this.player.setupPlayerCallbacks({
onReady: this.handlePlayerReady, onReady: this.handlePlayerReady,
@@ -120,6 +124,9 @@ export default class App extends Component {
clearTimeout(this.disableChatTimer); clearTimeout(this.disableChatTimer);
clearInterval(this.streamDurationTimer); clearInterval(this.streamDurationTimer);
window.removeEventListener('resize', this.handleWindowResize); window.removeEventListener('resize', this.handleWindowResize);
if (this.hasTouchScreen) {
window.removeEventListener('orientationchange', this.handleWindowResize);
}
} }
// fetch /config data // fetch /config data
@@ -404,12 +411,14 @@ export default class App extends Component {
const shortHeight = windowHeight <= HEIGHT_SHORT_WIDE; const shortHeight = windowHeight <= HEIGHT_SHORT_WIDE;
const singleColMode = windowWidth <= WIDTH_SINGLE_COL && !shortHeight; const singleColMode = windowWidth <= WIDTH_SINGLE_COL && !shortHeight;
const extraAppClasses = classNames({ const extraAppClasses = classNames({
chat: displayChat, chat: displayChat,
'no-chat': !displayChat, 'no-chat': !displayChat,
'single-col': singleColMode, 'single-col': singleColMode,
'bg-gray-800': singleColMode && displayChat, 'bg-gray-800': singleColMode && displayChat,
'short-wide': shortHeight, 'short-wide': shortHeight && windowWidth > WIDTH_SINGLE_COL,
'touch-screen': this.hasTouchScreen,
}); });
return html` return html`

View File

@@ -3,10 +3,17 @@ import htm from 'https://unpkg.com/htm?module';
const html = htm.bind(h); const html = htm.bind(h);
import { EmojiButton } from 'https://cdn.skypack.dev/pin/@joeattardi/emoji-button@v4.1.0-v8psdkkxts3LNdpA0m5Q/min/@joeattardi/emoji-button.js'; import { EmojiButton } from 'https://cdn.skypack.dev/pin/@joeattardi/emoji-button@v4.1.0-v8psdkkxts3LNdpA0m5Q/min/@joeattardi/emoji-button.js';
import ContentEditable from './content-editable.js'; import ContentEditable, { replaceCaret } from './content-editable.js';
import { generatePlaceholderText, getCaretPosition, convertToText, convertOnPaste } from '../../utils/chat.js'; import { generatePlaceholderText, getCaretPosition, convertToText, convertOnPaste } from '../../utils/chat.js';
import { getLocalStorage, setLocalStorage } from '../../utils/helpers.js'; import { getLocalStorage, setLocalStorage, classNames } from '../../utils/helpers.js';
import { URL_CUSTOM_EMOJIS, KEY_CHAT_FIRST_MESSAGE_SENT } from '../../utils/constants.js'; import {
URL_CUSTOM_EMOJIS,
KEY_CHAT_FIRST_MESSAGE_SENT,
CHAT_MAX_MESSAGE_LENGTH,
CHAT_CHAR_COUNT_BUFFER,
CHAT_OK_KEYCODES,
CHAT_KEY_MODIFIERS,
} from '../../utils/constants.js';
export default class ChatInput extends Component { export default class ChatInput extends Component {
constructor(props, context) { constructor(props, context) {
@@ -15,16 +22,16 @@ export default class ChatInput extends Component {
this.emojiPickerButton = createRef(); this.emojiPickerButton = createRef();
this.messageCharCount = 0; this.messageCharCount = 0;
this.maxMessageLength = 500;
this.maxMessageBuffer = 20;
this.emojiPicker = null; this.emojiPicker = null;
this.prepNewLine = false; this.prepNewLine = false;
this.modifierKeyPressed = false; // control/meta/shift/alt
this.state = { this.state = {
inputHTML: '', inputHTML: '',
inputWarning: '', inputText: '', // for counting
inputCharsLeft: CHAT_MAX_MESSAGE_LENGTH,
hasSentFirstChatMessage: getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT), hasSentFirstChatMessage: getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT),
}; };
@@ -56,11 +63,11 @@ export default class ChatInput extends Component {
.then(json => { .then(json => {
this.emojiPicker = new EmojiButton({ this.emojiPicker = new EmojiButton({
zIndex: 100, zIndex: 100,
theme: 'dark', theme: 'owncast', // see chat.css
custom: json, custom: json,
initialCategory: 'custom', initialCategory: 'custom',
showPreview: false, showPreview: false,
emojiSize: '30px', emojiSize: '24px',
position: 'right-start', position: 'right-start',
strategy: 'absolute', strategy: 'absolute',
}); });
@@ -93,6 +100,12 @@ export default class ChatInput extends Component {
this.setState({ this.setState({
inputHTML: inputHTML + content, inputHTML: inputHTML + content,
}); });
// a hacky way add focus back into input field
setTimeout( () => {
const input = this.formMessageInput.current;
input.focus();
replaceCaret(input);
}, 100);
} }
// autocomplete user names // autocomplete user names
@@ -133,23 +146,12 @@ export default class ChatInput extends Component {
} }
handleMessageInputKeydown(event) { handleMessageInputKeydown(event) {
const okCodes = [
'ArrowLeft',
'ArrowUp',
'ArrowRight',
'ArrowDown',
'Shift',
'Meta',
'Alt',
'Delete',
'Backspace',
];
const formField = this.formMessageInput.current; const formField = this.formMessageInput.current;
let textValue = formField.innerText.trim(); // get this only to count chars let textValue = formField.innerText.trim(); // get this only to count chars
const newStates = {};
let numCharsLeft = this.maxMessageLength - textValue.length; let numCharsLeft = CHAT_MAX_MESSAGE_LENGTH - textValue.length;
const key = event.key; const key = event && event.key;
if (key === 'Enter') { if (key === 'Enter') {
if (!this.prepNewLine) { if (!this.prepNewLine) {
@@ -159,6 +161,10 @@ export default class ChatInput extends Component {
return; return;
} }
} }
// allow key presses such as command/shift/meta, etc even when message length is full later.
if (CHAT_KEY_MODIFIERS.includes(key)) {
this.modifierKeyPressed = true;
}
if (key === 'Control' || key === 'Shift') { if (key === 'Control' || key === 'Shift') {
this.prepNewLine = true; this.prepNewLine = true;
} }
@@ -168,38 +174,52 @@ export default class ChatInput extends Component {
// value could have been changed, update char count // value could have been changed, update char count
textValue = formField.innerText.trim(); textValue = formField.innerText.trim();
numCharsLeft = this.maxMessageLength - textValue.length; numCharsLeft = CHAT_MAX_MESSAGE_LENGTH - textValue.length;
} }
} }
// text count if (numCharsLeft <= 0 && !CHAT_OK_KEYCODES.includes(key)) {
if (numCharsLeft <= this.maxMessageBuffer) { newStates.inputText = textValue;
this.setState({ this.setState(newStates);
inputWarning: `${numCharsLeft} chars left`, if (!this.modifierKeyPressed) {
});
if (numCharsLeft <= 0 && !okCodes.includes(key)) {
event.preventDefault(); // prevent typing more event.preventDefault(); // prevent typing more
return;
} }
} else { return;
this.setState({
inputWarning: '',
});
} }
newStates.inputText = textValue;
this.setState(newStates);
} }
handleMessageInputKeyup(event) { handleMessageInputKeyup(event) {
if (event.key === 'Control' || event.key === 'Shift') { const formField = this.formMessageInput.current;
const textValue = formField.innerText.trim(); // get this only to count chars
const { key } = event;
if (key === 'Control' || key === 'Shift') {
this.prepNewLine = false; this.prepNewLine = false;
} }
if (CHAT_KEY_MODIFIERS.includes(key)) {
this.modifierKeyPressed = false;
}
this.setState({
inputCharsLeft: CHAT_MAX_MESSAGE_LENGTH - textValue.length,
});
} }
handleMessageInputBlur(event) { handleMessageInputBlur(event) {
this.prepNewLine = false; this.prepNewLine = false;
this.modifierKeyPressed = false;
} }
handlePaste(event) { handlePaste(event) {
// don't allow paste if too much text already
if (CHAT_MAX_MESSAGE_LENGTH - this.state.inputText.length < 0) {
event.preventDefault();
return;
}
convertOnPaste(event); convertOnPaste(event);
this.handleMessageInputKeydown(event);
} }
handleSubmitChatButton(event) { handleSubmitChatButton(event) {
@@ -209,11 +229,15 @@ export default class ChatInput extends Component {
sendMessage() { sendMessage() {
const { handleSendMessage } = this.props; const { handleSendMessage } = this.props;
const { hasSentFirstChatMessage, inputHTML } = this.state; const { hasSentFirstChatMessage, inputHTML, inputText } = this.state;
if (CHAT_MAX_MESSAGE_LENGTH - inputText.length < 0) {
return;
}
const message = convertToText(inputHTML); const message = convertToText(inputHTML);
const newStates = { const newStates = {
inputWarning: '',
inputHTML: '', inputHTML: '',
inputText: '',
inputCharsLeft: CHAT_MAX_MESSAGE_LENGTH,
}; };
handleSendMessage(message); handleSendMessage(message);
@@ -232,57 +256,51 @@ export default class ChatInput extends Component {
} }
render(props, state) { render(props, state) {
const { hasSentFirstChatMessage, inputWarning, inputHTML } = state; const { hasSentFirstChatMessage, inputCharsLeft, inputHTML } = state;
const { inputEnabled } = props; const { inputEnabled } = props;
const emojiButtonStyle = { const emojiButtonStyle = {
display: this.emojiPicker ? 'block' : 'none', display: this.emojiPicker && inputCharsLeft > 0 ? 'block' : 'none',
}; };
const extraClasses = classNames({
'display-count': inputCharsLeft <= CHAT_CHAR_COUNT_BUFFER,
});
const placeholderText = generatePlaceholderText(inputEnabled, hasSentFirstChatMessage); const placeholderText = generatePlaceholderText(inputEnabled, hasSentFirstChatMessage);
return ( return (
html` html`
<div id="message-input-container" class="fixed bottom-0 shadow-md bg-gray-900 border-t border-gray-700 border-solid p-4 z-20"> <div id="message-input-container" class="relative shadow-md bg-gray-900 border-t border-gray-700 border-solid p-4 z-20 ${extraClasses}">
<${ContentEditable} <div
id="message-input" id="message-input-wrap"
class="appearance-none block w-full bg-gray-200 text-sm text-gray-700 border border-black-500 rounded py-2 px-2 my-2 focus:bg-white h-20 overflow-auto" class="flex flex-row justify-end appearance-none w-full bg-gray-200 border border-black-500 rounded py-2 px-2 pr-12 my-2 overflow-auto">
<${ContentEditable}
id="message-input"
class="appearance-none block w-full bg-transparent text-sm text-gray-700 h-full focus:outline-none"
placeholderText=${placeholderText} placeholderText=${placeholderText}
innerRef=${this.formMessageInput} innerRef=${this.formMessageInput}
html=${inputHTML} html=${inputHTML}
disabled=${!inputEnabled} disabled=${!inputEnabled}
onChange=${this.handleContentEditableChange} onChange=${this.handleContentEditableChange}
onKeyDown=${this.handleMessageInputKeydown} onKeyDown=${this.handleMessageInputKeydown}
onKeyUp=${this.handleMessageInputKeyup} onKeyUp=${this.handleMessageInputKeyup}
onBlur=${this.handleMessageInputBlur} onBlur=${this.handleMessageInputBlur}
onPaste=${this.handlePaste} onPaste=${this.handlePaste}
/> />
</div>
<div id="message-form-actions" class="flex flex-row justify-between items-center w-full"> <div id="message-form-actions" class="absolute flex flex-col w-10 justify-end items-center">
<span id="message-form-warning" class="text-red-600 text-xs">${inputWarning}</span>
<div id="message-form-actions-buttons" class="flex flex-row justify-end items-center">
<button <button
ref=${this.emojiPickerButton} ref=${this.emojiPickerButton}
id="emoji-button" id="emoji-button"
class="mr-2 text-2xl cursor-pointer" class="text-3xl leading-3 cursor-pointer text-purple-600"
type="button" type="button"
style=${emojiButtonStyle} style=${emojiButtonStyle}
onclick=${this.handleEmojiButtonClick} onclick=${this.handleEmojiButtonClick}
disabled=${!inputEnabled} disabled=${!inputEnabled}
>😏</button> ><img src="../../../img/smiley.png" /></button>
<button <span id="message-form-warning" class="text-red-600 text-xs">${inputCharsLeft}/${CHAT_MAX_MESSAGE_LENGTH}</span>
onclick=${this.handleSubmitChatButton}
disabled=${!inputEnabled}
type="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> </div>
</div>
</div> </div>
`); `);
} }

View File

@@ -5,9 +5,9 @@ const html = htm.bind(h);
import Message from './message.js'; import Message from './message.js';
import ChatInput from './chat-input.js'; import ChatInput from './chat-input.js';
import { CALLBACKS, SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js'; import { CALLBACKS, SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js';
import { setVHvar, hasTouchScreen, jumpToBottom } from '../../utils/helpers.js'; import { jumpToBottom, debounce } from '../../utils/helpers.js';
import { extraUserNamesFromMessageHistory } from '../../utils/chat.js'; import { extraUserNamesFromMessageHistory } from '../../utils/chat.js';
import { URL_CHAT_HISTORY } from '../../utils/constants.js'; import { URL_CHAT_HISTORY, MESSAGE_JUMPTOBOTTOM_BUFFER } from '../../utils/constants.js';
export default class Chat extends Component { export default class Chat extends Component {
constructor(props, context) { constructor(props, context) {
@@ -29,17 +29,13 @@ export default class Chat extends Component {
this.submitChat = this.submitChat.bind(this); this.submitChat = this.submitChat.bind(this);
this.submitChat = this.submitChat.bind(this); this.submitChat = this.submitChat.bind(this);
this.scrollToBottom = this.scrollToBottom.bind(this); this.scrollToBottom = this.scrollToBottom.bind(this);
this.jumpToBottomPending = false; this.handleWindowResize = debounce(this.handleWindowResize.bind(this), 500);
} }
componentDidMount() { componentDidMount() {
this.setupWebSocketCallbacks(); this.setupWebSocketCallbacks();
this.getChatHistory(); this.getChatHistory();
window.addEventListener('resize', this.handleWindowResize);
if (hasTouchScreen()) {
// setVHvar();
// window.addEventListener("orientationchange", setVHvar);
}
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
@@ -55,14 +51,13 @@ export default class Chat extends Component {
} }
// scroll to bottom of messages list when new ones come in // scroll to bottom of messages list when new ones come in
if (messages.length > prevMessages.length) { if (messages.length > prevMessages.length) {
this.jumpToBottomPending = true; if (!prevMessages.length || this.checkShouldScroll()) {
this.scrollToBottom();
}
} }
} }
componentWillUnmount() { componentWillUnmount() {
if (hasTouchScreen()) { window.removeEventListener('resize', this.handleWindowResize);
window.removeEventListener("orientationchange", setVHvar);
}
} }
setupWebSocketCallbacks() { setupWebSocketCallbacks() {
@@ -168,6 +163,17 @@ export default class Chat extends Component {
jumpToBottom(this.scrollableMessagesContainer.current); jumpToBottom(this.scrollableMessagesContainer.current);
} }
checkShouldScroll() {
const { scrollTop, scrollHeight, clientHeight } = this.scrollableMessagesContainer.current;
const fullyScrolled = scrollHeight - clientHeight;
return scrollHeight >= clientHeight && fullyScrolled - scrollTop < MESSAGE_JUMPTOBOTTOM_BUFFER;
}
handleWindowResize() {
this.scrollToBottom();
}
render(props, state) { render(props, state) {
const { username, messagesOnly, chatInputEnabled } = props; const { username, messagesOnly, chatInputEnabled } = props;
const { messages, chatUserNames } = state; const { messages, chatUserNames } = state;
@@ -181,20 +187,12 @@ export default class Chat extends Component {
/>` />`
); );
// After the render completes (based on requestAnimationFrame) then jump to bottom.
// This hopefully fixes the race conditions where jumpTobottom fires before the
// DOM element has re-drawn with its new size.
if (this.jumpToBottomPending) {
this.jumpToBottomPending = false;
window.requestAnimationFrame(this.scrollToBottom);
}
if (messagesOnly) { if (messagesOnly) {
return html` return html`
<div <div
id="messages-container" id="messages-container"
ref=${this.scrollableMessagesContainer} ref=${this.scrollableMessagesContainer}
class="py-1 overflow-auto" class="scrollbar-hidden py-1 overflow-auto"
> >
${messageList} ${messageList}
</div> </div>
@@ -210,7 +208,7 @@ export default class Chat extends Component {
<div <div
id="messages-container" id="messages-container"
ref=${this.scrollableMessagesContainer} ref=${this.scrollableMessagesContainer}
class="py-1 overflow-auto z-10" class="scrollbar-hidden py-1 overflow-auto z-10"
> >
${messageList} ${messageList}
</div> </div>
@@ -223,6 +221,5 @@ export default class Chat extends Component {
</section> </section>
`; `;
} }
} }

View File

@@ -8,7 +8,7 @@ https://stackoverflow.com/questions/22677931/react-js-onchange-event-for-content
*/ */
import { Component, createRef, h } from 'https://unpkg.com/preact?module'; import { Component, createRef, h } from 'https://unpkg.com/preact?module';
function replaceCaret(el) { export function replaceCaret(el) {
// Place the caret at the end of the element // Place the caret at the end of the element
const target = document.createTextNode(''); const target = document.createTextNode('');
el.appendChild(target); el.appendChild(target);
@@ -69,8 +69,6 @@ export default class ContentEditable extends Component {
props.innerRef !== nextProps.innerRef; props.innerRef !== nextProps.innerRef;
} }
componentDidUpdate() { componentDidUpdate() {
const el = this.getDOMElement(); const el = this.getDOMElement();
if (!el) return; if (!el) return;
@@ -118,6 +116,7 @@ export default class ContentEditable extends Component {
this.el.current = current this.el.current = current
} : innerRef || this.el, } : innerRef || this.el,
onInput: this.emitChange, onInput: this.emitChange,
onFocus: this.props.onFocus || this.emitChange,
onBlur: this.props.onBlur || this.emitChange, onBlur: this.props.onBlur || this.emitChange,
onKeyup: this.props.onKeyUp || this.emitChange, onKeyup: this.props.onKeyUp || this.emitChange,
onKeydown: this.props.onKeyDown || this.emitChange, onKeydown: this.props.onKeyDown || this.emitChange,

View File

@@ -34,7 +34,7 @@ export default class Message extends Component {
${author} ${author}
</div> </div>
<div <div
class="message-text text-gray-300 font-normal" class="message-text text-gray-300 font-normal overflow-y-hidden"
dangerouslySetInnerHTML=${ dangerouslySetInnerHTML=${
{ __html: formattedMessage } { __html: formattedMessage }
} }

View File

@@ -27,7 +27,26 @@ 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_INITIAL_PLACEHOLDER_TEXT = 'Type here to chat, no account necessary.';
export const CHAT_PLACEHOLDER_TEXT = 'Message'; export const CHAT_PLACEHOLDER_TEXT = 'Message';
export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.';
export const CHAT_MAX_MESSAGE_LENGTH = 500;
export const CHAT_CHAR_COUNT_BUFFER = 20;
export const CHAT_OK_KEYCODES = [
'ArrowLeft',
'ArrowUp',
'ArrowRight',
'ArrowDown',
'Shift',
'Meta',
'Alt',
'Delete',
'Backspace',
];
export const CHAT_KEY_MODIFIERS = [
'Control',
'Shift',
'Meta',
'Alt',
];
export const MESSAGE_JUMPTOBOTTOM_BUFFER = 260;
// app styling // app styling
export const WIDTH_SINGLE_COL = 730; export const WIDTH_SINGLE_COL = 730;

View File

@@ -30,7 +30,7 @@ export function jumpToBottom(element) {
element.scrollTo({ element.scrollTo({
top: element.scrollHeight, top: element.scrollHeight,
left: 0, left: 0,
behavior: 'smooth' behavior: 'auto'
}); });
}, 50, element); }, 50, element);
} }

View File

@@ -20,11 +20,12 @@ a:hover {
text-decoration: underline; text-decoration: underline;
} }
::-webkit-scrollbar { .scrollbar-hidden::-webkit-scrollbar {
width: 0px; width: 0px;
background: transparent; background: transparent;
} }
#app-container * { #app-container * {
transition: all .25s; transition: all .25s;
} }
@@ -48,6 +49,7 @@ button[disabled] {
header { header {
height: var(--header-height); height: var(--header-height);
background-color: var(--header-bg-color); background-color: var(--header-bg-color);
display: block;
} }
#logo-container { #logo-container {
@@ -125,8 +127,6 @@ header {
height: calc((9 / 16) * var(--content-width)); height: calc((9 / 16) * var(--content-width));
} }
.short-wide.chat #video-container { .short-wide.chat #video-container {
height: calc(100vh - var(--header-height) - 3rem); height: calc(100vh - var(--header-height) - 3rem);
min-height: auto; min-height: auto;
@@ -151,15 +151,11 @@ header {
z-index: 40; z-index: 40;
} }
.single-col #chat-container { .single-col #chat-container {
position: relative; position: fixed;
bottom: 0;
width: 100%; width: 100%;
height: auto; height: auto;
min-height: 800px;
} }
/* .single-col #video-container {
min-height: auto;
width: 100%;
} */
.single-col.chat #video-container, .single-col.chat #video-container,
.single-col.no-chat #video-container, .single-col.no-chat #video-container,
@@ -180,14 +176,34 @@ header {
.single-col.chat #user-content { .single-col.chat #user-content {
display: none; display: none;
} }
.single-col #messages-container {
flex-grow: 2;
margin-top: calc(var(--video-container-height));
}
.single-col #message-input-container { .single-col #message-input-container {
width: 100%; width: 100%;
} }
.single-col #message-input { .single-col #stream-info {
height: 3rem; height: 0;
padding: 0;
}
.single-col #stream-info span {
display: none;
} }
.single-col #message-input-wrap {
max-height: 3em;
}
@media screen and (max-height: 500px) {
.single-col.touch-screen {
--header-height: 0px;
}
.single-col.touch-screen header {
display: none;
}
}
/* ************************************************8 */ /* ************************************************8 */
@@ -205,7 +221,4 @@ header {
font-size: 1rem; font-size: 1rem;
} }
#stream-info {
display: none;
}
} }

View File

@@ -14,13 +14,30 @@
width: var(--right-col-width); width: var(--right-col-width);
} }
#messages-container { #message-input-wrap {
padding-bottom: 10rem; min-height: 2.5rem;
max-height: 5rem;
}
#message-form-actions {
right: 2rem;
bottom: 1.88rem;
}
#emoji-button {
height: 1.75rem;
width: 1.75rem;
}
#message-form-warning {
display: none;
}
.display-count #message-form-warning {
display: block;
} }
/******************************/ /******************************/
/******************************/ /******************************/
#message-input img { #message-input img {
display: inline; display: inline;
vertical-align: middle; vertical-align: middle;
@@ -46,6 +63,8 @@
opacity: 1.0; opacity: 1.0;
} }
#message-input::selection { background:#d7ddf4; }
/* When chat is disabled (contenteditable=false) chat input div should appear disabled. */ /* When chat is disabled (contenteditable=false) chat input div should appear disabled. */
#message-input:disabled, #message-input:disabled,
@@ -56,10 +75,43 @@
/******************************/ /******************************/
/******************************/
/* EMOJI PICKER OVERRIDES */
.emoji-picker.owncast {
--secondary-text-color: rgba(255,255,255,.5);
--category-button-color: rgba(255,255,255,.5);
background: rgba(26,32,44,1); /* tailwind bg-gray-900 */
color: rgba(226,232,240,1); /* tailwind text-gray-300 */
border-color: black;
font-family: inherit;
}
.emoji-picker h2 {
font-family: inherit;
}
.emoji-picker__emoji { .emoji-picker__emoji {
border-radius: 5px; border-radius: 5px;
} }
.emoji-picker__emojis::-webkit-scrollbar {
background: transparent;
border-radius: 8px;
}
.emoji-picker__emojis::-webkit-scrollbar-track {
border-radius: 8px;
background-color: rgba(0,0,0,.2);
box-shadow: inset 0 0 3px rgba(0,0,0,0.3);
}
.emoji-picker__emojis::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,.45);
border-radius: 8px;
}
/******************************/
.message-avatar { .message-avatar {
height: 3.0em; height: 3.0em;
@@ -77,6 +129,9 @@
/* MESSAGE TEXT HTML */ /* MESSAGE TEXT HTML */
/* MESSAGE TEXT HTML */ /* MESSAGE TEXT HTML */
/* MESSAGE TEXT HTML */ /* MESSAGE TEXT HTML */
.message-text {
word-break: break-word;
}
.message-text a { .message-text a {
color: #7F9CF5; /* indigo-400 */ color: #7F9CF5; /* indigo-400 */
} }
@@ -102,15 +157,6 @@
padding: .25rem; padding: .25rem;
} }
/* Hide emoji button on small screens */
@media screen and (max-width: 860px) {
#emoji-button {
/* Emoji library overrides this so important is needed */
display: none !important;
}
}
.message-text .chat-embed { .message-text .chat-embed {
width: 100%; width: 100%;
border-radius: .25rem; border-radius: .25rem;