restyle chatbox and some refactoring

This commit is contained in:
Sharon Kennedy 2020-02-23 23:12:47 -05:00
parent 37455346f3
commit dcd4e4a9ba
14 changed files with 385 additions and 385 deletions

View File

@ -10,10 +10,10 @@
</p>
<p style="font-family:sans-serif; padding: 3rem 5rem;">Look down!</p>
<script src="./widget.js"></script>
<script src="./chatbox.js"></script>
<script>
EmbeddableWidget.mount({
EmbeddableChatbox.mount({
termsUrl: "https://tosdr.org/",
privacyStatement: "This chat application does not collect any of your personal data or any data from your use of this service.",
matrixServerUrl: "https://matrix.rhok.space",

View File

@ -1,6 +1,178 @@
@import url('https://fonts.googleapis.com/css?family=Assistant&display=swap');
@keyframes slideInUp {
from {
transform: translate3d(0, 100%, 0);
display: inherit;
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
@keyframes slideOutDown {
from {
transform: translate3d(0, 0, 0);
}
to {
display: none;
visibility: hidden;
transform: translate3d(0, 100%, 0);
}
}
.docked-widget {
position: fixed;
bottom: 10px;
right: 10px;
z-index: 9999;
width: 400px;
}
.dock {
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
width: 400px;
max-width: calc(100vw - 10px);
color: $white;
font-family: $theme-font;
font-size: 1rem;
border: none;
color: $white;
font-size: 1rem;
line-height: 1;
#open-chatbox-label {
background: $theme-color;
padding: 0.75rem 1.5rem;
flex: 1 1 auto;
text-align: left;
margin-right: 0.25rem;
}
.label-icon {
background: $theme-color;
height: 40px;
width: 40px;
border-radius: 40px;
display: flex;
justify-content: center;
align-items: center;
}
}
.widget {
width: 400px;
max-width: calc(100vw - 10px);
border-bottom: none;
animation-duration: 0.2s;
animation-fill-mode: forwards;
&-entering {
animation-name: slideInUp;
}
&-entered {
display: inherit;
visibility: visible;
}
&-exiting {
animation-name: slideOutDown;
}
&-exited {
display: none;
visibility: hidden;
}
&-header {
display: flex;
align-items: center;
margin-bottom: 0.2rem;
justify-content: flex-end;
&-title {
display: flex;
flex-grow: 1;
}
&-minimize {
cursor: pointer;
display: flex;
align-items: center;
justify-content: flex-start;
border: 1px solid $dark-color !important;
background: $white;
color: $dark-color;
flex: 1 1 auto;
font-family: $theme-font;
font-size: 1rem;
padding: 0.5rem;
}
&-close {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid $dark-color !important;
background: $white;
border-radius: 40px;
height: 40px;
width: 40px;
margin-left: 0.2rem;
color: $dark-color;
}
}
&-body {
background: white;
padding: 10px;
height: 150px;
}
&-footer {
background: green;
line-height: 30px;
padding-left: 10px;
}
}
.btn-icon {
font-size: 1.5rem;
line-height: 1;
transform: rotateX(0deg);
transition: all 0.5s linear;
display: flex;
align-items: center;
justify-content: center;
}
.arrow {
margin-right: 0.5rem;
transform: translateY(5px);
&.opened {
color: $dark-color;
transform: rotateX(180deg) translateY(5px);
}
}
@media screen and (max-width: 420px){
.docked-widget {
right: 0;
left: 0;
}
.dock, .widget {
width: 100vw;
max-width: 100vw;
}
}
#ocrcc-chatbox {
font-family: $theme-font;
@ -9,24 +181,25 @@
height: 60vh;
max-height: 100vh;
min-height: 180px;
border: 1px solid $theme-color;
a {
color: inherit;
}
.message-window {
background-color: $light-color;
background-color: $white;
border: 1px solid $dark-color;
flex: 1 1 auto;
padding: 0.5rem;
overflow: auto;
display: flex;
flex-direction: column-reverse;
justify-content: space-between;
margin-bottom: 0.2rem;
}
.notices {
color: $dark-color;
color: $gray-color;
font-size: 0.9rem;
> div {
@ -48,7 +221,7 @@
}
&.from-bot {
color: $dark-color;
color: $gray-color;
font-size: 0.9rem;
}
@ -71,8 +244,8 @@
justify-content: flex-start;
.text {
border: 1px solid $theme-color;
background-color: $white;
border: 1px solid $light-color;
background-color: $light-color;
color: $dark-color;
border-radius: 15px 15px 15px 0;
margin-right: 10%;
@ -88,17 +261,18 @@
display: flex;
align-items: center;
margin-bottom: 0;
border-top: 1px solid $theme-color;
}
input[type="text"] {
font-size: 1rem;
padding: 0.5rem 1rem;
padding: 0.5rem;
border: none;
flex: 1 0 auto;
border: none;
border: 1px solid $dark-color;
background: $light-color;
color: $dark-color;
font-family: $theme-font;
margin-right: 0.2rem;
&:focus {
border: none;
@ -112,7 +286,7 @@
font-size: 1rem;
color: $white;
font-weight: bold;
border: none;
border: 1px solid $theme-color;
font-family: $theme-font;
cursor: pointer;
}

View File

@ -1,21 +1,23 @@
@media (prefers-color-scheme: dark) {
.dock {
background: $dark-theme-color;
}
.loader {
color: $dark-theme-color;
}
#ocrcc-chatbox {
border: 1px solid $dark-theme-color;
.btn-icon {
color: $light-text-color;
}
.widget-header {
background-color: $dark-theme-color;
.widget-header-minimize, .widget-header-close {
background: $dark-background-color;
color: $light-text-color;
border: 1px solid $white;
}
.message-window {
background-color: $dark-background-color;
border: 1px solid $white;
}
.notices {
@ -30,9 +32,9 @@
&.from-me {
.text {
background-color: $light-background-color;
color: $dark-text-color;
border: 1px solid $light-text-color;
background-color: $theme-color;
color: $light-text-color;
border: 1px solid $theme-color;
}
}
@ -40,24 +42,26 @@
.text {
background-color: $dark-theme-color;
color: $light-text-color;
border: 1px solid $light-text-color;
border: 1px solid $dark-theme-color;
}
}
}
.input-window {
form {
border-top: 1px solid $dark-theme-color;
input[type="text"] {
background-color: $dark-theme-color;
color: $light-text-color;
border: 1px solid $white;
}
input[type="text"] {
background-color: $light-background-color;
color: $dark-text-color;
::placeholder {
color: transparentize($light-text-color, 0.3);
}
input[type="submit"] {
background-color: $dark-theme-color;
background-color: $theme-color;
color: $light-text-color;
border: 1px solid $white;
}
}
}

View File

@ -1,11 +1,16 @@
$light-color: #f6faff;
@import url('https://fonts.googleapis.com/css?family=Poppins&display=swap');
$purple: #785BEC;
$charcoal: #828282;
$light-color: #F2F2F2;
$gray-color: $charcoal;
$dark-color: #04090F;
$yellow: #FFFACD;
$dark-blue: #2660A4;
$white: #ffffff;
$highlight-color: $yellow;
$theme-color: $dark-blue;
$theme-font: 'Assistant', 'Helvetica', sans-serif;
$theme-color: $purple;
$theme-font: 'Poppins', 'Helvetica', sans-serif;
$drop-shadow-color: #BDBEBF;
/* Dark mode colors */
@ -14,4 +19,4 @@ $dark-background-color: #0F1116;
$light-background-color: #ffffff;
$light-text-color: #ffffff;
$dark-text-color: #0F1116;
$dark-theme-color: #333C4B;
$dark-theme-color: #4F4F4F;

View File

@ -1,137 +0,0 @@
@keyframes slideInUp {
from {
transform: translate3d(0, 100%, 0);
display: inherit;
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
@keyframes slideOutDown {
from {
transform: translate3d(0, 0, 0);
}
to {
display: none;
visibility: hidden;
transform: translate3d(0, 100%, 0);
}
}
.docked-widget {
position: fixed;
bottom: 0px;
right: 10px;
z-index: 9999;
}
.dock {
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem;
width: 400px;
max-width: calc(100vw - 10px);
background: $theme-color;
color: $white;
font-family: $theme-font;
font-size: 1rem;
border: none;
color: $white;
font-size: 1rem;
line-height: 1;
box-shadow: 0 2px 15px $drop-shadow-color;
}
.widget {
width: 400px;
max-width: calc(100vw - 10px);
border-bottom: none;
animation-duration: 0.2s;
animation-fill-mode: forwards;
box-shadow: 0 2px 15px $drop-shadow-color;
&-entering {
animation-name: slideInUp;
}
&-entered {
display: inherit;
visibility: visible;
}
&-exiting {
animation-name: slideOutDown;
}
&-exited {
display: none;
visibility: hidden;
}
&-header {
background: $theme-color;
color: $white;
padding: 0.5rem;
display: flex;
align-items: center;
&-title {
display: flex;
flex-grow: 1;
}
&-icon {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
}
}
&-body {
background: white;
padding: 10px;
height: 150px;
}
&-footer {
background: green;
line-height: 30px;
padding-left: 10px;
}
}
.arrow {
transform: rotateX(0deg);
transition: all 0.5s linear;
color: $white;
font-size: 1rem;
line-height: 1;
&.opened {
transform: rotateX(180deg);
}
&.closed {
transform: rotateX(0deg) translateY(25%);
}
}
@media screen and (max-width: 420px){
.docked-widget {
right: 0;
left: 0;
}
.dock, .widget {
width: 100vw;
max-width: 100vw;
}
}

View File

@ -1,5 +1,6 @@
import React from "react"
import PropTypes from "prop-types"
import { Transition } from 'react-transition-group';
import * as util from "util";
import * as os from "os";
import * as path from "path";
@ -12,6 +13,10 @@ import * as matrix from "matrix-js-sdk";
import {uuid} from "uuidv4"
import Message from "./message";
import Dock from "./dock";
import Header from "./header";
import './styles.scss';
const DEFAULT_MATRIX_SERVER = "https://matrix.rhok.space"
@ -28,30 +33,59 @@ const DEFAULT_THEME = {
placement: "right"
};
const initialState = {
opened: false,
showDock: true,
client: null,
ready: false,
accessToken: null,
userId: null,
messages: [],
inputValue: "",
errors: [],
roomId: null,
typingStatus: null,
}
class ChatBox extends React.Component {
constructor(props) {
super(props)
const client = matrix.createClient(this.props.matrixServerUrl)
this.state = {
client: null,
ready: false,
accessToken: null,
userId: null,
messages: [],
inputValue: "",
errors: [],
roomId: null,
typingStatus: null,
}
this.state = initialState
this.chatboxInput = React.createRef();
}
handleToggleOpen = () => {
this.setState((prev) => {
let { showDock } = prev;
if (!prev.opened) {
showDock = false;
}
return {
showDock,
opened: !prev.opened,
};
});
}
handleWidgetExit = () => {
this.setState({
showDock: true,
});
}
handleExitChat = () => {
this.leaveRoom()
.then(() => {
this.setState(initialState)
})
.catch(err => console.log("Error leaving room", err))
}
leaveRoom = () => {
if (this.state.roomId) {
this.state.client.leave(this.state.roomId).then(data => {
console.log("Left room", data)
})
return this.state.client.leave(this.state.roomId)
}
}
@ -203,13 +237,13 @@ class ChatBox extends React.Component {
handleEscape = (e) => {
if (e.keyCode === 27 && this.props.opened) {
this.props.handleToggleOpen()
if (e.keyCode === 27 && this.state.opened) {
this.handleToggleOpen()
}
}
componentDidUpdate(prevProps, prevState) {
if (prevState.client !== this.state.client) {
if (this.state.client && prevState.client !== this.state.client) {
this.createRoom()
this.state.client.once('sync', (state, prevState, res) => {
@ -219,19 +253,6 @@ class ChatBox extends React.Component {
});
this.state.client.on("Room.timeline", (event, room, toStartOfTimeline) => {
// if (event.getType() === "m.room.message") {
// if (event.status === "not sent") {
// return console.log("message not sent!", event)
// }
// if (event.isEncrypted()) {
// return console.log("message encrypted")
// }
// this.handleMessageEvent(event)
// }
if (event.getType() === "m.room.encryption") {
const msgList = [...this.state.messages]
const encryptionMsg = {
@ -263,17 +284,11 @@ class ChatBox extends React.Component {
});
}
// if (prevState.roomId !== this.state.roomId) {
// this.setState({
// isRoomEncrypted: this.state.client.isRoomEncrypted(this.state.roomId)
// })
// }
if (!prevState.ready && this.state.ready) {
this.chatboxInput.current.focus()
}
if (this.state.client === null && prevProps.status !== "entered" && this.props.status === "entered") {
if (this.state.client === null && !prevState.opened && this.state.opened) {
this.initializeClient()
}
}
@ -303,58 +318,55 @@ class ChatBox extends React.Component {
}
render() {
const { ready, messages, inputValue, userId, typingStatus } = this.state;
const { opened, handleToggleOpen, privacyStatement, termsUrl } = this.props;
const { ready, messages, inputValue, userId, roomId, typingStatus, opened, showDock } = this.state;
const inputLabel = 'Send a message...'
return (
<div id="ocrcc-chatbox" aria-haspopup="dialog" aria-label="Open support chat">
<div className="widget-header">
<div className="widget-header-title">
Support Chat
</div>
<button
type="button"
className={`widget-header-icon`}
onClick={handleToggleOpen}
onKeyPress={handleToggleOpen}
>
<span className={`arrow ${opened ? "opened" : "closed"}`}></span>
</button>
</div>
<div className="docked-widget" role="complementary">
<Transition in={opened} timeout={250} onExited={this.handleWidgetExit}>
{(status) => (
<div className={`widget widget-${status}`} aria-hidden={!opened}>
<div id="ocrcc-chatbox" aria-haspopup="dialog">
<Header handleToggleOpen={this.handleToggleOpen} opened={opened} handleExitChat={this.handleExitChat} />
<div className="message-window">
<div className="messages">
{
ready ?
messages.map((message, index) => {
return(
<Message key={message.id} message={message} userId={userId} botId={BOT_USERNAME} />
)
}) :
<div className="loader">loading...</div>
}
{ typingStatus &&
<div className="notices">
<div role="status">{typingStatus}</div>
<div className="message-window">
<div className="messages">
{
ready ?
messages.map((message, index) => {
return(
<Message key={message.id} message={message} userId={userId} botId={BOT_USERNAME} />
)
}) :
<div className="loader">loading...</div>
}
{ typingStatus &&
<div className="notices">
<div role="status">{typingStatus}</div>
</div>
}
</div>
</div>
<div className="input-window">
<form onSubmit={this.handleSubmit}>
<input
type="text"
onChange={this.handleInputChange}
value={inputValue}
aria-label={inputLabel}
placeholder={inputLabel}
autoFocus={true}
ref={this.chatboxInput}
/>
<input type="submit" value="Send" />
</form>
</div>
</div>
}
</div>
</div>
<div className="input-window">
<form onSubmit={this.handleSubmit}>
<input
type="text"
onChange={this.handleInputChange}
value={inputValue}
aria-label={inputLabel}
placeholder={inputLabel}
autoFocus={true}
ref={this.chatboxInput}
/>
<input type="submit" value="Send" />
</form>
</div>
</div>
)}
</Transition>
{showDock && !roomId && <Dock handleToggleOpen={this.handleToggleOpen} />}
{showDock && roomId && <Header handleToggleOpen={this.handleToggleOpen} opened={opened} handleExitChat={this.handleExitChat} />}
</div>
);
}

22
src/components/dock.jsx Normal file
View File

@ -0,0 +1,22 @@
import React from "react"
import PropTypes from "prop-types"
const Dock = ({ handleToggleOpen }) => {
return(
<button
type="button"
className="dock"
onClick={handleToggleOpen}
onKeyPress={handleToggleOpen}
aria-labelledby="open-chatbox-label"
>
<div id="open-chatbox-label">Start a new chat</div>
<div className="label-icon">
<div className={`btn-icon`} aria-label={`Open support chat window`}>+</div>
</div>
</button>
)
}
export default Dock;

33
src/components/header.jsx Normal file
View File

@ -0,0 +1,33 @@
import React from "react"
import PropTypes from "prop-types"
const Header = ({ handleToggleOpen, handleExitChat, opened }) => {
return(
<div className="widget-header">
<button
type="button"
className={`widget-header-minimize`}
onClick={handleToggleOpen}
onKeyPress={handleToggleOpen}
aria-label="Minimize the chat window"
title="Minimize the chat window"
>
<span className={`btn-icon arrow ${opened ? "opened" : "closed"}`}></span>
<span>{`${opened ? "Hide" : "Show"} the chat`}</span>
</button>
<button
type="button"
className={`widget-header-close`}
onClick={handleExitChat}
onKeyPress={handleExitChat}
aria-label="Exit the chat"
title="Exit the chat"
>
<span className={`btn-icon`}>×</span>
</button>
</div>
)
}
export default Header;

View File

@ -1,5 +1,4 @@
@import "variables";
@import "loader";
@import "widget";
@import "chat";
@import "dark_mode";

View File

@ -1,76 +0,0 @@
import React, { Component } from 'react';
import { Transition } from 'react-transition-group';
import Chatbox from './chatbox';
import './styles.scss';
class Widget extends Component {
constructor(props) {
super(props);
this.state = {
opened: false,
showDock: true,
};
}
handleToggleOpen = () => {
this.setState((prev) => {
let { showDock } = prev;
if (!prev.opened) {
showDock = false;
}
return {
showDock,
opened: !prev.opened,
};
});
}
handleWidgetExit = () => {
this.setState({
showDock: true,
});
}
render() {
const { opened, showDock } = this.state;
return (
<div className="docked-widget" role="complementary">
<Transition in={opened} timeout={250} onExited={this.handleWidgetExit}>
{(status) => (
<div className={`widget widget-${status}`} aria-hidden={!opened}>
<Chatbox
handleToggleOpen={this.handleToggleOpen}
opened={opened}
status={status}
{...this.props} // eslint-disable-line
/>
</div>
)}
</Transition>
{showDock && (
<button
type="button"
className="dock"
onClick={this.handleToggleOpen}
onKeyPress={this.handleToggleOpen}
aria-labelledby="open-chatbox-label"
>
<span id="open-chatbox-label">Open support chat</span>
<span className={`arrow ${opened ? 'opened' : 'closed'}`} aria-label={`${opened ? 'Close' : 'Open'} support chat window`}></span>
</button>
)}
</div>
);
}
}
Widget.propTypes = {
};
Widget.defaultProps = {
};
export default Widget;

View File

@ -1,36 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import Widget from './widget';
import { waitForSelection } from '../test-helpers';
describe('<Widget />', () => {
test('open/close', async () => {
const widgetDom = mount(<Widget />);
expect(widgetDom).toMatchSnapshot();
{
const dockAnchorEl = widgetDom.find('button.dock');
expect(dockAnchorEl).toHaveLength(1);
// open widget
dockAnchorEl.simulate('click');
}
expect(widgetDom).toMatchSnapshot();
// dock does not exist anymore
expect(widgetDom.find('a.dock')).toHaveLength(0);
const closeAnchorEl = await waitForSelection(widgetDom, 'button.widget-header-icon');
expect(closeAnchorEl).toHaveLength(1);
// close widget
closeAnchorEl.simulate('click');
{
const dockAnchorEl = await waitForSelection(widgetDom, 'button.dock');
expect(dockAnchorEl).toHaveLength(1);
}
expect(widgetDom).toMatchSnapshot();
});
});

View File

@ -1,12 +1,12 @@
import EmbeddableWidget from './embeddable-widget';
import EmbeddableChatbox from './embeddable-chatbox';
export default function bookmarklet() {
if (window.EmbeddableWidget) {
if (window.EmbeddableChatbox) {
return;
}
window.EmbeddableWidget = EmbeddableWidget;
window.EmbeddableChatbox = EmbeddableChatbox;
EmbeddableWidget.mount({
EmbeddableChatbox.mount({
termsUrl: 'https://tosdr.org/',
privacyStatement: 'This chat application does not collect any of your personal data or any data from your use of this service.',
matrixServerUrl: 'https://matrix.rhok.space',

View File

@ -1,17 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Widget from '../components/widget';
import Chatbox from '../components/chatbox';
import '../../vendor/cleanslate.css';
export default class EmbeddableWidget {
export default class EmbeddableChatbox {
static el;
static mount({ parentElement = null, ...props } = {}) {
const component = <Widget {...props} />; // eslint-disable-line
const component = <Chatbox {...props} />; // eslint-disable-line
function doRender() {
if (EmbeddableWidget.el) {
throw new Error('EmbeddableWidget is already mounted, unmount first');
if (EmbeddableChatbox.el) {
throw new Error('EmbeddableChatbox is already mounted, unmount first');
}
const el = document.createElement('div');
@ -26,7 +26,7 @@ export default class EmbeddableWidget {
component,
el,
);
EmbeddableWidget.el = el;
EmbeddableChatbox.el = el;
}
if (document.readyState === 'complete') {
@ -39,11 +39,11 @@ export default class EmbeddableWidget {
}
static unmount() {
if (!EmbeddableWidget.el) {
throw new Error('EmbeddableWidget is not mounted, mount first');
if (!EmbeddableChatbox.el) {
throw new Error('EmbeddableChatbox is not mounted, mount first');
}
ReactDOM.unmountComponentAtNode(EmbeddableWidget.el);
EmbeddableWidget.el.parentNode.removeChild(EmbeddableWidget.el);
EmbeddableWidget.el = null;
ReactDOM.unmountComponentAtNode(EmbeddableChatbox.el);
EmbeddableChatbox.el.parentNode.removeChild(EmbeddableChatbox.el);
EmbeddableChatbox.el = null;
}
}

View File

@ -77,12 +77,12 @@ const defaultConfig = {
module.exports = [{
...defaultConfig,
entry: './src/outputs/embeddable-widget.js',
entry: './src/outputs/embeddable-chatbox.js',
output: {
path: distDir,
publicPath: '/',
filename: 'widget.js',
library: 'EmbeddableWidget',
filename: 'chatbox.js',
library: 'EmbeddableChatbox',
libraryExport: 'default',
libraryTarget: 'window',
},