Initial setup for standalone chat with Preact.
- 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 consistency
This commit is contained in:
parent
2c1bc52487
commit
dad802f19a
117
webroot/js/chat/chat.js
Normal file
117
webroot/js/chat/chat.js
Normal file
@ -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>
|
||||
`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
52
webroot/js/chat/message.js
Normal file
52
webroot/js/chat/message.js
Normal file
@ -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>
|
||||
`);
|
||||
}
|
||||
}
|
54
webroot/js/chat/standalone.js
Normal file
54
webroot/js/chat/standalone.js
Normal file
@ -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>
|
||||
`);
|
||||
}
|
||||
|
||||
}
|
108
webroot/js/chat/user-info.js
Normal file
108
webroot/js/chat/user-info.js
Normal file
@ -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>
|
||||
`);
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
|
||||
const LOCAL_TEST = window.location.href.indexOf('localhost:') >= 0;
|
||||
const URL_PREFIX = LOCAL_TEST ? 'http://localhost:8080' : '';
|
||||
export const LOCAL_TEST = window.location.href.indexOf('localhost:') >= 0;
|
||||
export const URL_PREFIX = LOCAL_TEST ? 'http://localhost:8080' : '';
|
||||
|
||||
const POSTER_DEFAULT = `${URL_PREFIX}/img/logo.png`;
|
||||
const POSTER_THUMB = `${URL_PREFIX}/thumbnail.jpg`;
|
||||
export const POSTER_DEFAULT = `${URL_PREFIX}/img/logo.png`;
|
||||
export const POSTER_THUMB = `${URL_PREFIX}/thumbnail.jpg`;
|
||||
|
||||
const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer
|
||||
export const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer
|
||||
|
||||
function getLocalStorage(key) {
|
||||
export function getLocalStorage(key) {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch (e) {
|
||||
@ -15,7 +15,7 @@ function getLocalStorage(key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function setLocalStorage(key, value) {
|
||||
export function setLocalStorage(key, value) {
|
||||
try {
|
||||
if (value !== "" && value !== null) {
|
||||
localStorage.setItem(key, value);
|
||||
@ -27,12 +27,12 @@ function setLocalStorage(key, value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function clearLocalStorage(key) {
|
||||
export function clearLocalStorage(key) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
// jump down to the max height of a div, with a slight delay
|
||||
function jumpToBottom(element) {
|
||||
export function jumpToBottom(element) {
|
||||
if (!element) return;
|
||||
|
||||
setTimeout(() => {
|
||||
@ -45,11 +45,11 @@ function jumpToBottom(element) {
|
||||
}
|
||||
|
||||
// convert newlines to <br>s
|
||||
function addNewlines(str) {
|
||||
export function addNewlines(str) {
|
||||
return str.replace(/(?:\r\n|\r|\n)/g, '<br />');
|
||||
}
|
||||
|
||||
function pluralize(string, count) {
|
||||
export function pluralize(string, count) {
|
||||
if (count === 1) {
|
||||
return string;
|
||||
} else {
|
||||
@ -60,12 +60,12 @@ function pluralize(string, count) {
|
||||
|
||||
// Trying to determine if browser is mobile/tablet.
|
||||
// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
|
||||
function hasTouchScreen() {
|
||||
export function hasTouchScreen() {
|
||||
var hasTouchScreen = false;
|
||||
if ("maxTouchPoints" in navigator) {
|
||||
if ("maxTouchPoints" in navigator) {
|
||||
hasTouchScreen = navigator.maxTouchPoints > 0;
|
||||
} else if ("msMaxTouchPoints" in navigator) {
|
||||
hasTouchScreen = navigator.msMaxTouchPoints > 0;
|
||||
hasTouchScreen = navigator.msMaxTouchPoints > 0;
|
||||
} else {
|
||||
var mQ = window.matchMedia && matchMedia("(pointer:coarse)");
|
||||
if (mQ && mQ.media === "(pointer:coarse)") {
|
||||
@ -85,20 +85,20 @@ function hasTouchScreen() {
|
||||
}
|
||||
|
||||
// generate random avatar from https://robohash.org
|
||||
function generateAvatar(hash) {
|
||||
export function generateAvatar(hash) {
|
||||
const avatarSource = 'https://robohash.org/';
|
||||
const optionSize = '?size=80x80';
|
||||
const optionSet = '&set=set3';
|
||||
const optionSet = '&set=set3';
|
||||
const optionBg = ''; // or &bgset=bg1 or bg2
|
||||
|
||||
return avatarSource + hash + optionSize + optionSet + optionBg;
|
||||
}
|
||||
|
||||
function generateUsername() {
|
||||
export function generateUsername() {
|
||||
return `User ${(Math.floor(Math.random() * 42) + 1)}`;
|
||||
}
|
||||
|
||||
function secondsToHMMSS(seconds = 0) {
|
||||
export function secondsToHMMSS(seconds = 0) {
|
||||
const finiteSeconds = Number.isFinite(+seconds) ? Math.abs(seconds) : 0;
|
||||
|
||||
const hours = Math.floor(finiteSeconds / 3600);
|
||||
@ -113,13 +113,15 @@ function secondsToHMMSS(seconds = 0) {
|
||||
return hoursString + minString + secsString;
|
||||
}
|
||||
|
||||
function setVHvar() {
|
||||
export function setVHvar() {
|
||||
var vh = window.innerHeight * 0.01;
|
||||
// Then we set the value in the --vh custom property to the root of the document
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
console.log("== new vh", vh)
|
||||
}
|
||||
|
||||
function doesObjectSupportFunction(object, functionName) {
|
||||
export function doesObjectSupportFunction(object, functionName) {
|
||||
return typeof object[functionName] === "function";
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
|
7
webroot/js/utils/chat.js
Normal file
7
webroot/js/utils/chat.js
Normal file
@ -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.';
|
@ -8,4 +8,4 @@ export default {
|
||||
PING: 'PING',
|
||||
NAME_CHANGE: 'NAME_CHANGE',
|
||||
PONG: 'PONG'
|
||||
}
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
function getHashFromString(string) {
|
||||
export function getHashFromString(string) {
|
||||
let hash = 1;
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
const codepoint = string.charCodeAt(i);
|
||||
@ -8,7 +8,7 @@ function getHashFromString(string) {
|
||||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
function digitsFromNumber(number) {
|
||||
export function digitsFromNumber(number) {
|
||||
const numberString = number.toString();
|
||||
let digits = [];
|
||||
|
||||
@ -50,7 +50,7 @@ function digitsFromNumber(number) {
|
||||
// return filename + '.svg';
|
||||
// }
|
||||
|
||||
function colorForString(str) {
|
||||
export function colorForString(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
// eslint-disable-next-line
|
||||
@ -65,7 +65,7 @@ function colorForString(str) {
|
||||
return colour;
|
||||
}
|
||||
|
||||
function messageBubbleColorForString(str) {
|
||||
export function messageBubbleColorForString(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
// eslint-disable-next-line
|
||||
@ -85,4 +85,4 @@ function messageBubbleColorForString(str) {
|
||||
b: parseInt(result[3], 16),
|
||||
} : null;
|
||||
return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ', 0.4)';
|
||||
}
|
||||
}
|
22
webroot/standalone-chat.html
Normal file
22
webroot/standalone-chat.html
Normal file
@ -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>
|
87
webroot/styles/message.css
Normal file
87
webroot/styles/message.css
Normal file
@ -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
|
||||
}
|
102
webroot/styles/user-content.css
Normal file
102
webroot/styles/user-content.css
Normal file
@ -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…
x
Reference in New Issue
Block a user