diff --git a/__mocks__/matrix-js-sdk.js b/__mocks__/matrix-js-sdk.js
new file mode 100644
index 0000000..d6bd58a
--- /dev/null
+++ b/__mocks__/matrix-js-sdk.js
@@ -0,0 +1,11 @@
+export const mockCreateClient = jest.fn();
+export const mockStartClient = jest.fn();
+
+const mockMatrix = jest.fn().mockImplementation(() => {
+ return {
+ createClient: mockCreateClient,
+ startClient: mockStartClient
+ };
+});
+
+export default mockMatrix;
\ No newline at end of file
diff --git a/package.json b/package.json
index 64b6627..fdd314a 100644
--- a/package.json
+++ b/package.json
@@ -88,9 +88,10 @@
"copy-webpack-plugin": "5.1.1",
"css-loader": "3.4.1",
"cssimportant-loader": "0.4.0",
- "enzyme": "3.11.0",
- "enzyme-adapter-react-16": "1.15.2",
+ "enzyme": "^3.11.0",
+ "enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "3.4.3",
+ "enzyme-wait": "^1.0.9",
"eslint": "6.8.0",
"eslint-config-airbnb": "18.0.1",
"eslint-import-resolver-webpack": "0.12.0",
@@ -120,6 +121,7 @@
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
+ "react-test-renderer": "^16.13.0",
"react-transition-group": "^4.0.0",
"uuidv4": "^6.0.2"
}
diff --git a/public/index.html b/public/index.html
index 8784007..bbebf00 100644
--- a/public/index.html
+++ b/public/index.html
@@ -18,9 +18,9 @@
roomName: 'Support Chat',
termsUrl: 'https://tosdr.org/',
introMessage: 'This chat application does not collect any of your personal data or any data from your use of this service.',
- agreementMessage: '👉 Do you want to continue? Type yes or no.',
+ agreementMessage: 'Do you want to continue?',
confirmationMessage: 'Waiting for a facilitator to join the chat...',
- exitMessage: 'The chat was not started.',
+ exitMessage: 'The chat is closed. You may close this window.',
chatUnavailableMessage: 'The chat service is not available right now. Please try again later.',
anonymousDisplayName: 'Anonymous',
}
diff --git a/src/components/__snapshots__/widget.test.js.snap b/src/components/__snapshots__/widget.test.js.snap
deleted file mode 100644
index 4041c21..0000000
--- a/src/components/__snapshots__/widget.test.js.snap
+++ /dev/null
@@ -1,197 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[` open/close 1`] = `
-
-
-
-
-
-
- Header
-
-
-
-
- Body
-
-
- Footer
-
-
-
-
-
-
-`;
-
-exports[` open/close 2`] = `
-
-
-
-
-
-
- Header
-
-
-
-
- Body
-
-
- Footer
-
-
-
-
-
-`;
-
-exports[` open/close 3`] = `
-
-
-
-
-
-
- Header
-
-
-
-
- Body
-
-
- Footer
-
-
-
-
-
-
-`;
diff --git a/src/components/_chat.scss b/src/components/_chat.scss
index a28153b..fe30bdc 100644
--- a/src/components/_chat.scss
+++ b/src/components/_chat.scss
@@ -270,6 +270,42 @@
width: fit-content;
}
+ .buttons {
+ display: flex;
+ align-items: center;
+ font-size: 1rem;
+
+ button {
+ background-color: transparent;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.9rem;
+ color: inherit;
+ font-weight: bold;
+ font-family: $theme-font;
+ cursor: pointer;
+ display: flex;
+ flex: 0 1 auto;
+ border: 1px solid $theme-color;
+ transition: all 0.2s ease-in-out;
+ border-radius: 10px;
+ margin-left: 4px;
+
+ &:hover {
+ border: 1px solid $dark-color;
+ box-shadow: inset 0px 0px 0px 1px $dark-color;
+ }
+
+ &:focus {
+ outline: none;
+ color: $white;
+ border: 1px solid $dark-color;
+ box-shadow: inset 0px 0px 0px 1px $dark-color;
+ background-color: $theme-highlight-color;
+ }
+ }
+ }
+
+
&.from-bot {
color: $gray-color;
font-size: 0.9rem;
diff --git a/src/components/chatbox.jsx b/src/components/chatbox.jsx
index 528482c..c01ac8d 100644
--- a/src/components/chatbox.jsx
+++ b/src/components/chatbox.jsx
@@ -24,11 +24,14 @@ const ENCRYPTION_NOTICE = "Messages in this chat are secured with end-to-end enc
const UNENCRYPTION_NOTICE = "End-to-end message encryption is not available on this browser."
const RESTARTING_UNENCRYPTED_CHAT_MESSAGE = "Restarting chat without encryption."
+const DEFAULT_MATRIX_SERVER = "https://matrix.rhok.space/"
+const DEFAULT_BOT_USERNAME = "@help-bot:rhok.space"
+const DEFAULT_TERMS_URL = "https://tosdr.org/"
const DEFAULT_ROOM_NAME = "Support Chat"
const DEFAULT_INTRO_MESSAGE = "This chat application does not collect any of your personal data or any data from your use of this service."
-const DEFAULT_AGREEMENT_MESSAGE = "👉 Do you want to continue? Type yes or no."
+const DEFAULT_AGREEMENT_MESSAGE = "Do you want to continue?"
const DEFAULT_CONFIRMATION_MESSAGE = "Waiting for a facilitator to join the chat..."
-const DEFAULT_EXIT_MESSAGE = "The chat was not started."
+const DEFAULT_EXIT_MESSAGE = "The chat is closed. You may close this window."
const DEFAULT_ANONYMOUS_DISPLAY_NAME="Anonymous"
const DEFAULT_CHAT_UNAVAILABLE_MESSAGE = "The chat service is not available right now. Please try again later."
@@ -36,7 +39,6 @@ const DEFAULT_CHAT_UNAVAILABLE_MESSAGE = "The chat service is not available righ
class ChatBox extends React.Component {
constructor(props) {
super(props)
- const client = matrix.createClient(this.props.matrixServerUrl)
this.initialState = {
opened: false,
showDock: true,
@@ -46,38 +48,17 @@ class ChatBox extends React.Component {
userId: null,
password: null,
localStorage: null,
- messages: [
- {
- id: 'intro-msg-id',
- type: 'm.room.message',
- sender: this.props.botUsername,
- content: { body: this.props.introMessage },
- },
- {
- id: 'terms-msg-id',
- type: 'm.room.message',
- sender: this.props.botUsername,
- content: {
- body: `Please read the full terms and conditions at ${this.props.termsUrl}.`,
- formatted_body: `Please read the full terms and conditions.`
- }
- },
- {
- id: 'agreement-msg-id',
- type: 'm.room.message',
- sender: this.props.botUsername,
- content: { body: this.props.agreementMessage }, },
- ],
+ messages: [],
inputValue: "",
errors: [],
roomId: null,
typingStatus: null,
awaitingAgreement: true,
- awaitingFacilitator: false,
}
this.state = this.initialState
this.chatboxInput = React.createRef();
this.messageWindow = React.createRef();
+ this.termsUrl = React.createRef();
}
handleToggleOpen = () => {
@@ -100,7 +81,11 @@ class ChatBox extends React.Component {
}
handleWidgetEnter = () => {
- this.chatboxInput.current.focus()
+ if (this.state.awaitingAgreement) {
+ this.termsUrl.current.focus()
+ } else {
+ this.chatboxInput.current.focus()
+ }
}
handleExitChat = () => {
@@ -442,24 +427,20 @@ class ChatBox extends React.Component {
this.setState({ inputValue: e.currentTarget.value })
}
+ handleAcceptTerms = () => {
+ this.setState({ awaitingAgreement: false })
+ this.initializeChat()
+ }
+
+ handleRejectTerms = () => {
+ this.exitChat()
+ this.displayBotMessage({ body: this.props.exitMessage })
+ }
+
handleSubmit = e => {
e.preventDefault()
if (!Boolean(this.state.inputValue)) return null;
- if (this.state.awaitingAgreement && !this.state.client) {
- if (this.state.inputValue.toLowerCase() === 'yes') {
- this.displayFakeMessage({ body: this.state.inputValue }, 'from-me')
- this.setState({ inputValue: "" })
-
- return this.initializeChat()
-
- } else {
- this.displayFakeMessage({ body: this.state.inputValue }, 'from-me')
- this.displayBotMessage({ body: this.props.exitMessage })
- return this.setState({ inputValue: "" })
- }
- }
-
if (this.state.client && this.state.roomId) {
return this.sendMessage()
}
@@ -480,6 +461,26 @@ class ChatBox extends React.Component {
+
+
{ this.props.introMessage }
+
+
+
+
Please read the full terms and conditions. By using this chat, you agree to these terms.
+
+
+
+
{ this.props.agreementMessage }
+
+
+
+
+ {`👉`}
+
+
+
+
+
{
messages.map((message, index) => {
return(
@@ -537,6 +538,9 @@ ChatBox.propTypes = {
}
ChatBox.defaultProps = {
+ matrixServerUrl: DEFAULT_MATRIX_SERVER,
+ botUsername: DEFAULT_BOT_USERNAME,
+ termsUrl: DEFAULT_TERMS_URL,
roomName: DEFAULT_ROOM_NAME,
introMessage: DEFAULT_INTRO_MESSAGE,
agreementMessage: DEFAULT_AGREEMENT_MESSAGE,
diff --git a/src/components/chatbox.test.js b/src/components/chatbox.test.js
new file mode 100644
index 0000000..a84a36e
--- /dev/null
+++ b/src/components/chatbox.test.js
@@ -0,0 +1,167 @@
+import React from 'react';
+import Chatbox from './chatbox';
+import mockMatrix, { mockCreateClient } from "matrix-js-sdk";
+import { mount, shallow } from 'enzyme';
+import { createWaitForElement } from 'enzyme-wait';
+import { config } from 'react-transition-group';
+
+config.disabled = true
+
+const testConfig = {
+ matrixServerUrl: 'https://matrix.rhok.space',
+ botUsername: '@help-bot:rhok.space',
+ roomName: 'Support Chat',
+ termsUrl: 'https://tosdr.org/',
+ introMessage: 'This chat application does not collect any of your personal data or any data from your use of this service.',
+ agreementMessage: '👉 Do you want to continue? Type yes or no.',
+ confirmationMessage: 'Waiting for a facilitator to join the chat...',
+ exitMessage: 'The chat was not started.',
+ chatUnavailableMessage: 'The chat service is not available right now. Please try again later.',
+ anonymousDisplayName: 'Anonymous',
+}
+
+
+describe('Chatbox', () => {
+
+ test('chat window should open and close', async () => {
+ const chatbox = mount()
+
+ let dock = chatbox.find('button.dock')
+ let chatWindow = chatbox.find('.widget')
+
+ expect(dock.length).toEqual(1)
+ expect(chatWindow.hasClass('widget-exited')).toEqual(true)
+
+ // open chat window
+ dock.simulate('click')
+
+ const openChatWindow = await createWaitForElement('.widget-entered')(chatbox)
+ dock = chatbox.find('button.dock')
+ expect(openChatWindow.length).toEqual(1)
+ expect(dock.length).toEqual(0)
+
+ // close chat window
+ const closeButton = chatbox.find('button.widget-header-close')
+ closeButton.simulate('click')
+
+ chatWindow = chatbox.find('.widget')
+ dock = chatbox.find('button.dock')
+
+ expect(dock.length).toEqual(1)
+ expect(chatWindow.hasClass('widget-exited')).toEqual(true)
+ })
+
+ test('chat window should contain the right messages', () => {
+ const chatbox = mount()
+ const props = chatbox.props()
+ const messages = chatbox.find('.messages')
+
+ expect(messages.text()).toContain(props.introMessage)
+ expect(messages.html()).toContain(props.termsUrl)
+ expect(messages.text()).toContain(props.agreementMessage)
+ });
+
+ test('#handleExitChat should call exitChat if the client has been initialized', () => {
+
+ })
+
+ test('#exitChat should leave the room and destroy client', () => {
+ // leave room
+ // deactivate account
+ // stop client
+ // clear stores
+ // reset initial state
+ })
+
+ test('agreeing to terms should start encrypted chat', async () => {
+ const chatbox = mount()
+ const dock = chatbox.find('button.dock')
+
+ dock.simulate('click')
+
+ const yesButton = chatbox.find('#accept')
+ yesButton.simulate('click')
+
+ expect(mockCreateClient).toHaveBeenCalled()
+ })
+
+ // test('rejecting terms should not start chat', async () => {
+ // const chatbox = mount()
+ // const dock = chatbox.find('button.dock')
+
+ // dock.simulate('click')
+
+ // const noButton = chatbox.find('#reject')
+ // noButton.simulate('click')
+
+ // expect(mockMatrix.mockCreateClient.mock.calls.length).toEqual(0)
+ // })
+
+ test('#initializeChat should notify user if client fails to initialize', () => {
+ // handleInitError
+ })
+
+ test('#initializeChat should create unencypted chat if initCrypto fails', () => {
+ // initializeUnencryptedChat
+ })
+
+ test('#initializeUnencryptedChat should initialize an unencrypted client', () => {
+ // initializeUnencryptedChat
+ })
+
+ test('#handleDecryptionError should restart client without encryption and notify user', () => {
+ // initializeUnencryptedChat
+ })
+
+ test('#verifyAllRoomDevices should mark all devices in the room as verified devices', () => {
+
+ })
+
+ test('#createRoom should create a new encrypted room with bot as admin', () => {
+
+ })
+
+ test('#createRoom should create a new unencrypted room if encryption is not enabled', () => {
+
+ })
+
+ test('#sendMessage should send text message with input value', () => {
+
+ })
+
+ test('#sendMessage should mark devices as known and retry sending on UnknownDeviceError', () => {
+
+ })
+
+ test('#sendMessage should mark devices as known and retry sending on UnknownDeviceError', () => {
+
+ })
+
+ test('#displayFakeMessage should add a message object to message list', () => {
+
+ })
+
+ test('#displayBotMessage should add a message object with bot as sender to message list', () => {
+
+ })
+
+ test('#handleMessageEvent should add received message to message list', () => {
+
+ })
+
+ test('#componentDidUpdate should set state listeners', () => {
+
+ })
+
+ test('#handleSubmit should listen for yes if awaiting agreement and initialize client', () => {
+
+ })
+
+ test('#handleSubmit should listen for no if awaiting agreement and do nothing', () => {
+
+ })
+
+ test('#handleSubmit should send message if awaitingAgreement is false', () => {
+
+ })
+});
diff --git a/src/outputs/bookmarklet.js b/src/outputs/bookmarklet.js
index 9698bb0..de4df6a 100644
--- a/src/outputs/bookmarklet.js
+++ b/src/outputs/bookmarklet.js
@@ -6,12 +6,20 @@ export default function bookmarklet() {
}
window.EmbeddableChatbox = EmbeddableChatbox;
- 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.',
+ var config = {
matrixServerUrl: 'https://matrix.rhok.space',
+ botUsername: '@help-bot:rhok.space',
roomName: 'Support Chat',
- });
+ termsUrl: 'https://tosdr.org/',
+ introMessage: 'This chat application does not collect any of your personal data or any data from your use of this service.',
+ agreementMessage: '👉 Do you want to continue? Type yes or no.',
+ confirmationMessage: 'Waiting for a facilitator to join the chat...',
+ exitMessage: 'The chat was not started.',
+ chatUnavailableMessage: 'The chat service is not available right now. Please try again later.',
+ anonymousDisplayName: 'Anonymous',
+ }
+
+ EmbeddableChatbox.mount(config);
}
bookmarklet();
diff --git a/src/outputs/bookmarklet.test.js b/src/outputs/bookmarklet.test.js
index 89c8d9b..3f7f3e3 100644
--- a/src/outputs/bookmarklet.test.js
+++ b/src/outputs/bookmarklet.test.js
@@ -6,11 +6,11 @@ describe('bookmarklet', () => {
const el = document.querySelectorAll('body > div');
ReactDOM.unmountComponentAtNode(el[0]);
el[0].parentNode.removeChild(el[0]);
- window.EmbeddableWidget = null;
+ window.EmbeddableChatbox = null;
});
test('#mount document becomes ready', async () => {
- expect(window.EmbeddableWidget).not.toBeNull();
+ expect(window.EmbeddableChatbox).not.toBeNull();
bookmarklet();
const el = document.querySelectorAll('body > div');
expect(el).toHaveLength(1);
diff --git a/src/outputs/embeddable-widget.test.js b/src/outputs/embeddable-chatbox.test.js
similarity index 70%
rename from src/outputs/embeddable-widget.test.js
rename to src/outputs/embeddable-chatbox.test.js
index d28f958..3578a2d 100644
--- a/src/outputs/embeddable-widget.test.js
+++ b/src/outputs/embeddable-chatbox.test.js
@@ -1,23 +1,24 @@
-import EmbeddableWidget from './embeddable-widget';
+import EmbeddableChatbox from './embeddable-chatbox';
import { waitForSelection } from '../test-helpers';
-describe('EmbeddableWidget', () => {
+
+describe('EmbeddableChatbox', () => {
afterEach(() => {
document.readyState = 'complete';
- if (EmbeddableWidget.el) {
- EmbeddableWidget.unmount();
+ if (EmbeddableChatbox.el) {
+ EmbeddableChatbox.unmount();
}
});
test('#mount document becomes ready', async () => {
document.readyState = 'loading';
- EmbeddableWidget.mount();
+ EmbeddableChatbox.mount();
window.dispatchEvent(new Event('load', {}));
await waitForSelection(document, 'div');
});
test('#mount document complete', async () => {
- EmbeddableWidget.mount();
+ EmbeddableChatbox.mount();
await waitForSelection(document, 'div');
});
@@ -26,7 +27,7 @@ describe('EmbeddableWidget', () => {
newElement.setAttribute('id', 'widget-mount');
document.body.appendChild(newElement);
- EmbeddableWidget.mount({
+ EmbeddableChatbox.mount({
parentElement: '#widget-mount',
});
@@ -36,8 +37,8 @@ describe('EmbeddableWidget', () => {
});
test('#mount twice', async () => {
- EmbeddableWidget.mount();
- expect(() => EmbeddableWidget.mount()).toThrow('already mounted');
+ EmbeddableChatbox.mount();
+ expect(() => EmbeddableChatbox.mount()).toThrow('already mounted');
});
test('#unmount', async () => {
@@ -45,13 +46,13 @@ describe('EmbeddableWidget', () => {
document.body.appendChild(el);
expect(document.querySelectorAll('div')).toHaveLength(1);
- EmbeddableWidget.el = el;
- EmbeddableWidget.unmount();
+ EmbeddableChatbox.el = el;
+ EmbeddableChatbox.unmount();
expect(document.querySelectorAll('div')).toHaveLength(0);
});
test('#unmount without mounting', async () => {
- expect(() => EmbeddableWidget.unmount()).toThrow('not mounted');
+ expect(() => EmbeddableChatbox.unmount()).toThrow('not mounted');
});
});
diff --git a/yarn.lock b/yarn.lock
index e5bd495..0fcf7e4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2598,6 +2598,11 @@ assert@^1.1.1:
object-assign "^4.1.1"
util "0.10.3"
+assertion-error@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
+ integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
+
assign-symbols@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
@@ -4828,7 +4833,7 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
-enzyme-adapter-react-16@1.15.2:
+enzyme-adapter-react-16@^1.15.2:
version "1.15.2"
resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.2.tgz#b16db2f0ea424d58a808f9df86ab6212895a4501"
integrity sha512-SkvDrb8xU3lSxID8Qic9rB8pvevDbLybxPK6D/vW7PrT0s2Cl/zJYuXvsd1EBTz0q4o3iqG3FJhpYz3nUNpM2Q==
@@ -4870,7 +4875,14 @@ enzyme-to-json@3.4.3:
dependencies:
lodash "^4.17.15"
-enzyme@3.11.0:
+enzyme-wait@^1.0.9:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/enzyme-wait/-/enzyme-wait-1.0.9.tgz#f7a08bf49c7047358fa03e1f411a565c3a15101a"
+ integrity sha1-96CL9JxwRzWPoD4fQRpWXDoVEBo=
+ dependencies:
+ assertion-error "^1.0.2"
+
+enzyme@^3.11.0:
version "3.11.0"
resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28"
integrity sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==
@@ -10089,6 +10101,16 @@ react-test-renderer@^16.0.0-0:
react-is "^16.8.6"
scheduler "^0.18.0"
+react-test-renderer@^16.13.0:
+ version "16.13.0"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.0.tgz#39ba3bf72cedc8210c3f81983f0bb061b14a3014"
+ integrity sha512-NQ2S9gdMUa7rgPGpKGyMcwl1d6D9MCF0lftdI3kts6kkiX+qvpC955jNjAZXlIDTjnN9jwFI8A8XhRh/9v0spA==
+ dependencies:
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ react-is "^16.8.6"
+ scheduler "^0.19.0"
+
react-textarea-autosize@^7.1.0:
version "7.1.2"
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-7.1.2.tgz#70fdb333ef86bcca72717e25e623e90c336e2cda"
@@ -10706,6 +10728,14 @@ scheduler@^0.18.0:
loose-envify "^1.1.0"
object-assign "^4.1.1"
+scheduler@^0.19.0:
+ version "0.19.0"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.0.tgz#a715d56302de403df742f4a9be11975b32f5698d"
+ integrity sha512-xowbVaTPe9r7y7RUejcK73/j8tt2jfiyTednOvHbA8JoClvMYCp+r8QegLwK/n8zWQAtZb1fFnER4XLBZXrCxA==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
schema-utils@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"