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:
parent
f2f5993e22
commit
661eedc03a
BIN
webroot/img/smiley.png
Normal file
BIN
webroot/img/smiley.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
@ -1,3 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@ -46,7 +47,7 @@
|
||||
<link rel="preconnect" href="https://unpkg.com/htm?module" />
|
||||
|
||||
</head>
|
||||
<body class="bg-gray-300 text-gray-800">
|
||||
<body class="scrollbar-hidden bg-gray-300 text-gray-800">
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module">
|
||||
|
@ -7,7 +7,7 @@ import SocialIcon from './components/social.js';
|
||||
import UsernameForm from './components/chat/username.js';
|
||||
import Chat from './components/chat/chat.js';
|
||||
import Websocket from './utils/websocket.js';
|
||||
import { secondsToHMMSS } from './utils/helpers.js';
|
||||
import { secondsToHMMSS, hasTouchScreen } from './utils/helpers.js';
|
||||
|
||||
import {
|
||||
addNewlines,
|
||||
@ -69,6 +69,8 @@ export default class App extends Component {
|
||||
windowHeight: window.innerHeight,
|
||||
};
|
||||
|
||||
this.hasTouchScreen = hasTouchScreen();
|
||||
|
||||
// timers
|
||||
this.playerRestartTimer = null;
|
||||
this.offlineTimer = null;
|
||||
@ -79,7 +81,7 @@ export default class App extends Component {
|
||||
// misc dom events
|
||||
this.handleChatPanelToggle = this.handleChatPanelToggle.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.handleOnlineMode = this.handleOnlineMode.bind(this);
|
||||
@ -101,7 +103,9 @@ export default class App extends Component {
|
||||
componentDidMount() {
|
||||
this.getConfig();
|
||||
window.addEventListener('resize', this.handleWindowResize);
|
||||
|
||||
if (this.hasTouchScreen) {
|
||||
window.addEventListener('orientationchange', this.handleWindowResize);
|
||||
}
|
||||
this.player = new OwncastPlayer();
|
||||
this.player.setupPlayerCallbacks({
|
||||
onReady: this.handlePlayerReady,
|
||||
@ -120,6 +124,9 @@ export default class App extends Component {
|
||||
clearTimeout(this.disableChatTimer);
|
||||
clearInterval(this.streamDurationTimer);
|
||||
window.removeEventListener('resize', this.handleWindowResize);
|
||||
if (this.hasTouchScreen) {
|
||||
window.removeEventListener('orientationchange', this.handleWindowResize);
|
||||
}
|
||||
}
|
||||
|
||||
// fetch /config data
|
||||
@ -404,12 +411,14 @@ export default class App extends Component {
|
||||
|
||||
const shortHeight = windowHeight <= HEIGHT_SHORT_WIDE;
|
||||
const singleColMode = windowWidth <= WIDTH_SINGLE_COL && !shortHeight;
|
||||
|
||||
const extraAppClasses = classNames({
|
||||
chat: displayChat,
|
||||
'no-chat': !displayChat,
|
||||
'single-col': singleColMode,
|
||||
'bg-gray-800': singleColMode && displayChat,
|
||||
'short-wide': shortHeight,
|
||||
'short-wide': shortHeight && windowWidth > WIDTH_SINGLE_COL,
|
||||
'touch-screen': this.hasTouchScreen,
|
||||
});
|
||||
|
||||
return html`
|
||||
|
@ -3,10 +3,17 @@ import htm from 'https://unpkg.com/htm?module';
|
||||
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 ContentEditable from './content-editable.js';
|
||||
import ContentEditable, { replaceCaret } from './content-editable.js';
|
||||
import { generatePlaceholderText, getCaretPosition, convertToText, convertOnPaste } from '../../utils/chat.js';
|
||||
import { getLocalStorage, setLocalStorage } from '../../utils/helpers.js';
|
||||
import { URL_CUSTOM_EMOJIS, KEY_CHAT_FIRST_MESSAGE_SENT } from '../../utils/constants.js';
|
||||
import { getLocalStorage, setLocalStorage, classNames } from '../../utils/helpers.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 {
|
||||
constructor(props, context) {
|
||||
@ -15,16 +22,16 @@ export default class ChatInput extends Component {
|
||||
this.emojiPickerButton = createRef();
|
||||
|
||||
this.messageCharCount = 0;
|
||||
this.maxMessageLength = 500;
|
||||
this.maxMessageBuffer = 20;
|
||||
|
||||
this.emojiPicker = null;
|
||||
|
||||
this.prepNewLine = false;
|
||||
this.modifierKeyPressed = false; // control/meta/shift/alt
|
||||
|
||||
this.state = {
|
||||
inputHTML: '',
|
||||
inputWarning: '',
|
||||
inputText: '', // for counting
|
||||
inputCharsLeft: CHAT_MAX_MESSAGE_LENGTH,
|
||||
hasSentFirstChatMessage: getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT),
|
||||
};
|
||||
|
||||
@ -56,11 +63,11 @@ export default class ChatInput extends Component {
|
||||
.then(json => {
|
||||
this.emojiPicker = new EmojiButton({
|
||||
zIndex: 100,
|
||||
theme: 'dark',
|
||||
theme: 'owncast', // see chat.css
|
||||
custom: json,
|
||||
initialCategory: 'custom',
|
||||
showPreview: false,
|
||||
emojiSize: '30px',
|
||||
emojiSize: '24px',
|
||||
position: 'right-start',
|
||||
strategy: 'absolute',
|
||||
});
|
||||
@ -93,6 +100,12 @@ export default class ChatInput extends Component {
|
||||
this.setState({
|
||||
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
|
||||
@ -133,23 +146,12 @@ export default class ChatInput extends Component {
|
||||
}
|
||||
|
||||
handleMessageInputKeydown(event) {
|
||||
const okCodes = [
|
||||
'ArrowLeft',
|
||||
'ArrowUp',
|
||||
'ArrowRight',
|
||||
'ArrowDown',
|
||||
'Shift',
|
||||
'Meta',
|
||||
'Alt',
|
||||
'Delete',
|
||||
'Backspace',
|
||||
];
|
||||
const formField = this.formMessageInput.current;
|
||||
|
||||
let textValue = formField.innerText.trim(); // get this only to count chars
|
||||
|
||||
let numCharsLeft = this.maxMessageLength - textValue.length;
|
||||
const key = event.key;
|
||||
const newStates = {};
|
||||
let numCharsLeft = CHAT_MAX_MESSAGE_LENGTH - textValue.length;
|
||||
const key = event && event.key;
|
||||
|
||||
if (key === 'Enter') {
|
||||
if (!this.prepNewLine) {
|
||||
@ -159,6 +161,10 @@ export default class ChatInput extends Component {
|
||||
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') {
|
||||
this.prepNewLine = true;
|
||||
}
|
||||
@ -168,38 +174,52 @@ export default class ChatInput extends Component {
|
||||
|
||||
// value could have been changed, update char count
|
||||
textValue = formField.innerText.trim();
|
||||
numCharsLeft = this.maxMessageLength - textValue.length;
|
||||
numCharsLeft = CHAT_MAX_MESSAGE_LENGTH - textValue.length;
|
||||
}
|
||||
}
|
||||
|
||||
// text count
|
||||
if (numCharsLeft <= this.maxMessageBuffer) {
|
||||
this.setState({
|
||||
inputWarning: `${numCharsLeft} chars left`,
|
||||
});
|
||||
if (numCharsLeft <= 0 && !okCodes.includes(key)) {
|
||||
if (numCharsLeft <= 0 && !CHAT_OK_KEYCODES.includes(key)) {
|
||||
newStates.inputText = textValue;
|
||||
this.setState(newStates);
|
||||
if (!this.modifierKeyPressed) {
|
||||
event.preventDefault(); // prevent typing more
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
inputWarning: '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
newStates.inputText = textValue;
|
||||
this.setState(newStates);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (CHAT_KEY_MODIFIERS.includes(key)) {
|
||||
this.modifierKeyPressed = false;
|
||||
}
|
||||
this.setState({
|
||||
inputCharsLeft: CHAT_MAX_MESSAGE_LENGTH - textValue.length,
|
||||
});
|
||||
}
|
||||
|
||||
handleMessageInputBlur(event) {
|
||||
this.prepNewLine = false;
|
||||
this.modifierKeyPressed = false;
|
||||
}
|
||||
|
||||
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);
|
||||
this.handleMessageInputKeydown(event);
|
||||
}
|
||||
|
||||
handleSubmitChatButton(event) {
|
||||
@ -209,11 +229,15 @@ export default class ChatInput extends Component {
|
||||
|
||||
sendMessage() {
|
||||
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 newStates = {
|
||||
inputWarning: '',
|
||||
inputHTML: '',
|
||||
inputText: '',
|
||||
inputCharsLeft: CHAT_MAX_MESSAGE_LENGTH,
|
||||
};
|
||||
|
||||
handleSendMessage(message);
|
||||
@ -232,57 +256,51 @@ export default class ChatInput extends Component {
|
||||
}
|
||||
|
||||
render(props, state) {
|
||||
const { hasSentFirstChatMessage, inputWarning, inputHTML } = state;
|
||||
const { hasSentFirstChatMessage, inputCharsLeft, inputHTML } = state;
|
||||
const { inputEnabled } = props;
|
||||
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);
|
||||
return (
|
||||
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}
|
||||
id="message-input"
|
||||
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"
|
||||
<div
|
||||
id="message-input-wrap"
|
||||
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}
|
||||
innerRef=${this.formMessageInput}
|
||||
html=${inputHTML}
|
||||
disabled=${!inputEnabled}
|
||||
onChange=${this.handleContentEditableChange}
|
||||
onKeyDown=${this.handleMessageInputKeydown}
|
||||
onKeyUp=${this.handleMessageInputKeyup}
|
||||
onBlur=${this.handleMessageInputBlur}
|
||||
placeholderText=${placeholderText}
|
||||
innerRef=${this.formMessageInput}
|
||||
html=${inputHTML}
|
||||
disabled=${!inputEnabled}
|
||||
onChange=${this.handleContentEditableChange}
|
||||
onKeyDown=${this.handleMessageInputKeydown}
|
||||
onKeyUp=${this.handleMessageInputKeyup}
|
||||
onBlur=${this.handleMessageInputBlur}
|
||||
|
||||
onPaste=${this.handlePaste}
|
||||
/>
|
||||
|
||||
<div id="message-form-actions" class="flex flex-row justify-between items-center w-full">
|
||||
<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">
|
||||
onPaste=${this.handlePaste}
|
||||
/>
|
||||
</div>
|
||||
<div id="message-form-actions" class="absolute flex flex-col w-10 justify-end items-center">
|
||||
<button
|
||||
ref=${this.emojiPickerButton}
|
||||
id="emoji-button"
|
||||
class="mr-2 text-2xl cursor-pointer"
|
||||
class="text-3xl leading-3 cursor-pointer text-purple-600"
|
||||
type="button"
|
||||
style=${emojiButtonStyle}
|
||||
onclick=${this.handleEmojiButtonClick}
|
||||
disabled=${!inputEnabled}
|
||||
>😏</button>
|
||||
><img src="../../../img/smiley.png" /></button>
|
||||
|
||||
<button
|
||||
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>
|
||||
<span id="message-form-warning" class="text-red-600 text-xs">${inputCharsLeft}/${CHAT_MAX_MESSAGE_LENGTH}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
@ -5,9 +5,9 @@ const html = htm.bind(h);
|
||||
import Message from './message.js';
|
||||
import ChatInput from './chat-input.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 { URL_CHAT_HISTORY } from '../../utils/constants.js';
|
||||
import { URL_CHAT_HISTORY, MESSAGE_JUMPTOBOTTOM_BUFFER } from '../../utils/constants.js';
|
||||
|
||||
export default class Chat extends Component {
|
||||
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.scrollToBottom = this.scrollToBottom.bind(this);
|
||||
this.jumpToBottomPending = false;
|
||||
this.handleWindowResize = debounce(this.handleWindowResize.bind(this), 500);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setupWebSocketCallbacks();
|
||||
this.getChatHistory();
|
||||
|
||||
if (hasTouchScreen()) {
|
||||
// setVHvar();
|
||||
// window.addEventListener("orientationchange", setVHvar);
|
||||
}
|
||||
window.addEventListener('resize', this.handleWindowResize);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
@ -55,14 +51,13 @@ export default class Chat extends Component {
|
||||
}
|
||||
// scroll to bottom of messages list when new ones come in
|
||||
if (messages.length > prevMessages.length) {
|
||||
this.jumpToBottomPending = true;
|
||||
if (!prevMessages.length || this.checkShouldScroll()) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (hasTouchScreen()) {
|
||||
window.removeEventListener("orientationchange", setVHvar);
|
||||
}
|
||||
window.removeEventListener('resize', this.handleWindowResize);
|
||||
}
|
||||
|
||||
setupWebSocketCallbacks() {
|
||||
@ -168,6 +163,17 @@ export default class Chat extends Component {
|
||||
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) {
|
||||
const { username, messagesOnly, chatInputEnabled } = props;
|
||||
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) {
|
||||
return html`
|
||||
<div
|
||||
id="messages-container"
|
||||
ref=${this.scrollableMessagesContainer}
|
||||
class="py-1 overflow-auto"
|
||||
class="scrollbar-hidden py-1 overflow-auto"
|
||||
>
|
||||
${messageList}
|
||||
</div>
|
||||
@ -210,7 +208,7 @@ export default class Chat extends Component {
|
||||
<div
|
||||
id="messages-container"
|
||||
ref=${this.scrollableMessagesContainer}
|
||||
class="py-1 overflow-auto z-10"
|
||||
class="scrollbar-hidden py-1 overflow-auto z-10"
|
||||
>
|
||||
${messageList}
|
||||
</div>
|
||||
@ -223,6 +221,5 @@ export default class Chat extends Component {
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
||||
function replaceCaret(el) {
|
||||
export function replaceCaret(el) {
|
||||
// Place the caret at the end of the element
|
||||
const target = document.createTextNode('');
|
||||
el.appendChild(target);
|
||||
@ -69,8 +69,6 @@ export default class ContentEditable extends Component {
|
||||
props.innerRef !== nextProps.innerRef;
|
||||
}
|
||||
|
||||
|
||||
|
||||
componentDidUpdate() {
|
||||
const el = this.getDOMElement();
|
||||
if (!el) return;
|
||||
@ -118,6 +116,7 @@ export default class ContentEditable extends Component {
|
||||
this.el.current = current
|
||||
} : innerRef || this.el,
|
||||
onInput: this.emitChange,
|
||||
onFocus: this.props.onFocus || this.emitChange,
|
||||
onBlur: this.props.onBlur || this.emitChange,
|
||||
onKeyup: this.props.onKeyUp || this.emitChange,
|
||||
onKeydown: this.props.onKeyDown || this.emitChange,
|
||||
|
@ -34,7 +34,7 @@ export default class Message extends Component {
|
||||
${author}
|
||||
</div>
|
||||
<div
|
||||
class="message-text text-gray-300 font-normal"
|
||||
class="message-text text-gray-300 font-normal overflow-y-hidden"
|
||||
dangerouslySetInnerHTML=${
|
||||
{ __html: formattedMessage }
|
||||
}
|
||||
|
@ -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_PLACEHOLDER_TEXT = 'Message';
|
||||
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
|
||||
export const WIDTH_SINGLE_COL = 730;
|
||||
|
@ -30,7 +30,7 @@ export function jumpToBottom(element) {
|
||||
element.scrollTo({
|
||||
top: element.scrollHeight,
|
||||
left: 0,
|
||||
behavior: 'smooth'
|
||||
behavior: 'auto'
|
||||
});
|
||||
}, 50, element);
|
||||
}
|
||||
|
@ -20,11 +20,12 @@ a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
.scrollbar-hidden::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
#app-container * {
|
||||
transition: all .25s;
|
||||
}
|
||||
@ -48,6 +49,7 @@ button[disabled] {
|
||||
header {
|
||||
height: var(--header-height);
|
||||
background-color: var(--header-bg-color);
|
||||
display: block;
|
||||
}
|
||||
|
||||
#logo-container {
|
||||
@ -125,8 +127,6 @@ header {
|
||||
height: calc((9 / 16) * var(--content-width));
|
||||
}
|
||||
|
||||
|
||||
|
||||
.short-wide.chat #video-container {
|
||||
height: calc(100vh - var(--header-height) - 3rem);
|
||||
min-height: auto;
|
||||
@ -151,15 +151,11 @@ header {
|
||||
z-index: 40;
|
||||
}
|
||||
.single-col #chat-container {
|
||||
position: relative;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 800px;
|
||||
}
|
||||
/* .single-col #video-container {
|
||||
min-height: auto;
|
||||
width: 100%;
|
||||
} */
|
||||
|
||||
.single-col.chat #video-container,
|
||||
.single-col.no-chat #video-container,
|
||||
@ -180,14 +176,34 @@ header {
|
||||
.single-col.chat #user-content {
|
||||
display: none;
|
||||
}
|
||||
.single-col #messages-container {
|
||||
flex-grow: 2;
|
||||
margin-top: calc(var(--video-container-height));
|
||||
}
|
||||
.single-col #message-input-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.single-col #message-input {
|
||||
height: 3rem;
|
||||
.single-col #stream-info {
|
||||
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 */
|
||||
@ -205,7 +221,4 @@ header {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#stream-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -14,13 +14,30 @@
|
||||
width: var(--right-col-width);
|
||||
}
|
||||
|
||||
#messages-container {
|
||||
padding-bottom: 10rem;
|
||||
#message-input-wrap {
|
||||
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 {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
@ -46,6 +63,8 @@
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
#message-input::selection { background:#d7ddf4; }
|
||||
|
||||
|
||||
/* When chat is disabled (contenteditable=false) chat input div should appear 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 {
|
||||
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 {
|
||||
height: 3.0em;
|
||||
@ -77,6 +129,9 @@
|
||||
/* MESSAGE TEXT HTML */
|
||||
/* MESSAGE TEXT HTML */
|
||||
/* MESSAGE TEXT HTML */
|
||||
.message-text {
|
||||
word-break: break-word;
|
||||
}
|
||||
.message-text a {
|
||||
color: #7F9CF5; /* indigo-400 */
|
||||
}
|
||||
@ -102,15 +157,6 @@
|
||||
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 {
|
||||
width: 100%;
|
||||
border-radius: .25rem;
|
||||
|
Loading…
x
Reference in New Issue
Block a user