forked from Github/ocrcc-chatbox
restyle chatbox and some refactoring
This commit is contained in:
parent
37455346f3
commit
dcd4e4a9ba
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
22
src/components/dock.jsx
Normal 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
33
src/components/header.jsx
Normal 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;
|
@ -1,5 +1,4 @@
|
||||
@import "variables";
|
||||
@import "loader";
|
||||
@import "widget";
|
||||
@import "chat";
|
||||
@import "dark_mode";
|
@ -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;
|
@ -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();
|
||||
});
|
||||
});
|
@ -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',
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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',
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user