mirror of
https://github.com/Safe-Support-Chat/ocrcc-chatbox
synced 2024-11-25 20:24:54 +00:00
restyle chatbox and some refactoring
This commit is contained in:
parent
37455346f3
commit
dcd4e4a9ba
@ -10,10 +10,10 @@
|
|||||||
</p>
|
</p>
|
||||||
<p style="font-family:sans-serif; padding: 3rem 5rem;">Look down!</p>
|
<p style="font-family:sans-serif; padding: 3rem 5rem;">Look down!</p>
|
||||||
|
|
||||||
<script src="./widget.js"></script>
|
<script src="./chatbox.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
EmbeddableWidget.mount({
|
EmbeddableChatbox.mount({
|
||||||
termsUrl: "https://tosdr.org/",
|
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.",
|
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",
|
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 {
|
#ocrcc-chatbox {
|
||||||
font-family: $theme-font;
|
font-family: $theme-font;
|
||||||
@ -9,24 +181,25 @@
|
|||||||
height: 60vh;
|
height: 60vh;
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
min-height: 180px;
|
min-height: 180px;
|
||||||
border: 1px solid $theme-color;
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-window {
|
.message-window {
|
||||||
background-color: $light-color;
|
background-color: $white;
|
||||||
|
border: 1px solid $dark-color;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notices {
|
.notices {
|
||||||
color: $dark-color;
|
color: $gray-color;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
@ -48,7 +221,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.from-bot {
|
&.from-bot {
|
||||||
color: $dark-color;
|
color: $gray-color;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,8 +244,8 @@
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
border: 1px solid $theme-color;
|
border: 1px solid $light-color;
|
||||||
background-color: $white;
|
background-color: $light-color;
|
||||||
color: $dark-color;
|
color: $dark-color;
|
||||||
border-radius: 15px 15px 15px 0;
|
border-radius: 15px 15px 15px 0;
|
||||||
margin-right: 10%;
|
margin-right: 10%;
|
||||||
@ -88,17 +261,18 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
border-top: 1px solid $theme-color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem;
|
||||||
border: none;
|
border: none;
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
border: none;
|
border: 1px solid $dark-color;
|
||||||
|
background: $light-color;
|
||||||
color: $dark-color;
|
color: $dark-color;
|
||||||
font-family: $theme-font;
|
font-family: $theme-font;
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border: none;
|
border: none;
|
||||||
@ -112,7 +286,7 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: $white;
|
color: $white;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border: none;
|
border: 1px solid $theme-color;
|
||||||
font-family: $theme-font;
|
font-family: $theme-font;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.dock {
|
|
||||||
background: $dark-theme-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
color: $dark-theme-color;
|
color: $dark-theme-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ocrcc-chatbox {
|
#ocrcc-chatbox {
|
||||||
border: 1px solid $dark-theme-color;
|
.btn-icon {
|
||||||
|
color: $light-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
.widget-header {
|
.widget-header-minimize, .widget-header-close {
|
||||||
background-color: $dark-theme-color;
|
background: $dark-background-color;
|
||||||
|
color: $light-text-color;
|
||||||
|
border: 1px solid $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-window {
|
.message-window {
|
||||||
background-color: $dark-background-color;
|
background-color: $dark-background-color;
|
||||||
|
border: 1px solid $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notices {
|
.notices {
|
||||||
@ -30,9 +32,9 @@
|
|||||||
|
|
||||||
&.from-me {
|
&.from-me {
|
||||||
.text {
|
.text {
|
||||||
background-color: $light-background-color;
|
background-color: $theme-color;
|
||||||
color: $dark-text-color;
|
color: $light-text-color;
|
||||||
border: 1px solid $light-text-color;
|
border: 1px solid $theme-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,24 +42,26 @@
|
|||||||
.text {
|
.text {
|
||||||
background-color: $dark-theme-color;
|
background-color: $dark-theme-color;
|
||||||
color: $light-text-color;
|
color: $light-text-color;
|
||||||
border: 1px solid $light-text-color;
|
border: 1px solid $dark-theme-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-window {
|
.input-window {
|
||||||
form {
|
input[type="text"] {
|
||||||
border-top: 1px solid $dark-theme-color;
|
background-color: $dark-theme-color;
|
||||||
|
color: $light-text-color;
|
||||||
|
border: 1px solid $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
::placeholder {
|
||||||
background-color: $light-background-color;
|
color: transparentize($light-text-color, 0.3);
|
||||||
color: $dark-text-color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="submit"] {
|
input[type="submit"] {
|
||||||
background-color: $dark-theme-color;
|
background-color: $theme-color;
|
||||||
color: $light-text-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;
|
$dark-color: #04090F;
|
||||||
$yellow: #FFFACD;
|
$yellow: #FFFACD;
|
||||||
$dark-blue: #2660A4;
|
$dark-blue: #2660A4;
|
||||||
$white: #ffffff;
|
$white: #ffffff;
|
||||||
$highlight-color: $yellow;
|
$highlight-color: $yellow;
|
||||||
$theme-color: $dark-blue;
|
$theme-color: $purple;
|
||||||
$theme-font: 'Assistant', 'Helvetica', sans-serif;
|
$theme-font: 'Poppins', 'Helvetica', sans-serif;
|
||||||
$drop-shadow-color: #BDBEBF;
|
$drop-shadow-color: #BDBEBF;
|
||||||
|
|
||||||
/* Dark mode colors */
|
/* Dark mode colors */
|
||||||
@ -14,4 +19,4 @@ $dark-background-color: #0F1116;
|
|||||||
$light-background-color: #ffffff;
|
$light-background-color: #ffffff;
|
||||||
$light-text-color: #ffffff;
|
$light-text-color: #ffffff;
|
||||||
$dark-text-color: #0F1116;
|
$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 React from "react"
|
||||||
import PropTypes from "prop-types"
|
import PropTypes from "prop-types"
|
||||||
|
import { Transition } from 'react-transition-group';
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
@ -12,6 +13,10 @@ import * as matrix from "matrix-js-sdk";
|
|||||||
import {uuid} from "uuidv4"
|
import {uuid} from "uuidv4"
|
||||||
|
|
||||||
import Message from "./message";
|
import Message from "./message";
|
||||||
|
import Dock from "./dock";
|
||||||
|
import Header from "./header";
|
||||||
|
|
||||||
|
import './styles.scss';
|
||||||
|
|
||||||
|
|
||||||
const DEFAULT_MATRIX_SERVER = "https://matrix.rhok.space"
|
const DEFAULT_MATRIX_SERVER = "https://matrix.rhok.space"
|
||||||
@ -28,30 +33,59 @@ const DEFAULT_THEME = {
|
|||||||
placement: "right"
|
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 {
|
class ChatBox extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
const client = matrix.createClient(this.props.matrixServerUrl)
|
const client = matrix.createClient(this.props.matrixServerUrl)
|
||||||
this.state = {
|
this.state = initialState
|
||||||
client: null,
|
|
||||||
ready: false,
|
|
||||||
accessToken: null,
|
|
||||||
userId: null,
|
|
||||||
messages: [],
|
|
||||||
inputValue: "",
|
|
||||||
errors: [],
|
|
||||||
roomId: null,
|
|
||||||
typingStatus: null,
|
|
||||||
}
|
|
||||||
this.chatboxInput = React.createRef();
|
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 = () => {
|
leaveRoom = () => {
|
||||||
if (this.state.roomId) {
|
if (this.state.roomId) {
|
||||||
this.state.client.leave(this.state.roomId).then(data => {
|
return this.state.client.leave(this.state.roomId)
|
||||||
console.log("Left room", data)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,13 +237,13 @@ class ChatBox extends React.Component {
|
|||||||
|
|
||||||
|
|
||||||
handleEscape = (e) => {
|
handleEscape = (e) => {
|
||||||
if (e.keyCode === 27 && this.props.opened) {
|
if (e.keyCode === 27 && this.state.opened) {
|
||||||
this.props.handleToggleOpen()
|
this.handleToggleOpen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (prevState.client !== this.state.client) {
|
if (this.state.client && prevState.client !== this.state.client) {
|
||||||
this.createRoom()
|
this.createRoom()
|
||||||
|
|
||||||
this.state.client.once('sync', (state, prevState, res) => {
|
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) => {
|
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") {
|
if (event.getType() === "m.room.encryption") {
|
||||||
const msgList = [...this.state.messages]
|
const msgList = [...this.state.messages]
|
||||||
const encryptionMsg = {
|
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) {
|
if (!prevState.ready && this.state.ready) {
|
||||||
this.chatboxInput.current.focus()
|
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()
|
this.initializeClient()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -303,58 +318,55 @@ class ChatBox extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { ready, messages, inputValue, userId, typingStatus } = this.state;
|
const { ready, messages, inputValue, userId, roomId, typingStatus, opened, showDock } = this.state;
|
||||||
const { opened, handleToggleOpen, privacyStatement, termsUrl } = this.props;
|
|
||||||
const inputLabel = 'Send a message...'
|
const inputLabel = 'Send a message...'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="ocrcc-chatbox" aria-haspopup="dialog" aria-label="Open support chat">
|
<div className="docked-widget" role="complementary">
|
||||||
<div className="widget-header">
|
<Transition in={opened} timeout={250} onExited={this.handleWidgetExit}>
|
||||||
<div className="widget-header-title">
|
{(status) => (
|
||||||
Support Chat
|
<div className={`widget widget-${status}`} aria-hidden={!opened}>
|
||||||
</div>
|
<div id="ocrcc-chatbox" aria-haspopup="dialog">
|
||||||
<button
|
<Header handleToggleOpen={this.handleToggleOpen} opened={opened} handleExitChat={this.handleExitChat} />
|
||||||
type="button"
|
|
||||||
className={`widget-header-icon`}
|
|
||||||
onClick={handleToggleOpen}
|
|
||||||
onKeyPress={handleToggleOpen}
|
|
||||||
>
|
|
||||||
<span className={`arrow ${opened ? "opened" : "closed"}`}>⌃</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="message-window">
|
<div className="message-window">
|
||||||
<div className="messages">
|
<div className="messages">
|
||||||
{
|
{
|
||||||
ready ?
|
ready ?
|
||||||
messages.map((message, index) => {
|
messages.map((message, index) => {
|
||||||
return(
|
return(
|
||||||
<Message key={message.id} message={message} userId={userId} botId={BOT_USERNAME} />
|
<Message key={message.id} message={message} userId={userId} botId={BOT_USERNAME} />
|
||||||
)
|
)
|
||||||
}) :
|
}) :
|
||||||
<div className="loader">loading...</div>
|
<div className="loader">loading...</div>
|
||||||
}
|
}
|
||||||
{ typingStatus &&
|
{ typingStatus &&
|
||||||
<div className="notices">
|
<div className="notices">
|
||||||
<div role="status">{typingStatus}</div>
|
<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>
|
)}
|
||||||
</div>
|
</Transition>
|
||||||
<div className="input-window">
|
{showDock && !roomId && <Dock handleToggleOpen={this.handleToggleOpen} />}
|
||||||
<form onSubmit={this.handleSubmit}>
|
{showDock && roomId && <Header handleToggleOpen={this.handleToggleOpen} opened={opened} handleExitChat={this.handleExitChat} />}
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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 "variables";
|
||||||
@import "loader";
|
@import "loader";
|
||||||
@import "widget";
|
|
||||||
@import "chat";
|
@import "chat";
|
||||||
@import "dark_mode";
|
@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() {
|
export default function bookmarklet() {
|
||||||
if (window.EmbeddableWidget) {
|
if (window.EmbeddableChatbox) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.EmbeddableWidget = EmbeddableWidget;
|
window.EmbeddableChatbox = EmbeddableChatbox;
|
||||||
|
|
||||||
EmbeddableWidget.mount({
|
EmbeddableChatbox.mount({
|
||||||
termsUrl: 'https://tosdr.org/',
|
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.',
|
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',
|
matrixServerUrl: 'https://matrix.rhok.space',
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import Widget from '../components/widget';
|
import Chatbox from '../components/chatbox';
|
||||||
import '../../vendor/cleanslate.css';
|
import '../../vendor/cleanslate.css';
|
||||||
|
|
||||||
export default class EmbeddableWidget {
|
export default class EmbeddableChatbox {
|
||||||
static el;
|
static el;
|
||||||
|
|
||||||
static mount({ parentElement = null, ...props } = {}) {
|
static mount({ parentElement = null, ...props } = {}) {
|
||||||
const component = <Widget {...props} />; // eslint-disable-line
|
const component = <Chatbox {...props} />; // eslint-disable-line
|
||||||
|
|
||||||
function doRender() {
|
function doRender() {
|
||||||
if (EmbeddableWidget.el) {
|
if (EmbeddableChatbox.el) {
|
||||||
throw new Error('EmbeddableWidget is already mounted, unmount first');
|
throw new Error('EmbeddableChatbox is already mounted, unmount first');
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
@ -26,7 +26,7 @@ export default class EmbeddableWidget {
|
|||||||
component,
|
component,
|
||||||
el,
|
el,
|
||||||
);
|
);
|
||||||
EmbeddableWidget.el = el;
|
EmbeddableChatbox.el = el;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === 'complete') {
|
if (document.readyState === 'complete') {
|
||||||
@ -39,11 +39,11 @@ export default class EmbeddableWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static unmount() {
|
static unmount() {
|
||||||
if (!EmbeddableWidget.el) {
|
if (!EmbeddableChatbox.el) {
|
||||||
throw new Error('EmbeddableWidget is not mounted, mount first');
|
throw new Error('EmbeddableChatbox is not mounted, mount first');
|
||||||
}
|
}
|
||||||
ReactDOM.unmountComponentAtNode(EmbeddableWidget.el);
|
ReactDOM.unmountComponentAtNode(EmbeddableChatbox.el);
|
||||||
EmbeddableWidget.el.parentNode.removeChild(EmbeddableWidget.el);
|
EmbeddableChatbox.el.parentNode.removeChild(EmbeddableChatbox.el);
|
||||||
EmbeddableWidget.el = null;
|
EmbeddableChatbox.el = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -77,12 +77,12 @@ const defaultConfig = {
|
|||||||
|
|
||||||
module.exports = [{
|
module.exports = [{
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
entry: './src/outputs/embeddable-widget.js',
|
entry: './src/outputs/embeddable-chatbox.js',
|
||||||
output: {
|
output: {
|
||||||
path: distDir,
|
path: distDir,
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
filename: 'widget.js',
|
filename: 'chatbox.js',
|
||||||
library: 'EmbeddableWidget',
|
library: 'EmbeddableChatbox',
|
||||||
libraryExport: 'default',
|
libraryExport: 'default',
|
||||||
libraryTarget: 'window',
|
libraryTarget: 'window',
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user