Compare commits

...

4 Commits

Author SHA1 Message Date
Mejans
047c454988
Update Occitan locale (#241)
To translate the commit "username and settings are now persistant "
2024-01-14 15:29:44 +01:00
Jérémie Pardou
eede08a840
Fix IOS incompatibility (#239) 2023-12-31 11:49:58 +01:00
Jérémie Pardou
d67bc945ce
Fix Ci (#238) 2023-12-30 18:00:23 +01:00
Jérémie Pardou
d875eefed0
Migrate to functionnal components (#232)
* Refactor Home component

* Refactor Welcome

* Refactor small components

* Refactor Nav component

* refactor Settings

* Refactor Chat

* Refactor New message notifications

* Fix tests

* Remove tooltip

* Remove react-simple-dropdown

* Change to last redux

* Switch to redux hooks

* Add github action
2023-12-30 17:53:50 +01:00
47 changed files with 2342 additions and 1862 deletions

View File

@ -1,49 +0,0 @@
# Javascript Node CircleCI 2.0 configuration file
#
jobs:
test-job:
docker:
- image: "cimg/node:lts"
working_directory: ~/repo
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- dependencies-{{ checksum "yarn.lock" }}
# fallback to using the latest cache if no exact match is found
- dependencies-
- run: yarn setup
- save_cache:
paths:
- node_modules
- client/node_modules
- server/node_modules
key: dependencies-{{ checksum "yarn.lock" }}
- run:
command: yarn lint
- run:
command: yarn test
environment:
TZ: UTC
VITE_COMMIT_SHA: some_sha
- store_artifacts: # For coverage report
path: client/coverage
orbs: # declare what orbs we are going to use
node: circleci/node@2.0.2 # the node orb provides common node-related configuration
version: 2.1
workflows:
tests:
jobs:
- test-job

35
.github/workflows/lint-test.yml vendored Normal file
View File

@ -0,0 +1,35 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
push:
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm i yarn -g
- run: yarn setup
env:
TZ: UTC
VITE_COMMIT_SHA: some_sha
- run: yarn lint
- run: yarn test
env:
TZ: UTC
VITE_COMMIT_SHA: some_sha

View File

@ -1,5 +1,11 @@
module.exports = { module.exports = {
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended'
],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'], plugins: ['@typescript-eslint'],
root: true, root: true,
@ -7,4 +13,14 @@ module.exports = {
browser: true, browser: true,
node: true, node: true,
}, },
settings: {
react: {
pragma: 'React', // Pragma to use, default to "React"
version: 'detect', // React version. "detect" automatically picks the version you have installed.
},
},
rules: {
"react/prop-types": "off",
"@typescript-eslint/no-empty-function": "off"
}
}; };

View File

@ -8,7 +8,8 @@
<meta name="robots" content="index,nofollow" /> <meta name="robots" content="index,nofollow" />
<meta name="googlebot" content="index,nofollow" /> <meta name="googlebot" content="index,nofollow" />
<meta name="description" content="darkwire.io is the simplest way to chat with encryption online." /> <meta name="description" content="darkwire.io is the simplest way to chat with encryption online." />
<title>Darkwire.io - instant encrypted web chat</title> <title>Darkwire</title>
<link rel="manifest" href="/manifest.json" />
</head> </head>
<body class="h-100"> <body class="h-100">
<div id="root" class="h-100"></div> <div id="root" class="h-100"></div>

View File

@ -1,9 +0,0 @@
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["src/*"]
}
}
}

View File

@ -13,9 +13,10 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@react-hookz/web": "^20.0.2",
"@reduxjs/toolkit": "^1.9.1",
"bootstrap": "^4.6.2", "bootstrap": "^4.6.2",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"clipboard": "^2.0.11",
"jquery": "3", "jquery": "3",
"moment": "^2.29.4", "moment": "^2.29.4",
"nanoid": "^4.0.0", "nanoid": "^4.0.0",
@ -28,7 +29,7 @@
"react-redux": "^8.0.5", "react-redux": "^8.0.5",
"react-router": "^6.4.4", "react-router": "^6.4.4",
"react-router-dom": "^6.4.4", "react-router-dom": "^6.4.4",
"react-simple-dropdown": "^3.2.3", "react-tooltip": "^5.2.0",
"redux": "^4.2.0", "redux": "^4.2.0",
"redux-thunk": "^2.4.2", "redux-thunk": "^2.4.2",
"sanitize-html": "^2.7.3", "sanitize-html": "^2.7.3",
@ -54,6 +55,8 @@
"eslint": "^8.29.0", "eslint": "^8.29.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3", "eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"jest-environment-jsdom-sixteen": "^1.0.3", "jest-environment-jsdom-sixteen": "^1.0.3",
"jest-fetch-mock": "^3.0.3", "jest-fetch-mock": "^3.0.3",
"prettier": "^2.0.5", "prettier": "^2.0.5",

View File

@ -1,15 +1,17 @@
{ {
"short_name": "Darkwire", "short_name": "Darkwire",
"name": "Darkwire.io - encrypted web chat", "name": "Darkwire.io - Encrypted Web Chat",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16", "sizes": "192x192",
"type": "image/x-icon" "type": "image/png"
} }
], ],
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "fullscreen",
"theme_color": "#000000", "theme_color": "#000000",
"background_color": "#ffffff" "background_color": "#ffffff",
"scope": "/",
"description": "Secure and encrypted web chat with Darkwire.io"
} }

View File

@ -31,10 +31,6 @@ export const toggleSocketConnected = payload => async dispatch => {
dispatch({ type: 'TOGGLE_SOCKET_CONNECTED', payload }); dispatch({ type: 'TOGGLE_SOCKET_CONNECTED', payload });
}; };
export const createUser = payload => async dispatch => {
dispatch({ type: 'CREATE_USER', payload });
};
export const clearActivities = () => async dispatch => { export const clearActivities = () => async dispatch => {
dispatch({ type: 'CLEAR_ACTIVITIES' }); dispatch({ type: 'CLEAR_ACTIVITIES' });
}; };

View File

@ -40,7 +40,6 @@ describe('App actions', () => {
[actions.showNotice('test'), 'SHOW_NOTICE'], [actions.showNotice('test'), 'SHOW_NOTICE'],
[actions.toggleSoundEnabled('test'), 'TOGGLE_SOUND_ENABLED'], [actions.toggleSoundEnabled('test'), 'TOGGLE_SOUND_ENABLED'],
[actions.toggleSocketConnected('test'), 'TOGGLE_SOCKET_CONNECTED'], [actions.toggleSocketConnected('test'), 'TOGGLE_SOCKET_CONNECTED'],
[actions.createUser('test'), 'CREATE_USER'],
[actions.setLanguage('test'), 'CHANGE_LANGUAGE'], [actions.setLanguage('test'), 'CHANGE_LANGUAGE'],
]; ];

View File

@ -1,10 +1,18 @@
import { getSocket } from '@/utils/socket'; import { getSocket } from '@/utils/socket';
import { prepare as prepareMessage, process as processMessage } from '@/utils/message'; import { prepare as prepareMessage, process as processMessage } from '@/utils/message';
import { changeUsername } from '@/reducers/user';
export const sendEncryptedMessage = payload => async (dispatch, getState) => { export const sendEncryptedMessage = payload => async (dispatch, getState) => {
const state = getState(); const state = getState();
const msg = await prepareMessage(payload, state); const msg = await prepareMessage(payload, state);
switch(msg.original.type){
case "CHANGE_USERNAME":
dispatch(changeUsername(msg.original.payload));
dispatch({ type: `SEND_ENCRYPTED_MESSAGE_${msg.original.type}`, payload: msg.original.payload }); dispatch({ type: `SEND_ENCRYPTED_MESSAGE_${msg.original.type}`, payload: msg.original.payload });
break;
default:
dispatch({ type: `SEND_ENCRYPTED_MESSAGE_${msg.original.type}`, payload: msg.original.payload });
}
getSocket().emit('ENCRYPTED_MESSAGE', msg.toSend); getSocket().emit('ENCRYPTED_MESSAGE', msg.toSend);
}; };

View File

@ -1,37 +1,27 @@
/* eslint-disable */ /* eslint-disable */
import React, { Component } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { COMMIT_SHA } from '@/config/env'; import { COMMIT_SHA } from '@/config/env';
import apiUrlGenerator from '@/api/generator'; import apiUrlGenerator from '@/api/generator';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
class About extends Component { const About = ({ roomId: roomIdProp }) => {
constructor(props) { const [roomId, setRoomId] = React.useState(roomIdProp);
super(props); const [abuseReported, setAbuseReported] = React.useState(false);
this.state = {
roomId: props.roomId, const handleUpdateRoomId = evt => {
abuseReported: false, setRoomId(evt.target.value);
}; };
}
handleUpdateRoomId(evt) { const handleReportAbuse = evt => {
this.setState({
roomId: evt.target.value,
});
}
handleReportAbuse(evt) {
evt.preventDefault(); evt.preventDefault();
fetch(`${apiUrlGenerator('abuse')}/${this.state.roomId}`, { fetch(`${apiUrlGenerator('abuse')}/${roomId}`, {
method: 'POST', method: 'POST',
}); });
this.setState({ setAbuseReported(true);
abuseReported: true, };
});
}
render() {
return ( return (
<div className={styles.base}> <div className={styles.base}>
<div className={styles.links}> <div className={styles.links}>
@ -96,8 +86,8 @@ class About extends Component {
<h4>Report Abuse</h4> <h4>Report Abuse</h4>
<p> <p>
We encourage you to report problematic content to us. Please keep in mind that to help ensure the safety, We encourage you to report problematic content to us. Please keep in mind that to help ensure the safety,
confidentiality and security of your messages, we do not have the contents of messages available to us, confidentiality and security of your messages, we do not have the contents of messages available to us, which
which limits our ability to verify the report and take action. limits our ability to verify the report and take action.
</p> </p>
<p> <p>
When needed, you can take a screenshot of the content and share it, along with any available contact info, When needed, you can take a screenshot of the content and share it, along with any available contact info,
@ -106,15 +96,15 @@ class About extends Component {
<p> <p>
To report any content, email us at abuse[at]darkwire.io or submit the room ID below to report anonymously. To report any content, email us at abuse[at]darkwire.io or submit the room ID below to report anonymously.
</p> </p>
<form onSubmit={this.handleReportAbuse.bind(this)}> <form onSubmit={handleReportAbuse}>
{this.state.abuseReported && <div>Thank you!</div>} {abuseReported && <div>Thank you!</div>}
<div> <div>
<div className="input-group"> <div className="input-group">
<input <input
className="form-control" className="form-control"
placeholder="Room ID" placeholder="Room ID"
onChange={this.handleUpdateRoomId.bind(this)} onChange={handleUpdateRoomId}
value={this.state.roomId} value={roomId}
type="text" type="text"
/> />
<div className="input-group-append"> <div className="input-group-append">
@ -142,31 +132,30 @@ class About extends Component {
<h4>Acceptable Use Policy</h4> <h4>Acceptable Use Policy</h4>
<p> <p>
This Acceptable Use Policy (this Policy) describes prohibited uses of the web services offered by Darkwire This Acceptable Use Policy (this Policy) describes prohibited uses of the web services offered by Darkwire
and its affiliates (the Services) and the website located at https://darkwire.io (the Darkwire Site). and its affiliates (the Services) and the website located at https://darkwire.io (the Darkwire Site). The
The examples described in this Policy are not exhaustive. We may modify this Policy at any time by posting a examples described in this Policy are not exhaustive. We may modify this Policy at any time by posting a
revised version on the Darkwire Site. By using the Services or accessing the Darkwire Site, you agree to the revised version on the Darkwire Site. By using the Services or accessing the Darkwire Site, you agree to the
latest version of this Policy. If you violate the Policy or authorize or help others to do so, we may latest version of this Policy. If you violate the Policy or authorize or help others to do so, we may suspend
suspend or terminate your use of the Services. or terminate your use of the Services.
</p> </p>
<strong>No Illegal, Harmful, or Offensive Use or Content</strong> <strong>No Illegal, Harmful, or Offensive Use or Content</strong>
<p> <p>
You may not use, or encourage, promote, facilitate or instruct others to use, the Services or Darkwire Site You may not use, or encourage, promote, facilitate or instruct others to use, the Services or Darkwire Site
for any illegal, harmful, fraudulent, infringing or offensive use, or to transmit, store, display, for any illegal, harmful, fraudulent, infringing or offensive use, or to transmit, store, display, distribute
distribute or otherwise make available content that is illegal, harmful, fraudulent, infringing or or otherwise make available content that is illegal, harmful, fraudulent, infringing or offensive. Prohibited
offensive. Prohibited activities or content include: activities or content include:
</p> </p>
<ul> <ul>
<li> <li>
<strong>Illegal, Harmful or Fraudulent Activities.</strong> Any activities that are illegal, that violate <strong>Illegal, Harmful or Fraudulent Activities.</strong> Any activities that are illegal, that violate
the rights of others, or that may be harmful to others, our operations or reputation, including the rights of others, or that may be harmful to others, our operations or reputation, including
disseminating, promoting or facilitating child pornography, offering or disseminating fraudulent goods, disseminating, promoting or facilitating child pornography, offering or disseminating fraudulent goods,
services, schemes, or promotions, make-money-fast schemes, ponzi and pyramid schemes, phishing, or services, schemes, or promotions, make-money-fast schemes, ponzi and pyramid schemes, phishing, or pharming.
pharming.
</li> </li>
<li> <li>
<strong>Infringing Content.</strong> Content that infringes or misappropriates the intellectual property <strong>Infringing Content.</strong> Content that infringes or misappropriates the intellectual property or
or proprietary rights of others. proprietary rights of others.
</li> </li>
<li> <li>
@ -184,13 +173,12 @@ class About extends Component {
<strong>No Security Violations</strong> <strong>No Security Violations</strong>
<br /> <br />
You may not use the Services to violate the security or integrity of any network, computer or communications You may not use the Services to violate the security or integrity of any network, computer or communications
system, software application, or network or computing device (each, a System). Prohibited activities system, software application, or network or computing device (each, a System). Prohibited activities include:
include:
<ul> <ul>
<li> <li>
<strong>Unauthorized Access.</strong> Accessing or using any System without permission, including <strong>Unauthorized Access.</strong> Accessing or using any System without permission, including attempting
attempting to probe, scan, or test the vulnerability of a System or to breach any security or to probe, scan, or test the vulnerability of a System or to breach any security or authentication measures
authentication measures used by a System. used by a System.
</li> </li>
<li> <li>
@ -205,8 +193,8 @@ class About extends Component {
</ul> </ul>
<strong>No Network Abuse</strong> <strong>No Network Abuse</strong>
<br /> <br />
You may not make network connections to any users, hosts, or networks unless you have permission to You may not make network connections to any users, hosts, or networks unless you have permission to communicate
communicate with them. Prohibited activities include: with them. Prohibited activities include:
<ul> <ul>
<li> <li>
<strong>Monitoring or Crawling.</strong> Monitoring or crawling of a System that impairs or disrupts the <strong>Monitoring or Crawling.</strong> Monitoring or crawling of a System that impairs or disrupts the
@ -219,9 +207,9 @@ class About extends Component {
</li> </li>
<li> <li>
<strong>Intentional Interference.</strong> Interfering with the proper functioning of any System, <strong>Intentional Interference.</strong> Interfering with the proper functioning of any System, including
including any deliberate attempt to overload a system by mail bombing, news bombing, broadcast attacks, or any deliberate attempt to overload a system by mail bombing, news bombing, broadcast attacks, or flooding
flooding techniques. techniques.
</li> </li>
<li> <li>
@ -230,21 +218,21 @@ class About extends Component {
</li> </li>
<li> <li>
<strong>Avoiding System Restrictions.</strong> Using manual or electronic means to avoid any use <strong>Avoiding System Restrictions.</strong> Using manual or electronic means to avoid any use limitations
limitations placed on a System, such as access and storage restrictions. placed on a System, such as access and storage restrictions.
</li> </li>
</ul> </ul>
<strong>No E-Mail or Other Message Abuse</strong> <strong>No E-Mail or Other Message Abuse</strong>
<br /> <br />
You will not distribute, publish, send, or facilitate the sending of unsolicited mass e-mail or other You will not distribute, publish, send, or facilitate the sending of unsolicited mass e-mail or other messages,
messages, promotions, advertising, or solicitations (like spam), including commercial advertising and promotions, advertising, or solicitations (like spam), including commercial advertising and informational
informational announcements. You will not alter or obscure mail headers or assume a senders identity without announcements. You will not alter or obscure mail headers or assume a senders identity without the senders
the senders explicit permission. You will not collect replies to messages sent from another internet service explicit permission. You will not collect replies to messages sent from another internet service provider if
provider if those messages violate this Policy or the acceptable use policy of that provider. those messages violate this Policy or the acceptable use policy of that provider.
<strong>Our Monitoring and Enforcement</strong> <strong>Our Monitoring and Enforcement</strong>
<br /> <br />
We reserve the right, but do not assume the obligation, to investigate any violation of this Policy or misuse We reserve the right, but do not assume the obligation, to investigate any violation of this Policy or misuse of
of the Services or Darkwire Site. We may: the Services or Darkwire Site. We may:
<ul> <ul>
<li>investigate violations of this Policy or misuse of the Services or Darkwire Site; or</li> <li>investigate violations of this Policy or misuse of the Services or Darkwire Site; or</li>
<li> <li>
@ -253,17 +241,17 @@ class About extends Component {
</li> </li>
<li> <li>
We may report any activity that we suspect violates any law or regulation to appropriate law enforcement We may report any activity that we suspect violates any law or regulation to appropriate law enforcement
officials, regulators, or other appropriate third parties. Our reporting may include disclosing officials, regulators, or other appropriate third parties. Our reporting may include disclosing appropriate
appropriate customer information. We also may cooperate with appropriate law enforcement agencies, customer information. We also may cooperate with appropriate law enforcement agencies, regulators, or other
regulators, or other appropriate third parties to help with the investigation and prosecution of illegal appropriate third parties to help with the investigation and prosecution of illegal conduct by providing
conduct by providing network and systems information related to alleged violations of this Policy. network and systems information related to alleged violations of this Policy.
</li> </li>
</ul> </ul>
Reporting of Violations of this Policy Reporting of Violations of this Policy
<br /> <br />
If you become aware of any violation of this Policy, you will immediately notify us and provide us with If you become aware of any violation of this Policy, you will immediately notify us and provide us with
assistance, as requested, to stop or remedy the violation. To report any violation of this Policy, please assistance, as requested, to stop or remedy the violation. To report any violation of this Policy, please follow
follow our abuse reporting process. our abuse reporting process.
</section> </section>
<section id="terms"> <section id="terms">
@ -289,9 +277,9 @@ class About extends Component {
<p> <p>
Darkwire has no control over, and assumes no responsibility for, the content, privacy policies, or practices Darkwire has no control over, and assumes no responsibility for, the content, privacy policies, or practices
of any third party web sites or services. You further acknowledge and agree that Darkwire shall not be of any third party web sites or services. You further acknowledge and agree that Darkwire shall not be
responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in
in connection with use of or reliance on any such content, goods or services available on or through any connection with use of or reliance on any such content, goods or services available on or through any such web
such web sites or services. sites or services.
</p> </p>
<p> <p>
We strongly advise you to read the terms and conditions and privacy policies of any third-party web sites or We strongly advise you to read the terms and conditions and privacy policies of any third-party web sites or
@ -313,11 +301,10 @@ class About extends Component {
regard to its conflict of law provisions. regard to its conflict of law provisions.
</p> </p>
<p> <p>
Our failure to enforce any right or provision of these Terms will not be considered a waiver of those Our failure to enforce any right or provision of these Terms will not be considered a waiver of those rights.
rights. If any provision of these Terms is held to be invalid or unenforceable by a court, the remaining If any provision of these Terms is held to be invalid or unenforceable by a court, the remaining provisions of
provisions of these Terms will remain in effect. These Terms constitute the entire agreement between us these Terms will remain in effect. These Terms constitute the entire agreement between us regarding our
regarding our Service, and supersede and replace any prior agreements we might have between us regarding the Service, and supersede and replace any prior agreements we might have between us regarding the Service.
Service.
</p> </p>
</section> </section>
@ -328,8 +315,8 @@ class About extends Component {
&nbsp;Proceed with caution and always confirm recipients beforre starting a chat session. &nbsp;Proceed with caution and always confirm recipients beforre starting a chat session.
</p> </p>
<p> <p>
Please also note that <strong>ALL CHATROOMS</strong> are public. &nbsp;Anyone can guess your room URL. If Please also note that <strong>ALL CHATROOMS</strong> are public. &nbsp;Anyone can guess your room URL. If you
you need a more-private room, use the lock feature or set the URL manually by entering a room ID after need a more-private room, use the lock feature or set the URL manually by entering a room ID after
&quot;darkwire.io/&quot;. &quot;darkwire.io/&quot;.
</p> </p>
<br /> <br />
@ -340,17 +327,17 @@ class About extends Component {
WARRANTIES OF ANY KIND. TO THE FULLEST EXTENT PERMITTED BY LAW, Darkwire SPECIFICALLY DISCLAIMS ALL WARRANTIES OF ANY KIND. TO THE FULLEST EXTENT PERMITTED BY LAW, Darkwire SPECIFICALLY DISCLAIMS ALL
WARRANTIES AND CONDITIONS OF ANY KIND, INCLUDING ALL IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, WARRANTIES AND CONDITIONS OF ANY KIND, INCLUDING ALL IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NONINFRINGEMENT FOR OUR WEBSITE AND ANY CONTRACTS AND SERVICES FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NONINFRINGEMENT FOR OUR WEBSITE AND ANY CONTRACTS AND SERVICES
YOU PURCHASE THROUGH IT. Darkwire SHALL NOT HAVE ANY LIABILITY OR RESPONSIBILITY FOR ANY ERRORS OR YOU PURCHASE THROUGH IT. Darkwire SHALL NOT HAVE ANY LIABILITY OR RESPONSIBILITY FOR ANY ERRORS OR OMISSIONS
OMISSIONS IN THE CONTENT OF OUR WEBSITE, FOR CONTRACTS OR SERVICES SOLD THROUGH OUR WEBSITE, FOR YOUR IN THE CONTENT OF OUR WEBSITE, FOR CONTRACTS OR SERVICES SOLD THROUGH OUR WEBSITE, FOR YOUR ACTION OR
ACTION OR INACTION IN CONNECTION WITH OUR WEBSITE OR FOR ANY DAMAGE TO YOUR COMPUTER OR DATA OR ANY OTHER INACTION IN CONNECTION WITH OUR WEBSITE OR FOR ANY DAMAGE TO YOUR COMPUTER OR DATA OR ANY OTHER DAMAGE YOU
DAMAGE YOU MAY INCUR IN CONNECTION WITH OUR WEBSITE. YOUR USE OF OUR WEBSITE AND ANY CONTRACTS OR SERVICES MAY INCUR IN CONNECTION WITH OUR WEBSITE. YOUR USE OF OUR WEBSITE AND ANY CONTRACTS OR SERVICES ARE AT YOUR
ARE AT YOUR OWN RISK. IN NO EVENT SHALL EITHER Darkwire OR THEIR AGENTS BE LIABLE FOR ANY DIRECT, OWN RISK. IN NO EVENT SHALL EITHER Darkwire OR THEIR AGENTS BE LIABLE FOR ANY DIRECT, INDIRECT, PUNITIVE,
INDIRECT, PUNITIVE, INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR IN ANY WAY CONNECTED INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR IN ANY WAY CONNECTED WITH THE USE OF OUR
WITH THE USE OF OUR WEBSITE, CONTRACTS AND SERVICES PURCHASED THROUGH OUR WEBSITE, THE DELAY OR INABILITY WEBSITE, CONTRACTS AND SERVICES PURCHASED THROUGH OUR WEBSITE, THE DELAY OR INABILITY TO USE OUR WEBSITE OR
TO USE OUR WEBSITE OR OTHERWISE ARISING IN CONNECTION WITH OUR WEBSITE, CONTRACTS OR RELATED SERVICES, OTHERWISE ARISING IN CONNECTION WITH OUR WEBSITE, CONTRACTS OR RELATED SERVICES, WHETHER BASED ON CONTRACT,
WHETHER BASED ON CONTRACT, TORT, STRICT LIABILITY OR OTHERWISE, EVEN IF ADVISED OF THE POSSIBILITY OF ANY TORT, STRICT LIABILITY OR OTHERWISE, EVEN IF ADVISED OF THE POSSIBILITY OF ANY SUCH DAMAGES. IN NO EVENT
SUCH DAMAGES. IN NO EVENT SHALL Darkwires LIABILITY FOR ANY DAMAGE CLAIM EXCEED THE AMOUNT PAID BY YOU TO SHALL Darkwires LIABILITY FOR ANY DAMAGE CLAIM EXCEED THE AMOUNT PAID BY YOU TO Darkwire FOR THE
Darkwire FOR THE TRANSACTION GIVING RISE TO SUCH DAMAGE CLAIM. TRANSACTION GIVING RISE TO SUCH DAMAGE CLAIM.
</strong> </strong>
</p> </p>
<p> <p>
@ -369,8 +356,8 @@ class About extends Component {
<p> <p>
<strong> <strong>
YOU AGREE THAT ALL TIMES, YOU WILL LOOK TO ATTORNEYS FROM WHOM YOU PURCHASE SERVICES FOR ANY CLAIMS OF ANY YOU AGREE THAT ALL TIMES, YOU WILL LOOK TO ATTORNEYS FROM WHOM YOU PURCHASE SERVICES FOR ANY CLAIMS OF ANY
NATURE, INCLUDING LOSS, DAMAGE, OR WARRANTY. Darkwire AND THEIR RESPECTIVE AFFILIATES MAKE NO NATURE, INCLUDING LOSS, DAMAGE, OR WARRANTY. Darkwire AND THEIR RESPECTIVE AFFILIATES MAKE NO REPRESENTATION
REPRESENTATION OR GUARANTEES ABOUT ANY CONTRACTS AND SERVICES OFFERED THROUGH OUR WEBSITE. OR GUARANTEES ABOUT ANY CONTRACTS AND SERVICES OFFERED THROUGH OUR WEBSITE.
</strong> </strong>
</p> </p>
<p> <p>
@ -381,15 +368,15 @@ class About extends Component {
</p> </p>
<strong>Indemnification</strong> <strong>Indemnification</strong>
<p> <p>
You agree to defend, indemnify and hold Darkwire harmless from and against any and all claims, damages, You agree to defend, indemnify and hold Darkwire harmless from and against any and all claims, damages, costs
costs and expenses, including attorneys' fees, arising from or related to your use of our Website or any and expenses, including attorneys' fees, arising from or related to your use of our Website or any Contracts
Contracts or Services you purchase through it. or Services you purchase through it.
</p> </p>
<strong>Changes</strong> <strong>Changes</strong>
<p> <p>
We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is
material we will try to provide at least 30 days notice prior to any new terms taking effect. What material we will try to provide at least 30 days notice prior to any new terms taking effect. What constitutes
constitutes a material change will be determined at our sole discretion. a material change will be determined at our sole discretion.
</p> </p>
<p> <p>
By continuing to access or use our Service after those revisions become effective, you agree to be bound by By continuing to access or use our Service after those revisions become effective, you agree to be bound by
@ -441,8 +428,7 @@ class About extends Component {
</section> </section>
</div> </div>
); );
} };
}
About.propTypes = { About.propTypes = {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,

View File

@ -1,22 +1,22 @@
import React, { Component } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import { CornerDownRight } from 'react-feather'; import { CornerDownRight } from 'react-feather';
import { getSelectedText, hasTouchSupport } from '@/utils/dom'; import { hasTouchSupport } from '@/utils/dom';
import FileTransfer from '@/components/FileTransfer'; import FileTransfer from '@/components/FileTransfer';
export class Chat extends Component { export const Chat = ({ sendEncryptedMessage, showNotice, userId, username, clearActivities, translations }) => {
constructor(props) { const [message, setMessage] = React.useState('');
super(props); const [shiftKeyDown, setShiftKeyDown] = React.useState(false);
this.state = { const textInputRef = React.useRef();
message: '',
touchSupport: hasTouchSupport,
shiftKeyDown: false,
};
this.commands = [ const touchSupport = hasTouchSupport;
const canSend = message.trim().length;
const commands = [
{ {
command: 'nick', command: 'nick',
description: 'Changes nickname.', description: 'Changes nickname.',
@ -45,18 +45,18 @@ export class Chat extends Component {
} }
if (errors.length) { if (errors.length) {
return this.props.showNotice({ return showNotice({
message: `${errors.join(', ')}`, message: `${errors.join(', ')}`,
level: 'error', level: 'error',
}); });
} }
this.props.sendEncryptedMessage({ sendEncryptedMessage({
type: 'CHANGE_USERNAME', type: 'CHANGE_USERNAME',
payload: { payload: {
id: this.props.userId, id: userId,
newUsername, newUsername,
currentUsername: this.props.username, currentUsername: username,
}, },
}); });
}, },
@ -64,13 +64,12 @@ export class Chat extends Component {
{ {
command: 'help', command: 'help',
description: 'Shows a list of commands.', description: 'Shows a list of commands.',
paramaters: [], parameters: [],
usage: '/help', usage: '/help',
scope: 'local', scope: 'local',
action: params => { action: () => {
// eslint-disable-line const validCommands = commands.map(command => `/${command.command}`);
const validCommands = this.commands.map(command => `/${command.command}`); showNotice({
this.props.showNotice({
message: `Valid commands: ${validCommands.sort().join(', ')}`, message: `Valid commands: ${validCommands.sort().join(', ')}`,
level: 'info', level: 'info',
}); });
@ -79,17 +78,16 @@ export class Chat extends Component {
{ {
command: 'me', command: 'me',
description: 'Invoke virtual action', description: 'Invoke virtual action',
paramaters: ['{action}'], parameters: ['{action}'],
usage: '/me {action}', usage: '/me {action}',
scope: 'global', scope: 'global',
action: params => { action: params => {
// eslint-disable-line
const actionMessage = params.join(' '); const actionMessage = params.join(' ');
if (!actionMessage.trim().length) { if (!actionMessage.trim().length) {
return false; return false;
} }
this.props.sendEncryptedMessage({ sendEncryptedMessage({
type: 'USER_ACTION', type: 'USER_ACTION',
payload: { payload: {
action: actionMessage, action: actionMessage,
@ -100,72 +98,37 @@ export class Chat extends Component {
{ {
command: 'clear', command: 'clear',
description: 'Clears the chat screen', description: 'Clears the chat screen',
paramaters: [], parameters: [],
usage: '/clear', usage: '/clear',
scope: 'local', scope: 'local',
action: (params = null) => { action: () => {
// eslint-disable-line clearActivities();
this.props.clearActivities();
}, },
}, },
]; ];
}
componentDidMount() { const handleKeyUp = e => {
if (!hasTouchSupport) {
// Disable for now due to vary issues:
// Paste not working, shift+enter line breaks
// autosize(this.textInput);
this.textInput.addEventListener('autosize:resized', () => {
this.props.scrollToBottom();
});
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.focusChat) {
if (!getSelectedText()) {
// Don't focus for now, evaluate UX benfits
// this.textInput.focus()
}
}
}
componentDidUpdate(nextProps, nextState) {
if (!nextState.message.trim().length) {
// autosize.update(this.textInput)
}
}
handleKeyUp(e) {
if (e.key === 'Shift') { if (e.key === 'Shift') {
this.setState({ setShiftKeyDown(false);
shiftKeyDown: false,
});
}
} }
};
handleKeyPress(e) { const handleKeyPress = e => {
if (e.key === 'Shift') { if (e.key === 'Shift') {
this.setState({ setShiftKeyDown(true);
shiftKeyDown: true,
});
} }
// Fix when autosize is enabled - line breaks require shift+enter twice if (e.key === 'Enter' && !hasTouchSupport && !shiftKeyDown) {
if (e.key === 'Enter' && !hasTouchSupport && !this.state.shiftKeyDown) {
e.preventDefault(); e.preventDefault();
if (this.canSend()) { if (canSend) {
this.sendMessage(); sendMessage();
} else { } else {
this.setState({ setMessage('');
message: '',
});
}
} }
} }
};
executeCommand(command) { const executeCommand = command => {
const commandToExecute = this.commands.find(cmnd => cmnd.command === command.command); const commandToExecute = commands.find(cmnd => cmnd.command === command.command);
if (commandToExecute) { if (commandToExecute) {
const { params } = command; const { params } = command;
@ -175,19 +138,20 @@ export class Chat extends Component {
} }
return null; return null;
} };
handleSendClick() { const handleSendClick = evt => {
this.sendMessage.bind(this);
this.textInput.focus();
}
handleFormSubmit(evt) {
evt.preventDefault(); evt.preventDefault();
this.sendMessage(); sendMessage();
} textInputRef.current.focus();
};
parseCommand(message) { const handleFormSubmit = evt => {
evt.preventDefault();
sendMessage();
};
const parseCommand = message => {
const commandTrigger = { const commandTrigger = {
command: null, command: null,
params: [], params: [],
@ -207,23 +171,22 @@ export class Chat extends Component {
} }
return false; return false;
} };
sendMessage() { const sendMessage = () => {
if (!this.canSend()) { if (!canSend) {
return; return;
} }
const { message } = this.state; const isCommand = parseCommand(message);
const isCommand = this.parseCommand(message);
if (isCommand) { if (isCommand) {
const res = this.executeCommand(isCommand); const res = executeCommand(isCommand);
if (res === false) { if (res === false) {
return; return;
} }
} else { } else {
this.props.sendEncryptedMessage({ sendEncryptedMessage({
type: 'TEXT_MESSAGE', type: 'TEXT_MESSAGE',
payload: { payload: {
text: message, text: message,
@ -232,55 +195,41 @@ export class Chat extends Component {
}); });
} }
this.setState({ setMessage('');
message: '', };
});
}
handleInputChange(evt) { const handleInputChange = evt => {
this.setState({ setMessage(evt.target.value);
message: evt.target.value, };
});
}
canSend() {
return this.state.message.trim().length;
}
render() {
const touchSupport = this.state.touchSupport;
return ( return (
<form onSubmit={this.handleFormSubmit.bind(this)} className="chat-preflight-container"> <form onSubmit={handleFormSubmit} className="chat-preflight-container">
<textarea <textarea
rows="1" rows="1"
onKeyUp={this.handleKeyUp.bind(this)} onKeyUp={handleKeyUp}
onKeyDown={this.handleKeyPress.bind(this)} onKeyDown={handleKeyPress}
ref={input => { ref={textInputRef}
this.textInput = input;
}}
autoFocus autoFocus
className="chat" className="chat"
value={this.state.message} value={message}
placeholder={this.props.translations.typePlaceholder} placeholder={translations.typePlaceholder}
onChange={this.handleInputChange.bind(this)} onChange={handleInputChange}
/> />
<div className="input-controls"> <div className="input-controls">
<FileTransfer sendEncryptedMessage={this.props.sendEncryptedMessage} /> <FileTransfer sendEncryptedMessage={sendEncryptedMessage} />
{touchSupport && ( {touchSupport && (
<button <button
onClick={this.handleSendClick.bind(this)} onClick={handleSendClick}
className={`icon is-right send btn btn-link ${this.canSend() ? 'active' : ''}`} className={`icon is-right send btn btn-link ${canSend ? 'active' : ''}`}
title="Send" title="Send"
> >
<CornerDownRight className={this.canSend() ? '' : 'disabled'} /> <CornerDownRight className={canSend ? '' : 'disabled'} />
</button> </button>
)} )}
</div> </div>
</form> </form>
); );
} };
}
Chat.propTypes = { Chat.propTypes = {
sendEncryptedMessage: PropTypes.func.isRequired, sendEncryptedMessage: PropTypes.func.isRequired,

View File

@ -207,7 +207,7 @@ describe('Chat component', () => {
fireEvent.keyDown(textarea, { key: 'Enter' }); fireEvent.keyDown(textarea, { key: 'Enter' });
}); });
it('should work with touch support', () => { it('should work with touch support', async () => {
// Enable touch support // Enable touch support
dom.hasTouchSupport = true; dom.hasTouchSupport = true;
@ -239,15 +239,15 @@ describe('Chat component', () => {
fireEvent.change(textarea, { target: { value: 'test' } }); fireEvent.change(textarea, { target: { value: 'test' } });
// Touch send button // Touch send button
fireEvent.click(getByTitle('Send')); await fireEvent.click(getByTitle('Send'));
expect(sendEncryptedMessage).toHaveBeenLastCalledWith({ expect(sendEncryptedMessage).toHaveBeenLastCalledWith({
payload: { text: 'test', timestamp: 1584183718135 }, payload: { text: 'test', timestamp: 1584183718135 },
type: 'TEXT_MESSAGE', type: 'TEXT_MESSAGE',
}); });
// Should not send message // Should not send message because of the empty message
fireEvent.click(getByTitle('Send')); await fireEvent.click(getByTitle('Send'));
expect(sendEncryptedMessage).toHaveBeenCalledTimes(1); expect(sendEncryptedMessage).toHaveBeenCalledTimes(1);
}); });

View File

@ -1,7 +1,5 @@
import React, { Component } from 'react'; const Connecting = () => {
export default class Connecting extends Component {
render() {
return <div>Please wait while we secure a connection to Darkwire...</div>; return <div>Please wait while we secure a connection to Darkwire...</div>;
} };
}
export default Connecting;

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defer } from 'lodash'; import { defer } from 'lodash';
import { useSafeState, useEventListener } from '@react-hookz/web/esnext';
import ChatInput from '@/components/Chat'; import ChatInput from '@/components/Chat';
import T from '@/components/T'; import T from '@/components/T';
@ -10,15 +11,11 @@ import styles from './styles.module.scss';
const ActivityList = ({ activities, openModal }) => { const ActivityList = ({ activities, openModal }) => {
const [focusChat, setFocusChat] = React.useState(false); const [focusChat, setFocusChat] = React.useState(false);
const [scrolledToBottom, setScrolledToBottom] = React.useState(true); const [scrolledToBottom, setScrolledToBottom] = useSafeState(true);
const messageStream = React.useRef(null); const messageStream = React.useRef(null);
const activitiesList = React.useRef(null); const activitiesList = React.useRef(null);
React.useEffect(() => { useEventListener(messageStream, 'scroll', () => {
const currentMessageStream = messageStream.current;
// Update scrolledToBottom state if we scroll the activity stream
const onScroll = () => {
const messageStreamHeight = messageStream.current.clientHeight; const messageStreamHeight = messageStream.current.clientHeight;
const activitiesListHeight = activitiesList.current.clientHeight; const activitiesListHeight = activitiesList.current.clientHeight;
@ -35,14 +32,7 @@ const ActivityList = ({ activities, openModal }) => {
} else if (scrolledToBottom) { } else if (scrolledToBottom) {
setScrolledToBottom(false); setScrolledToBottom(false);
} }
}; });
currentMessageStream.addEventListener('scroll', onScroll);
return () => {
// Unbind event if component unmounted
currentMessageStream.removeEventListener('scroll', onScroll);
};
}, [scrolledToBottom]);
const scrollToBottomIfShould = React.useCallback(() => { const scrollToBottomIfShould = React.useCallback(() => {
if (scrolledToBottom) { if (scrolledToBottom) {
@ -57,7 +47,7 @@ const ActivityList = ({ activities, openModal }) => {
const scrollToBottom = React.useCallback(() => { const scrollToBottom = React.useCallback(() => {
messageStream.current.scrollTop = messageStream.current.scrollHeight; messageStream.current.scrollTop = messageStream.current.scrollHeight;
setScrolledToBottom(true); setScrolledToBottom(true);
}, []); }, [setScrolledToBottom]);
const handleChatClick = () => { const handleChatClick = () => {
setFocusChat(true); setFocusChat(true);

View File

@ -1,7 +1,7 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react'; import { render, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { act } from 'react-dom/test-utils';
import configureStore from '@/store'; import configureStore from '@/store';
@ -58,21 +58,25 @@ describe('ActivityList component', () => {
</Provider>, </Provider>,
); );
fireEvent.click(getByText('By using Darkwire, you are agreeing to our Acceptable Use Policy and Terms of Service')); await fireEvent.click(
vi.runAllTimers(); getByText('By using Darkwire, you are agreeing to our Acceptable Use Policy and Terms of Service'),
);
await act(() => vi.runAllTimers());
expect(mockOpenModal.mock.calls[0][0]).toBe('About'); expect(mockOpenModal.mock.calls[0][0]).toBe('About');
vi.runAllTimers(); await act(() => vi.runAllTimers());
}); });
it('should focus chat', () => { it('should focus chat', async () => {
const { getByTestId } = render( const { getByTestId } = render(
<Provider store={store}> <Provider store={store}>
<ActivityList openModal={vi.fn()} activities={[]} /> <ActivityList openModal={vi.fn()} activities={[]} />
</Provider>, </Provider>,
); );
fireEvent.click(getByTestId('main-div'));
vi.runAllTimers(); await fireEvent.click(getByTestId('main-div'));
await act(() => vi.runAllTimers());
}); });
it('should scroll to bottom on new message if not scrolled', () => { it('should scroll to bottom on new message if not scrolled', () => {

View File

@ -1,9 +1,11 @@
import React, { Component } from 'react'; import React from 'react';
import Modal from 'react-modal'; import ReactModal from 'react-modal';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { X, AlertCircle } from 'react-feather'; import { X, AlertCircle } from 'react-feather';
import classNames from 'classnames'; import classNames from 'classnames';
import { useSelector, useDispatch } from 'react-redux';
import { createUser } from '@/reducers/user';
import Crypto from '@/utils/crypto'; import Crypto from '@/utils/crypto';
import { connect as connectSocket } from '@/utils/socket'; import { connect as connectSocket } from '@/utils/socket';
@ -21,68 +23,26 @@ import styles from './styles.module.scss';
const crypto = new Crypto(); const crypto = new Crypto();
Modal.setAppElement('#root'); ReactModal.setAppElement('#root');
class Home extends Component { const Modal = ({
async componentWillMount() { closeModal,
const user = await this.createUser(); modalComponent,
roomId,
const socket = connectSocket(this.props.socketId); translations,
toggleSoundEnabled,
this.socket = socket; togglePersistenceEnabled,
soundIsEnabled,
socket.on('disconnect', () => { persistenceIsEnabled,
this.props.toggleSocketConnected(false); toggleNotificationEnabled,
}); toggleNotificationAllowed,
notificationIsEnabled,
socket.on('connect', () => { notificationIsAllowed,
this.initApp(user); setLanguage,
this.props.toggleSocketConnected(true); language,
}); }) => {
const getModal = () => {
socket.on('USER_ENTER', payload => { switch (modalComponent) {
this.props.receiveUnencryptedMessage('USER_ENTER', payload);
this.props.sendEncryptedMessage({
type: 'ADD_USER',
payload: {
username: this.props.username,
publicKey: this.props.publicKey,
isOwner: this.props.iAmOwner,
id: this.props.userId,
},
});
if (payload.users.length === 1) {
this.props.openModal('Welcome');
}
});
socket.on('USER_EXIT', payload => {
this.props.receiveUnencryptedMessage('USER_EXIT', payload);
});
socket.on('ENCRYPTED_MESSAGE', payload => {
this.props.receiveEncryptedMessage(payload);
});
socket.on('TOGGLE_LOCK_ROOM', payload => {
this.props.receiveUnencryptedMessage('TOGGLE_LOCK_ROOM', payload);
});
socket.on('ROOM_LOCKED', payload => {
this.props.openModal('Room Locked');
});
window.addEventListener('beforeunload', evt => {
socket.emit('USER_DISCONNECT');
});
}
componentDidMount() {
this.bindEvents();
}
getModal() {
switch (this.props.modalComponent) {
case 'Connecting': case 'Connecting':
return { return {
component: <Connecting />, component: <Connecting />,
@ -91,40 +51,38 @@ class Home extends Component {
}; };
case 'About': case 'About':
return { return {
component: <About roomId={this.props.roomId} />, component: <About roomId={roomId} />,
title: this.props.translations.aboutHeader, title: translations.aboutHeader,
}; };
case 'Settings': case 'Settings':
return { return {
component: ( component: (
<Settings <Settings
roomId={this.props.roomId} roomId={roomId}
toggleSoundEnabled={this.props.toggleSoundEnabled} toggleSoundEnabled={toggleSoundEnabled}
togglePersistenceEnabled={this.props.togglePersistenceEnabled} togglePersistenceEnabled={togglePersistenceEnabled}
soundIsEnabled={this.props.soundIsEnabled} soundIsEnabled={soundIsEnabled}
persistenceIsEnabled={this.props.persistenceIsEnabled} persistenceIsEnabled={persistenceIsEnabled}
toggleNotificationEnabled={this.props.toggleNotificationEnabled} toggleNotificationEnabled={toggleNotificationEnabled}
toggleNotificationAllowed={this.props.toggleNotificationAllowed} toggleNotificationAllowed={toggleNotificationAllowed}
notificationIsEnabled={this.props.notificationIsEnabled} notificationIsEnabled={notificationIsEnabled}
notificationIsAllowed={this.props.notificationIsAllowed} notificationIsAllowed={notificationIsAllowed}
setLanguage={this.props.setLanguage} setLanguage={setLanguage}
language={this.props.language} language={language}
translations={this.props.translations} translations={translations}
/> />
), ),
title: this.props.translations.settingsHeader, title: translations.settingsHeader,
}; };
case 'Welcome': case 'Welcome':
return { return {
component: ( component: <Welcome roomId={roomId} close={closeModal} translations={translations} />,
<Welcome roomId={this.props.roomId} close={this.props.closeModal} translations={this.props.translations} /> title: translations.welcomeHeader,
),
title: this.props.translations.welcomeHeader,
}; };
case 'Room Locked': case 'Room Locked':
return { return {
component: <RoomLocked modalContent={this.props.translations.lockedRoomHeader} />, component: <RoomLocked modalContent={translations.lockedRoomHeader} />,
title: this.props.translations.lockedRoomHeader, title: translations.lockedRoomHeader,
preventClose: true, preventClose: true,
}; };
default: default:
@ -133,71 +91,12 @@ class Home extends Component {
title: null, title: null,
}; };
} }
}
initApp(user) {
this.socket.emit('USER_ENTER', {
publicKey: user.publicKey,
});
}
bindEvents() {
window.onfocus = () => {
this.props.toggleWindowFocus(true);
}; };
window.onblur = () => { const modalOpts = getModal();
this.props.toggleWindowFocus(false);
};
}
createUser() {
return new Promise(async resolve => {
const username = this.props.username || nanoid();
const encryptDecryptKeys = await crypto.createEncryptDecryptKeys();
const exportedEncryptDecryptPrivateKey = await crypto.exportKey(encryptDecryptKeys.privateKey);
const exportedEncryptDecryptPublicKey = await crypto.exportKey(encryptDecryptKeys.publicKey);
this.props.createUser({
username,
publicKey: exportedEncryptDecryptPublicKey,
privateKey: exportedEncryptDecryptPrivateKey,
});
resolve({
publicKey: exportedEncryptDecryptPublicKey,
});
});
}
render() {
const modalOpts = this.getModal();
return ( return (
<div className={classNames(styles.styles, 'h-100')}> <ReactModal
<div className="nav-container"> isOpen={Boolean(modalComponent)}
{!this.props.socketConnected && (
<div className="alert-banner">
<span className="icon">
<AlertCircle size="15" />
</span>{' '}
<span>Disconnected</span>
</div>
)}
<Nav
members={this.props.members}
roomId={this.props.roomId}
roomLocked={this.props.roomLocked}
toggleLockRoom={() => this.props.sendUnencryptedMessage('TOGGLE_LOCK_ROOM')}
openModal={this.props.openModal}
iAmOwner={this.props.iAmOwner}
userId={this.props.userId}
translations={this.props.translations}
/>
</div>
<ActivityList openModal={this.props.openModal} activities={this.props.activities} />
<Modal
isOpen={Boolean(this.props.modalComponent)}
contentLabel="Modal" contentLabel="Modal"
style={{ overlay: { zIndex: 10 } }} style={{ overlay: { zIndex: 10 } }}
className={{ className={{
@ -211,34 +110,249 @@ class Home extends Component {
beforeClose: 'react-modal-overlay_before-close', beforeClose: 'react-modal-overlay_before-close',
}} }}
shouldCloseOnOverlayClick={!modalOpts.preventClose} shouldCloseOnOverlayClick={!modalOpts.preventClose}
onRequestClose={this.props.closeModal} onRequestClose={closeModal}
> >
<div className="react-modal-header"> <div className="react-modal-header">
{!modalOpts.preventClose && ( {!modalOpts.preventClose && (
<button onClick={this.props.closeModal} className="btn btn-link btn-plain close-modal"> <button onClick={closeModal} className="btn btn-link btn-plain close-modal">
<X /> <X />
</button> </button>
)} )}
<h3 className="react-modal-title">{modalOpts.title}</h3> <h3 className="react-modal-title">{modalOpts.title}</h3>
</div> </div>
<div className="react-modal-component">{modalOpts.component}</div> <div className="react-modal-component">{modalOpts.component}</div>
</Modal> </ReactModal>
);
};
const Home = ({
receiveEncryptedMessage,
receiveUnencryptedMessage,
activities,
username,
publicKey,
members,
socketId,
roomId,
roomLocked,
modalComponent,
openModal,
closeModal,
iAmOwner,
userId,
toggleWindowFocus,
soundIsEnabled,
persistenceIsEnabled,
toggleSoundEnabled,
togglePersistenceEnabled,
notificationIsEnabled,
notificationIsAllowed,
toggleNotificationEnabled,
toggleNotificationAllowed,
toggleSocketConnected,
socketConnected,
sendUnencryptedMessage,
sendEncryptedMessage,
translations,
setLanguage,
language,
}) => {
const socketPayloadRef = React.useRef({
username: username,
publicKey: publicKey,
isOwner: iAmOwner,
id: userId,
});
socketPayloadRef.current = {
username: username,
publicKey: publicKey,
isOwner: iAmOwner,
id: userId,
};
// Add blur et focus listeners
React.useEffect(() => {
const onFocus = () => {
toggleWindowFocus(true);
};
const onBlur = () => {
toggleWindowFocus(false);
};
window.addEventListener('focus', onFocus);
window.addEventListener('blur', onBlur);
return () => {
window.removeEventListener('focus', onFocus);
window.removeEventListener('blur', onBlur);
};
}, [toggleWindowFocus]);
React.useEffect(() => {
const socket = connectSocket(socketId);
socket.on('disconnect', () => {
toggleSocketConnected(false);
});
socket.on('connect', () => {
socket.emit('USER_ENTER', {
publicKey: socketPayloadRef.current.publicKey,
});
toggleSocketConnected(true);
});
socket.on('USER_ENTER', payload => {
receiveUnencryptedMessage('USER_ENTER', payload);
sendEncryptedMessage({
type: 'ADD_USER',
payload: socketPayloadRef.current,
});
if (payload.users.length === 1) {
openModal('Welcome');
}
});
socket.on('USER_EXIT', payload => {
receiveUnencryptedMessage('USER_EXIT', payload);
});
socket.on('ENCRYPTED_MESSAGE', payload => {
receiveEncryptedMessage(payload);
});
socket.on('TOGGLE_LOCK_ROOM', payload => {
receiveUnencryptedMessage('TOGGLE_LOCK_ROOM', payload);
});
socket.on('ROOM_LOCKED', () => {
openModal('Room Locked');
});
const onUnload = () => {
socket.emit('USER_DISCONNECT');
};
window.addEventListener('beforeunload', onUnload);
return () => {
window.removeEventListener('beforeunload', onUnload);
onUnload();
socket.close();
};
}, [
openModal,
receiveEncryptedMessage,
receiveUnencryptedMessage,
sendEncryptedMessage,
socketId,
toggleSocketConnected,
]);
return (
<div className={classNames(styles.styles, 'h-100')}>
<div className="nav-container">
{!socketConnected && (
<div className="alert-banner">
<span className="icon">
<AlertCircle size="15" />
</span>{' '}
<span>Disconnected</span>
</div>
)}
<Nav
members={members}
roomId={roomId}
roomLocked={roomLocked}
toggleLockRoom={() => sendUnencryptedMessage('TOGGLE_LOCK_ROOM')}
openModal={openModal}
iAmOwner={iAmOwner}
userId={userId}
translations={translations}
/>
</div>
<ActivityList openModal={openModal} activities={activities} />
<Modal
closeModal={closeModal}
modalComponent={modalComponent}
roomId={roomId}
translations={translations}
toggleSoundEnabled={toggleSoundEnabled}
togglePersistenceEnabled={togglePersistenceEnabled}
soundIsEnabled={soundIsEnabled}
persistenceIsEnabled={persistenceIsEnabled}
toggleNotificationEnabled={toggleNotificationEnabled}
toggleNotificationAllowed={toggleNotificationAllowed}
notificationIsEnabled={notificationIsEnabled}
notificationIsAllowed={notificationIsAllowed}
setLanguage={setLanguage}
language={language}
/>
</div> </div>
); );
} };
export const WithUser = ({ ...rest }) => {
const [loaded, setLoaded] = React.useState(false);
const loading = React.useRef(false);
const user = useSelector(state => state.user);
const dispatch = useDispatch();
React.useEffect(() => {
let mounted = true;
const createUserLocal = async () => {
const localUsername = user.username || nanoid();
const encryptDecryptKeys = await crypto.createEncryptDecryptKeys();
const exportedEncryptDecryptPrivateKey = await crypto.exportKey(encryptDecryptKeys.privateKey);
const exportedEncryptDecryptPublicKey = await crypto.exportKey(encryptDecryptKeys.publicKey);
if (!mounted) {
loading.current = false;
return;
} }
Home.defaultProps = { const payload = {
username: localUsername,
publicKey: exportedEncryptDecryptPublicKey,
privateKey: exportedEncryptDecryptPrivateKey,
};
dispatch(createUser(payload));
dispatch({ type: 'CREATE_USER', payload });
loading.current = false;
setLoaded(true);
};
if (!loaded && !loading.current) {
loading.current = true;
createUserLocal();
}
return () => {
loading.current = false;
mounted = false;
};
}, [dispatch, loaded, user.username]);
if (!loaded) {
return null;
}
return <Home username={user.username} publicKey={user.publicKey} userId={user.id} {...rest} />;
};
WithUser.defaultProps = {
modalComponent: null, modalComponent: null,
}; };
Home.propTypes = { WithUser.propTypes = {
receiveEncryptedMessage: PropTypes.func.isRequired, receiveEncryptedMessage: PropTypes.func.isRequired,
receiveUnencryptedMessage: PropTypes.func.isRequired, receiveUnencryptedMessage: PropTypes.func.isRequired,
createUser: PropTypes.func.isRequired,
activities: PropTypes.array.isRequired, activities: PropTypes.array.isRequired,
username: PropTypes.string.isRequired,
publicKey: PropTypes.object.isRequired,
members: PropTypes.array.isRequired, members: PropTypes.array.isRequired,
socketId: PropTypes.string.isRequired, socketId: PropTypes.string.isRequired,
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
@ -247,9 +361,7 @@ Home.propTypes = {
openModal: PropTypes.func.isRequired, openModal: PropTypes.func.isRequired,
closeModal: PropTypes.func.isRequired, closeModal: PropTypes.func.isRequired,
iAmOwner: PropTypes.bool.isRequired, iAmOwner: PropTypes.bool.isRequired,
userId: PropTypes.string.isRequired,
toggleWindowFocus: PropTypes.func.isRequired, toggleWindowFocus: PropTypes.func.isRequired,
faviconCount: PropTypes.number.isRequired,
soundIsEnabled: PropTypes.bool.isRequired, soundIsEnabled: PropTypes.bool.isRequired,
persistenceIsEnabled: PropTypes.bool.isRequired, persistenceIsEnabled: PropTypes.bool.isRequired,
toggleSoundEnabled: PropTypes.func.isRequired, toggleSoundEnabled: PropTypes.func.isRequired,
@ -262,6 +374,9 @@ Home.propTypes = {
socketConnected: PropTypes.bool.isRequired, socketConnected: PropTypes.bool.isRequired,
sendUnencryptedMessage: PropTypes.func.isRequired, sendUnencryptedMessage: PropTypes.func.isRequired,
sendEncryptedMessage: PropTypes.func.isRequired, sendEncryptedMessage: PropTypes.func.isRequired,
setLanguage: PropTypes.func.isRequired,
language: PropTypes.string.isRequired,
translations: PropTypes.object.isRequired,
}; };
export default Home; export default WithUser;

View File

@ -1,4 +1,3 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { test, expect, vi } from 'vitest'; import { test, expect, vi } from 'vitest';
@ -20,10 +19,18 @@ vi.mock('@/utils/socket', () => {
return { return {
on: vi.fn(), on: vi.fn(),
emit: vi.fn(), emit: vi.fn(),
close: vi.fn(),
};
}),
getSocket: vi.fn().mockImplementation(() => {
return {
on: vi.fn(),
emit: vi.fn(),
close: vi.fn(),
}; };
}), }),
}; };
}); // });
vi.mock('../../utils/crypto', () => { vi.mock('../../utils/crypto', () => {
// Need window.crytpo.subtle // Need window.crytpo.subtle
@ -45,14 +52,13 @@ vi.mock('../../utils/crypto', () => {
}); });
test('Home component is displaying', async () => { test('Home component is displaying', async () => {
const { asFragment } = render( const { asFragment, findByText } = render(
<Provider store={store}> <Provider store={store}>
<Home <Home
translations={{}} translations={{}}
members={[]} members={[]}
openModal={() => {}} openModal={() => {}}
activities={[]} activities={[]}
match={{ params: { roomId: 'roomTest' } }}
createUser={() => {}} createUser={() => {}}
toggleSocketConnected={() => {}} toggleSocketConnected={() => {}}
receiveEncryptedMessage={() => {}} receiveEncryptedMessage={() => {}}
@ -76,9 +82,16 @@ test('Home component is displaying', async () => {
closeModal={() => {}} closeModal={() => {}}
publicKey={{}} publicKey={{}}
username={'linus'} username={'linus'}
socketId={'roomTest'}
persistenceIsEnabled={false}
togglePersistenceEnabled={() => {}}
setLanguage={() => {}}
language={'en'}
/> />
</Provider>, </Provider>,
); );
await findByText('Disconnected');
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });

View File

@ -1,9 +1,9 @@
import React, { Component } from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Tinycon from 'tinycon'; import Tinycon from 'tinycon';
import { notify, beep } from '@/utils/notifications'; import { notify, beep } from '@/utils/notifications';
import { toggleNotificationAllowed, toggleNotificationEnabled } from '@/actions'; import { toggleNotificationAllowed } from '@/actions';
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
@ -19,19 +19,9 @@ const mapStateToProps = state => {
const mapDispatchToProps = { const mapDispatchToProps = {
toggleNotificationAllowed, toggleNotificationAllowed,
toggleNotificationEnabled,
}; };
const WithNewMessageNotification = WrappedComponent => { const WithNewMessageNotification = ({
return connect(
mapStateToProps,
mapDispatchToProps,
)(
class WithNotificationHOC extends Component {
state = { lastMessage: null, unreadMessageCount: 0 };
static getDerivedStateFromProps(nextProps, prevState) {
const {
room: { id: roomId }, room: { id: roomId },
activities, activities,
notificationIsEnabled, notificationIsEnabled,
@ -39,16 +29,20 @@ const WithNewMessageNotification = WrappedComponent => {
soundIsEnabled, soundIsEnabled,
unreadMessageCount, unreadMessageCount,
windowIsFocused, windowIsFocused,
} = nextProps; toggleNotificationAllowed,
children,
}) => {
const [lastMessage, setLastMessage] = React.useState(null);
const [lastUnreadMessageCount, setLastUnreadMessageCount] = React.useState(0);
React.useEffect(() => {
if (activities.length === 0) { if (activities.length === 0) {
return null; return;
} }
const currentLastMessage = activities[activities.length - 1];
const { username, type, text, fileName, locked, newUsername, currentUsername, action } = currentLastMessage;
const lastMessage = activities[activities.length - 1]; if (currentLastMessage !== lastMessage && !windowIsFocused) {
const { username, type, text, fileName, locked, newUsername, currentUsername, action } = lastMessage;
if (lastMessage !== prevState.lastMessage && !windowIsFocused) {
if (notificationIsAllowed && notificationIsEnabled) { if (notificationIsAllowed && notificationIsEnabled) {
// Generate the proper notification according to the message type // Generate the proper notification according to the message type
switch (type) { switch (type) {
@ -84,44 +78,40 @@ const WithNewMessageNotification = WrappedComponent => {
if (soundIsEnabled) beep.play(); if (soundIsEnabled) beep.play();
} }
if (unreadMessageCount !== prevState.unreadMessageCount) { setLastMessage(currentLastMessage);
if (unreadMessageCount !== lastUnreadMessageCount) {
setLastUnreadMessageCount(unreadMessageCount);
Tinycon.setBubble(unreadMessageCount); Tinycon.setBubble(unreadMessageCount);
} }
}, [
return { lastMessage, unreadMessageCount };
}
componentDidMount() {
switch (Notification.permission) {
case 'granted':
this.props.toggleNotificationAllowed(true);
break;
case 'denied':
this.props.toggleNotificationAllowed(false);
break;
default:
this.props.toggleNotificationAllowed(null);
}
}
render() {
// Filter props
const {
room,
activities, activities,
lastMessage,
lastUnreadMessageCount,
notificationIsAllowed,
notificationIsEnabled, notificationIsEnabled,
motificationIsAllowed, roomId,
soundIsEnabled, soundIsEnabled,
unreadMessageCount, unreadMessageCount,
windowIsFocused, windowIsFocused,
toggleNotificationAllowed, ]);
toggleNotificationnEnabled,
...rest React.useEffect(() => {
} = this.props; switch (Notification.permission) {
return <WrappedComponent {...rest} />; case 'granted':
toggleNotificationAllowed(true);
break;
case 'denied':
toggleNotificationAllowed(false);
break;
default:
toggleNotificationAllowed(null);
} }
}, }, [toggleNotificationAllowed]);
);
return <>{children}</>;
}; };
export default WithNewMessageNotification; const ConnectedWithNewMessageNotification = connect(mapStateToProps, mapDispatchToProps)(WithNewMessageNotification);
export default ConnectedWithNewMessageNotification;

View File

@ -62,10 +62,7 @@ exports[`Connected Home component > should display 1`] = `
/> />
<button <button
class="btn btn-plain btn-link clipboard-trigger room-id ellipsis" class="btn btn-plain btn-link clipboard-trigger room-id ellipsis"
data-clipboard-text="http://localhost:3000/" id="copy-room-url-button"
data-placement="bottom"
data-toggle="tooltip"
title="Copied"
> >
/ /
</button> </button>
@ -74,9 +71,8 @@ exports[`Connected Home component > should display 1`] = `
> >
<button <button
class="lock-room btn btn-link btn-plain" class="lock-room btn btn-link btn-plain"
data-placement="bottom" data-testid="lock-room-button"
data-toggle="tooltip" id="lock-room-button"
title="You must be the owner to lock or unlock the room"
> >
<svg <svg
class="muted" class="muted"
@ -105,10 +101,7 @@ exports[`Connected Home component > should display 1`] = `
</button> </button>
</span> </span>
<div <div
class="dropdown members-dropdown" class="members-menu"
>
<a
class="dropdown__trigger "
> >
<button <button
class="btn btn-link btn-plain members-action" class="btn btn-link btn-plain members-action"
@ -143,16 +136,8 @@ exports[`Connected Home component > should display 1`] = `
</svg> </svg>
</button> </button>
<span> <span>
0 1
</span> </span>
</a>
<div
class="dropdown__content "
>
<ul
class="plain"
/>
</div>
</div> </div>
</div> </div>
<button <button

View File

@ -3,7 +3,6 @@ import { useLoaderData } from 'react-router-dom';
import { import {
receiveEncryptedMessage, receiveEncryptedMessage,
createUser,
openModal, openModal,
closeModal, closeModal,
toggleWindowFocus, toggleWindowFocus,
@ -26,8 +25,6 @@ const mapStateToProps = state => {
return { return {
activities: state.activities.items, activities: state.activities.items,
userId: state.user.id,
username: state.user.username,
publicKey: state.user.publicKey, publicKey: state.user.publicKey,
privateKey: state.user.privateKey, privateKey: state.user.privateKey,
members: state.room.members.filter(m => m.username && m.publicKey), members: state.room.members.filter(m => m.username && m.publicKey),
@ -48,7 +45,6 @@ const mapStateToProps = state => {
const mapDispatchToProps = { const mapDispatchToProps = {
receiveEncryptedMessage, receiveEncryptedMessage,
createUser,
openModal, openModal,
closeModal, closeModal,
toggleWindowFocus, toggleWindowFocus,
@ -63,11 +59,19 @@ const mapDispatchToProps = {
setLanguage, setLanguage,
}; };
export const ConnectedHome = WithNewMessageNotification(connect(mapStateToProps, mapDispatchToProps)(Home)); export const ConnectedHome = connect(mapStateToProps, mapDispatchToProps)(Home);
export const ConnectedHomeWithNotification = ({ socketId, ...props }) => {
return (
<WithNewMessageNotification>
<ConnectedHome socketId={socketId} {...props} />
</WithNewMessageNotification>
);
};
const HomeWithParams = ({ ...props }) => { const HomeWithParams = ({ ...props }) => {
const socketId = useLoaderData(); const socketId = useLoaderData();
return <ConnectedHome socketId={socketId} {...props} />; return <ConnectedHomeWithNotification socketId={socketId} {...props} />;
}; };
export default HomeWithParams; export default HomeWithParams;

View File

@ -1,4 +1,3 @@
import React from 'react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import Tinycon from 'tinycon'; import Tinycon from 'tinycon';
@ -9,8 +8,9 @@ import configureStore from '@/store';
import { toggleWindowFocus, toggleNotificationEnabled, toggleSoundEnabled } from '@/actions/app'; import { toggleWindowFocus, toggleNotificationEnabled, toggleSoundEnabled } from '@/actions/app';
import { receiveEncryptedMessage } from '@/actions/encrypted_messages'; import { receiveEncryptedMessage } from '@/actions/encrypted_messages';
import { notify, beep } from '@/utils/notifications'; import { notify, beep } from '@/utils/notifications';
import { act } from 'react-dom/test-utils';
import { ConnectedHome } from './'; import { ConnectedHomeWithNotification } from './';
const store = configureStore(); const store = configureStore();
@ -45,12 +45,14 @@ vi.mock('@/utils/socket', () => {
return { return {
on: vi.fn(), on: vi.fn(),
emit: vi.fn(), emit: vi.fn(),
close: vi.fn(),
}; };
}), }),
getSocket: vi.fn().mockImplementation(() => { getSocket: vi.fn().mockImplementation(() => {
return { return {
on: vi.fn(), on: vi.fn(),
emit: vi.fn(), emit: vi.fn(),
close: vi.fn(),
}; };
}), }),
}; };
@ -102,68 +104,85 @@ describe('Connected Home component', () => {
delete global.Notification; delete global.Notification;
}); });
it('should display', () => { it('should display', async () => {
const { asFragment } = render( const { asFragment, findByText } = render(
<Provider store={store}> <Provider store={store}>
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" /> <ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
</Provider>, </Provider>,
); );
await findByText('Disconnected');
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
it('should set notification', () => { it('should detect notification granted', async () => {
render( const { findByText } = render(
<Provider store={store}> <Provider store={store}>
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" /> <ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
</Provider>, </Provider>,
); );
await findByText('Disconnected');
expect(store.getState().app.notificationIsAllowed).toBe(true); expect(store.getState().app.notificationIsAllowed).toBe(true);
expect(store.getState().app.notificationIsEnabled).toBe(true); expect(store.getState().app.notificationIsEnabled).toBe(true);
});
it('should detect notification denied', async () => {
global.Notification = { global.Notification = {
permission: 'denied', permission: 'denied',
}; };
render( const { findByText } = render(
<Provider store={store}> <Provider store={store}>
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" /> <ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
</Provider>, </Provider>,
); );
expect(store.getState().app.notificationIsAllowed).toBe(false); await findByText('Disconnected');
expect(store.getState().app.notificationIsAllowed).toBe(false);
});
it('should detect notification default', async () => {
global.Notification = { global.Notification = {
permission: 'default', permission: 'default',
}; };
render( const { findByText } = render(
<Provider store={store}> <Provider store={store}>
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" /> <ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
</Provider>, </Provider>,
); );
await findByText('Disconnected');
expect(store.getState().app.notificationIsAllowed).toBe(null); expect(store.getState().app.notificationIsAllowed).toBe(null);
}); });
it('should send notifications', async () => { it('should send notifications', async () => {
Modal.prototype.getSnapshotBeforeUpdate = vi.fn().mockReturnValue(null); Modal.prototype.getSnapshotBeforeUpdate = vi.fn().mockReturnValue(null);
const { rerender } = render(
const { rerender, findByText } = render(
<Provider store={store}> <Provider store={store}>
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" roomId={'testId'} /> <ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
</Provider>, </Provider>,
); );
await findByText('Disconnected');
// Test with window focused // Test with window focused
await receiveEncryptedMessage({ await act(() =>
receiveEncryptedMessage({
type: 'TEXT_MESSAGE', type: 'TEXT_MESSAGE',
payload: {}, payload: {},
})(store.dispatch, store.getState); })(store.dispatch, store.getState),
);
rerender( rerender(
<Provider store={store}> <Provider store={store}>
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" roomId={'testId'} /> <ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
</Provider>, </Provider>,
); );
@ -173,39 +192,48 @@ describe('Connected Home component', () => {
expect(Tinycon.setBubble).not.toHaveBeenCalled(); expect(Tinycon.setBubble).not.toHaveBeenCalled();
// Test with window unfocused // Test with window unfocused
await toggleWindowFocus(false)(store.dispatch); await act(() => toggleWindowFocus(false)(store.dispatch));
await receiveEncryptedMessage({ await act(() =>
receiveEncryptedMessage({
type: 'TEXT_MESSAGE', type: 'TEXT_MESSAGE',
payload: {}, payload: {},
})(store.dispatch, store.getState); })(store.dispatch, store.getState),
);
rerender( rerender(
<Provider store={store}> <Provider store={store}>
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" roomId={'testId'} /> <ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
</Provider>, </Provider>,
); );
expect(store.getState().app.unreadMessageCount).toBe(1); expect(store.getState().app.unreadMessageCount).toBe(1);
expect(notify).toHaveBeenCalledTimes(1);
expect(notify).toHaveBeenLastCalledWith('sender said:', 'new message'); expect(notify).toHaveBeenLastCalledWith('sender said:', 'new message');
expect(beep.play).toHaveBeenCalledTimes(1);
expect(beep.play).toHaveBeenLastCalledWith(); expect(beep.play).toHaveBeenLastCalledWith();
expect(Tinycon.setBubble).toHaveBeenLastCalledWith(1); expect(Tinycon.setBubble).toHaveBeenLastCalledWith(1);
notify.mockClear();
beep.play.mockClear();
// Test with sound and notification disabled // Test with sound and notification disabled
await toggleNotificationEnabled(false)(store.dispatch); await act(() => toggleNotificationEnabled(false)(store.dispatch));
await toggleSoundEnabled(false)(store.dispatch); await act(() => toggleSoundEnabled(false)(store.dispatch));
await receiveEncryptedMessage({ await act(() =>
receiveEncryptedMessage({
type: 'TEXT_MESSAGE', type: 'TEXT_MESSAGE',
payload: {}, payload: {},
})(store.dispatch, store.getState); })(store.dispatch, store.getState),
);
rerender( rerender(
<Provider store={store}> <Provider store={store}>
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" roomId={'testId'} /> <ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
</Provider>, </Provider>,
); );
expect(store.getState().app.unreadMessageCount).toBe(2); expect(store.getState().app.unreadMessageCount).toBe(2);
expect(notify).toHaveBeenCalledTimes(1); expect(notify).toHaveBeenCalledTimes(0);
expect(beep.play).toHaveBeenCalledTimes(1); expect(beep.play).toHaveBeenCalledTimes(0);
expect(Tinycon.setBubble).toHaveBeenLastCalledWith(2); expect(Tinycon.setBubble).toHaveBeenLastCalledWith(2);
}); });
}); });

View File

@ -1,19 +1,17 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import Linkify from 'react-linkify'; import Linkify from 'react-linkify';
import Username from '@/components/Username'; import Username from '@/components/Username';
class Message extends Component { const Message = ({ message, timestamp, sender }) => {
render() { const msg = decodeURI(message);
const msg = decodeURI(this.props.message);
return ( return (
<div> <div>
<div className="chat-meta"> <div className="chat-meta">
<Username username={this.props.sender} /> <Username username={sender} />
<span className="muted timestamp">{moment(this.props.timestamp).format('LT')}</span> <span className="muted timestamp">{moment(timestamp).format('LT')}</span>
</div> </div>
<div className="chat"> <div className="chat">
<Linkify <Linkify
@ -27,8 +25,7 @@ class Message extends Component {
</div> </div>
</div> </div>
); );
} };
}
Message.propTypes = { Message.propTypes = {
sender: PropTypes.string.isRequired, sender: PropTypes.string.isRequired,

View File

@ -1,7 +1,7 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react'; import { render, fireEvent, waitFor } from '@testing-library/react';
import mock$ from 'jquery'; import mock$ from 'jquery';
import { test, expect, vi } from 'vitest'; import { test, expect, vi } from 'vitest';
import { act } from 'react-dom/test-utils';
import Nav from '.'; import Nav from '.';
@ -35,7 +35,7 @@ vi.mock('nanoid', () => {
}; };
}); });
vi.useFakeTimers(); const mockClipboardWriteTest = vi.fn();
const mockTranslations = { const mockTranslations = {
newRoomButton: 'new room', newRoomButton: 'new room',
@ -43,6 +43,8 @@ const mockTranslations = {
aboutButton: 'about', aboutButton: 'about',
}; };
vi.useFakeTimers();
test('Nav component is displaying', async () => { test('Nav component is displaying', async () => {
const { asFragment } = render( const { asFragment } = render(
<Nav <Nav
@ -58,10 +60,6 @@ test('Nav component is displaying', async () => {
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
expect(mock$).toHaveBeenCalledWith('.room-id');
expect(mock$).toHaveBeenLastCalledWith('.lock-room');
expect(mockTooltip).toHaveBeenLastCalledWith({ trigger: 'manual' });
}); });
test('Nav component is displaying with another configuration and can rerender', async () => { test('Nav component is displaying with another configuration and can rerender', async () => {
@ -99,16 +97,15 @@ test('Nav component is displaying with another configuration and can rerender',
/>, />,
); );
expect(mock$).toHaveBeenCalledWith('.me-icon-wrap'); expect(asFragment()).toMatchSnapshot();
expect(mock$).toHaveBeenLastCalledWith('.owner-icon-wrap');
}); });
test('Can copy room url', async () => { test('Can copy room url', async () => {
document.execCommand = vi.fn(() => true); navigator.clipboard = { writeText: mockClipboardWriteTest };
const toggleLockRoom = vi.fn(); const toggleLockRoom = vi.fn();
const { getByText } = render( const { getByText, queryByText } = render(
<Nav <Nav
members={[ members={[
{ id: 'id1', username: 'alan', isOwner: true }, { id: 'id1', username: 'alan', isOwner: true },
@ -120,27 +117,28 @@ test('Can copy room url', async () => {
toggleLockRoom={toggleLockRoom} toggleLockRoom={toggleLockRoom}
openModal={() => {}} openModal={() => {}}
iAmOwner={false} iAmOwner={false}
translations={{}} translations={{ copyButtonTooltip: 'Copied' }}
/>, />,
); );
fireEvent.click(getByText(`/testRoom`)); await act(async () => {
await fireEvent.click(getByText('/testRoom'));
});
expect(document.execCommand).toHaveBeenLastCalledWith('copy'); expect(mockClipboardWriteTest).toHaveBeenLastCalledWith('http://localhost:3000/testRoom');
expect(mock$).toHaveBeenCalledTimes(15);
expect(mockTooltip).toHaveBeenLastCalledWith('show'); await getByText('Copied');
// Wait tooltip closing // Wait tooltip closing
vi.runAllTimers(); await act(() => vi.runAllTimers());
expect(mock$).toHaveBeenCalledTimes(18); expect(queryByText('Copied')).not.toBeInTheDocument();
expect(mockTooltip).toHaveBeenLastCalledWith('hide');
}); });
test('Can lock/unlock room is room owner only', async () => { test('Can lock/unlock room is room owner only', async () => {
const toggleLockRoom = vi.fn(); const toggleLockRoom = vi.fn();
const { rerender, getByTitle } = render( const { rerender, getByTestId, getByText, queryByText } = render(
<Nav <Nav
members={[ members={[
{ id: 'id1', username: 'alan', isOwner: true }, { id: 'id1', username: 'alan', isOwner: true },
@ -156,13 +154,13 @@ test('Can lock/unlock room is room owner only', async () => {
/>, />,
); );
const toggleLockRoomButton = getByTitle('You must be the owner to lock or unlock the room'); const toggleLockRoomButton = getByTestId('lock-room-button');
fireEvent.click(toggleLockRoomButton); await fireEvent.click(toggleLockRoomButton);
expect(toggleLockRoom).toHaveBeenCalledWith(); expect(toggleLockRoom).toHaveBeenCalledWith();
fireEvent.click(toggleLockRoomButton); await fireEvent.click(toggleLockRoomButton);
expect(toggleLockRoom).toHaveBeenCalledTimes(2); expect(toggleLockRoom).toHaveBeenCalledTimes(2);
@ -183,11 +181,16 @@ test('Can lock/unlock room is room owner only', async () => {
/>, />,
); );
fireEvent.click(toggleLockRoomButton); await fireEvent.click(toggleLockRoomButton);
expect(toggleLockRoom).toHaveBeenCalledTimes(2); expect(toggleLockRoom).toHaveBeenCalledTimes(2);
expect(mock$).toHaveBeenLastCalledWith('.lock-room');
expect(mockTooltip).toHaveBeenLastCalledWith('show'); await getByText('You must be the owner to lock or unlock the room');
// Wait tooltip closing
await act(() => vi.runAllTimers());
expect(queryByText('You must be the owner to lock or unlock the room')).not.toBeInTheDocument();
}); });
test('Can show user list', async () => { test('Can show user list', async () => {
@ -227,8 +230,10 @@ test('Can show user list', async () => {
translations={{}} translations={{}}
/>, />,
); );
await waitFor(() => expect(getByText('alan')).toBeInTheDocument()); await waitFor(() => expect(getByText('alan')).toBeInTheDocument());
await waitFor(() => expect(getByText('dan')).toBeInTheDocument()); await waitFor(() => expect(getByText('dan')).toBeInTheDocument());
expect(queryByTitle('Owner')).not.toBeInTheDocument(); expect(queryByTitle('Owner')).not.toBeInTheDocument();
expect(queryByTitle('Me')).not.toBeInTheDocument(); expect(queryByTitle('Me')).not.toBeInTheDocument();
}); });

View File

@ -15,9 +15,7 @@ exports[`Nav component is displaying 1`] = `
/> />
<button <button
class="btn btn-plain btn-link clipboard-trigger room-id ellipsis" class="btn btn-plain btn-link clipboard-trigger room-id ellipsis"
data-clipboard-text="http://localhost:3000/testRoom" id="copy-room-url-button"
data-placement="bottom"
data-toggle="tooltip"
> >
/testRoom /testRoom
</button> </button>
@ -26,9 +24,8 @@ exports[`Nav component is displaying 1`] = `
> >
<button <button
class="lock-room btn btn-link btn-plain" class="lock-room btn btn-link btn-plain"
data-placement="bottom" data-testid="lock-room-button"
data-toggle="tooltip" id="lock-room-button"
title="You must be the owner to lock or unlock the room"
> >
<svg <svg
class="muted" class="muted"
@ -57,10 +54,7 @@ exports[`Nav component is displaying 1`] = `
</button> </button>
</span> </span>
<div <div
class="dropdown members-dropdown" class="members-menu"
>
<a
class="dropdown__trigger "
> >
<button <button
class="btn btn-link btn-plain members-action" class="btn btn-link btn-plain members-action"
@ -97,14 +91,6 @@ exports[`Nav component is displaying 1`] = `
<span> <span>
0 0
</span> </span>
</a>
<div
class="dropdown__content "
>
<ul
class="plain"
/>
</div>
</div> </div>
</div> </div>
<button <button
@ -257,9 +243,7 @@ exports[`Nav component is displaying with another configuration and can rerender
/> />
<button <button
class="btn btn-plain btn-link clipboard-trigger room-id ellipsis" class="btn btn-plain btn-link clipboard-trigger room-id ellipsis"
data-clipboard-text="http://localhost:3000/testRoom_2" id="copy-room-url-button"
data-placement="bottom"
data-toggle="tooltip"
> >
/testRoom_2 /testRoom_2
</button> </button>
@ -268,9 +252,8 @@ exports[`Nav component is displaying with another configuration and can rerender
> >
<button <button
class="lock-room btn btn-link btn-plain" class="lock-room btn btn-link btn-plain"
data-placement="bottom" data-testid="lock-room-button"
data-toggle="tooltip" id="lock-room-button"
title="You must be the owner to lock or unlock the room"
> >
<svg <svg
fill="none" fill="none"
@ -298,10 +281,234 @@ exports[`Nav component is displaying with another configuration and can rerender
</button> </button>
</span> </span>
<div <div
class="dropdown members-dropdown" class="members-menu"
> >
<a <button
class="dropdown__trigger " class="btn btn-link btn-plain members-action"
title="Users"
>
<svg
class="users-icon"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"
/>
<circle
cx="9"
cy="7"
r="4"
/>
<path
d="M23 21v-2a4 4 0 0 0-3-3.87"
/>
<path
d="M16 3.13a4 4 0 0 1 0 7.75"
/>
</svg>
</button>
<span>
2
</span>
</div>
</div>
<button
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
class="navbar-toggler"
data-target="#navbarSupportedContent"
data-toggle="collapse"
type="button"
>
<span
class="navbar-toggler-icon"
/>
</button>
<div
class="collapse navbar-collapse"
id="navbarSupportedContent"
>
<ul
class="navbar-nav ml-auto"
>
<li
class="nav-item"
>
<button
class="btn btn-plain nav-link"
target="blank"
>
<svg
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
/>
<line
x1="12"
x2="12"
y1="8"
y2="16"
/>
<line
x1="8"
x2="16"
y1="12"
y2="12"
/>
</svg>
<span />
</button>
</li>
<li
class="nav-item"
>
<button
class="btn btn-plain nav-link"
>
<svg
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="3"
/>
<path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
/>
</svg>
<span />
</button>
</li>
<li
class="nav-item"
>
<button
class="btn btn-plain nav-link"
>
<svg
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
/>
<line
x1="12"
x2="12"
y1="16"
y2="12"
/>
<line
x1="12"
x2="12.01"
y1="8"
y2="8"
/>
</svg>
<span />
</button>
</li>
</ul>
</div>
</nav>
</DocumentFragment>
`;
exports[`Nav component is displaying with another configuration and can rerender 2`] = `
<DocumentFragment>
<nav
class="navbar navbar-expand-md navbar-dark"
>
<div
class="meta"
>
<img
alt="Darkwire"
class="logo"
src="/src/img/logo.png"
/>
<button
class="btn btn-plain btn-link clipboard-trigger room-id ellipsis"
id="copy-room-url-button"
>
/testRoom_3
</button>
<span
class="lock-room-container"
>
<button
class="lock-room btn btn-link btn-plain"
data-testid="lock-room-button"
id="lock-room-button"
>
<svg
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<rect
height="11"
rx="2"
ry="2"
width="18"
x="3"
y="11"
/>
<path
d="M7 11V7a5 5 0 0 1 10 0v4"
/>
</svg>
</button>
</span>
<div
class="members-menu"
> >
<button <button
class="btn btn-link btn-plain members-action" class="btn btn-link btn-plain members-action"
@ -338,61 +545,6 @@ exports[`Nav component is displaying with another configuration and can rerender
<span> <span>
2 2
</span> </span>
</a>
<div
class="dropdown__content "
>
<ul
class="plain"
>
<li>
<span
class="username"
style="color: rgb(192, 202, 249);"
>
alan
</span>
<span
class="icon-container"
>
<span
class="owner-icon-wrap"
data-placement="bottom"
data-toggle="tooltip"
title="Owner"
>
<svg
class="owner-icon"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<polygon
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
/>
</svg>
</span>
</span>
</li>
<li>
<span
class="username"
style="color: rgb(156, 252, 223);"
>
dan
</span>
<span
class="icon-container"
/>
</li>
</ul>
</div>
</div> </div>
</div> </div>
<button <button

View File

@ -1,124 +1,132 @@
import React, { Component } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { Info, Settings, PlusCircle, User, Users, Lock, Unlock, Star } from 'react-feather'; import { Info, Settings, PlusCircle, User, Users, Lock, Unlock, Star } from 'react-feather';
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
import Clipboard from 'clipboard';
import $ from 'jquery'; import $ from 'jquery';
import { Tooltip } from 'react-tooltip';
import { useClickOutside, useSafeState } from '@react-hookz/web/esnext';
import logoImg from '@/img/logo.png'; import logoImg from '@/img/logo.png';
import Username from '@/components/Username'; import Username from '@/components/Username';
class Nav extends Component { const Nav = ({ members, roomId, userId, roomLocked, toggleLockRoom, openModal, iAmOwner, translations }) => {
componentDidMount() { const [showCopyTooltip, setShowCopyTooltip] = useSafeState(false);
const clip = new Clipboard('.clipboard-trigger'); const [showLockedTooltip, setShowLockedTooltip] = useSafeState(false);
const [showMemberList, setShowMemberList] = useSafeState(false);
const userListRef = React.useRef(null);
const roomUrl = `${window.location.origin}/${roomId}`;
clip.on('success', () => { useClickOutside(userListRef, () => {
$('.room-id').tooltip('show'); setShowMemberList(false);
setTimeout(() => {
$('.room-id').tooltip('hide');
}, 3000);
}); });
$(() => { const newRoom = () => {
$('.room-id').tooltip({
trigger: 'manual',
});
$('.lock-room').tooltip({
trigger: 'manual',
});
});
}
componentDidUpdate() {
$(() => {
$('.me-icon-wrap').tooltip();
$('.owner-icon-wrap').tooltip();
});
}
newRoom() {
$('.navbar-collapse').collapse('hide'); $('.navbar-collapse').collapse('hide');
window.open(`/${nanoid()}`); window.open(`/${nanoid()}`);
} };
handleSettingsClick() { const handleSettingsClick = () => {
$('.navbar-collapse').collapse('hide'); $('.navbar-collapse').collapse('hide');
this.props.openModal('Settings'); openModal('Settings');
} };
handleAboutClick() { const handleAboutClick = () => {
$('.navbar-collapse').collapse('hide'); $('.navbar-collapse').collapse('hide');
this.props.openModal('About'); openModal('About');
} };
handleToggleLock() { const handleToggleLock = () => {
if (!this.props.iAmOwner) { if (!iAmOwner) {
$('.lock-room').tooltip('show'); setShowLockedTooltip(true);
setTimeout(() => $('.lock-room').tooltip('hide'), 3000); setTimeout(() => {
return; setShowLockedTooltip(false);
} }, 2000);
this.props.toggleLockRoom(); } else {
toggleLockRoom();
} }
};
const handleCopy = async () => {
await navigator.clipboard.writeText(roomUrl);
setShowCopyTooltip(true);
setTimeout(() => {
setShowCopyTooltip(false);
}, 2000);
};
render() {
return ( return (
<nav className="navbar navbar-expand-md navbar-dark"> <nav className="navbar navbar-expand-md navbar-dark">
<div className="meta"> <div className="meta">
<img src={logoImg} alt="Darkwire" className="logo" /> <img src={logoImg} alt="Darkwire" className="logo" />
<button <button
data-toggle="tooltip" id="copy-room-url-button"
data-placement="bottom"
title={this.props.translations.copyButtonTooltip}
data-clipboard-text={`${window.location.origin}/${this.props.roomId}`}
className="btn btn-plain btn-link clipboard-trigger room-id ellipsis" className="btn btn-plain btn-link clipboard-trigger room-id ellipsis"
onClick={handleCopy}
> >
{`/${this.props.roomId}`} {`/${roomId}`}
</button> </button>
{showCopyTooltip && (
<Tooltip
anchorId="copy-room-url-button"
content={translations.copyButtonTooltip}
place="bottom"
events={[]}
isOpen={true}
/>
)}
<span className="lock-room-container"> <span className="lock-room-container">
<button <button
onClick={this.handleToggleLock.bind(this)} id="lock-room-button"
data-testid="lock-room-button"
onClick={handleToggleLock}
className="lock-room btn btn-link btn-plain" className="lock-room btn btn-link btn-plain"
data-toggle="tooltip"
data-placement="bottom"
title="You must be the owner to lock or unlock the room"
> >
{this.props.roomLocked && <Lock />} {roomLocked && <Lock />}
{!this.props.roomLocked && <Unlock className="muted" />} {!roomLocked && <Unlock className="muted" />}
</button> </button>
{showLockedTooltip && (
<Tooltip
anchorId="lock-room-button"
content="You must be the owner to lock or unlock the room"
place="bottom"
events={[]}
isOpen={true}
/>
)}
</span> </span>
<div className="members-menu" ref={userListRef}>
<Dropdown className="members-dropdown"> <button
<DropdownTrigger> className="btn btn-link btn-plain members-action"
<button className="btn btn-link btn-plain members-action" title="Users"> title="Users"
onClick={() => setShowMemberList(prev => !prev)}
>
<Users className="users-icon" /> <Users className="users-icon" />
</button> </button>
<span>{this.props.members.length}</span> <span>{members.length}</span>
</DropdownTrigger>
<DropdownContent> {showMemberList && (
<ul className="plain"> <ul className="member-list">
{this.props.members.map((member, index) => ( {members.map((member, index) => (
<li key={`user-${index}`}> <li key={`user-${index}`}>
<Username username={member.username} /> <Username username={member.username} />
<span className="icon-container"> <span className="icon-container">
{member.id === this.props.userId && ( {member.id === userId && (
<span data-toggle="tooltip" data-placement="bottom" title="Me" className="me-icon-wrap"> <span className="me-icon-wrap" title="Me">
<User className="me-icon" /> <User className="me-icon" />
</span> </span>
)} )}
{member.isOwner && ( {member.isOwner && (
<span data-toggle="tooltip" data-placement="bottom" title="Owner" className="owner-icon-wrap"> <span className="owner-icon-wrap">
<Star className="owner-icon" /> <Star className="owner-icon" title="Owner" />
</span> </span>
)} )}
</span> </span>
</li> </li>
))} ))}
</ul> </ul>
</DropdownContent> )}
</Dropdown> </div>
</div> </div>
<button <button
@ -135,26 +143,25 @@ class Nav extends Component {
<div className="collapse navbar-collapse" id="navbarSupportedContent"> <div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav ml-auto"> <ul className="navbar-nav ml-auto">
<li className="nav-item"> <li className="nav-item">
<button className="btn btn-plain nav-link" onClick={this.newRoom.bind(this)} target="blank"> <button className="btn btn-plain nav-link" onClick={newRoom} target="blank">
<PlusCircle /> <span>{this.props.translations.newRoomButton}</span> <PlusCircle /> <span>{translations.newRoomButton}</span>
</button> </button>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<button onClick={this.handleSettingsClick.bind(this)} className="btn btn-plain nav-link"> <button onClick={handleSettingsClick} className="btn btn-plain nav-link">
<Settings /> <span>{this.props.translations.settingsButton}</span> <Settings /> <span>{translations.settingsButton}</span>
</button> </button>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<button onClick={this.handleAboutClick.bind(this)} className="btn btn-plain nav-link"> <button onClick={handleAboutClick} className="btn btn-plain nav-link">
<Info /> <span>{this.props.translations.aboutButton}</span> <Info /> <span>{translations.aboutButton}</span>
</button> </button>
</li> </li>
</ul> </ul>
</div> </div>
</nav> </nav>
); );
} };
}
Nav.propTypes = { Nav.propTypes = {
members: PropTypes.array.isRequired, members: PropTypes.array.isRequired,

View File

@ -1,8 +1,7 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react'; import { render, fireEvent } from '@testing-library/react';
import RoomLink from '.'; import RoomLink from '.';
import mock$ from 'jquery'; import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, afterEach } from 'vitest'; import { act } from 'react-dom/test-utils';
const mockTooltip = vi.fn().mockImplementation(param => {}); const mockTooltip = vi.fn().mockImplementation(param => {});
@ -26,43 +25,31 @@ const mockTranslations = {
}; };
describe('RoomLink', () => { describe('RoomLink', () => {
afterEach(() => {
mock$.mockClear();
});
it('should display', async () => { it('should display', async () => {
const { asFragment, unmount } = render(<RoomLink roomId="roomId" translations={mockTranslations} />); const { asFragment } = render(<RoomLink roomId="roomId" translations={mockTranslations} />);
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
expect(mock$).toHaveBeenLastCalledWith('.copy-room');
expect(mockTooltip).toHaveBeenLastCalledWith({ trigger: 'manual' });
mock$.mockClear();
unmount();
expect(mock$).toHaveBeenLastCalledWith('.copy-room');
expect(mockTooltip).toHaveBeenLastCalledWith('hide');
}); });
it('should copy link', async () => { it('should copy link', async () => {
// Mock execCommand for paste const mockClipboardWriteTest = vi.fn();
document.execCommand = vi.fn(() => true); navigator.clipboard = { writeText: mockClipboardWriteTest };
const { getByTitle } = render(<RoomLink roomId="roomId" translations={mockTranslations} />); const { getByTestId, queryByText, getByText } = render(
<RoomLink roomId="roomId" translations={mockTranslations} />,
);
await fireEvent.click(getByTitle(mockTranslations.copyButtonTooltip)); await act(async () => {
await fireEvent.click(getByTestId('copy-room-button'));
});
expect(document.execCommand).toHaveBeenLastCalledWith('copy'); expect(mockClipboardWriteTest).toHaveBeenLastCalledWith('http://localhost:3000/roomId');
expect(mock$).toHaveBeenCalledTimes(4);
expect(mock$).toHaveBeenLastCalledWith('.copy-room');
expect(mockTooltip).toHaveBeenLastCalledWith('show');
// Wait for tooltip to close await getByText(mockTranslations.copyButtonTooltip);
vi.runAllTimers();
expect(mock$).toHaveBeenCalledTimes(6); // Wait tooltip closing
expect(mock$).toHaveBeenLastCalledWith('.copy-room'); await act(() => vi.runAllTimers());
expect(mockTooltip).toHaveBeenLastCalledWith('hide');
expect(queryByText('Copied')).not.toBeInTheDocument();
}); });
}); });

View File

@ -21,10 +21,8 @@ exports[`RoomLink > should display 1`] = `
> >
<button <button
class="copy-room btn btn-secondary" class="copy-room btn btn-secondary"
data-clipboard-text="http://localhost:3000/roomId" data-testid="copy-room-button"
data-placement="bottom" id="copy-room"
data-toggle="tooltip"
title="copyButton"
type="button" type="button"
> >
<svg <svg

View File

@ -1,62 +1,61 @@
import React, { Component } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Copy } from 'react-feather'; import { Copy } from 'react-feather';
import Clipboard from 'clipboard'; import { Tooltip } from 'react-tooltip';
import $ from 'jquery';
class RoomLink extends Component { const RoomLink = ({ roomId, translations }) => {
constructor(props) { const [showTooltip, setShowTooltip] = React.useState(false);
super(props); const mountedRef = React.useRef(true);
this.state = {
roomUrl: `${window.location.origin}/${props.roomId}`, const roomUrl = `${window.location.origin}/${roomId}`;
React.useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
}; };
} }, []);
componentDidMount() { const handleClick = async () => {
const clip = new Clipboard('.copy-room'); await navigator.clipboard.writeText(roomUrl);
setShowTooltip(true);
clip.on('success', () => {
$('.copy-room').tooltip('show');
setTimeout(() => { setTimeout(() => {
$('.copy-room').tooltip('hide'); if (mountedRef.current) {
}, 3000); setShowTooltip(false);
});
$(() => {
$('.copy-room').tooltip({
trigger: 'manual',
});
});
} }
}, 2000);
};
componentWillUnmount() {
if ($('.copy-room').tooltip) $('.copy-room').tooltip('hide');
}
render() {
return ( return (
<form> <form>
<div className="form-group"> <div className="form-group">
<div className="input-group"> <div className="input-group">
<input id="room-url" className="form-control" type="text" readOnly value={this.state.roomUrl} /> <input id="room-url" className="form-control" type="text" readOnly value={roomUrl} />
<div className="input-group-append"> <div className="input-group-append">
<button <button
id="copy-room"
data-testid="copy-room-button"
className="copy-room btn btn-secondary" className="copy-room btn btn-secondary"
type="button" type="button"
data-toggle="tooltip" onClick={handleClick}
data-placement="bottom"
data-clipboard-text={this.state.roomUrl}
title={this.props.translations.copyButtonTooltip}
> >
<Copy /> <Copy />
</button> </button>
</div> </div>
{showTooltip && (
<Tooltip
anchorId="copy-room"
content={translations.copyButtonTooltip}
place="top"
events={[]}
isOpen={true}
/>
)}
</div> </div>
</div> </div>
</form> </form>
); );
} };
}
RoomLink.propTypes = { RoomLink.propTypes = {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,

View File

@ -1,7 +1,5 @@
import React, { Component } from 'react'; const RoomLocked = ({ modalContent }) => {
return <div>{modalContent}</div>;
};
export default class RoomLocked extends Component { export default RoomLocked;
render() {
return <div>{this.props.modalContent}</div>;
}
}

View File

@ -1,4 +1,3 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react'; import { render, fireEvent, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
@ -9,10 +8,6 @@ import Settings from '.';
const store = configureStore(); const store = configureStore();
const mockTranslations = {
sound: 'soundCheck',
};
vi.useFakeTimers(); vi.useFakeTimers();
vi.mock('@/components/RoomLink'); vi.mock('@/components/RoomLink');
@ -39,11 +34,14 @@ describe('Settings component', () => {
<Settings <Settings
soundIsEnabled={true} soundIsEnabled={true}
toggleSoundEnabled={() => {}} toggleSoundEnabled={() => {}}
persistenceIsEnabled={true}
togglePersistenceEnabled={() => {}}
notificationIsEnabled={true} notificationIsEnabled={true}
toggleNotificationEnabled={() => {}} toggleNotificationEnabled={() => {}}
toggleNotificationAllowed={vi.fn()} toggleNotificationAllowed={vi.fn()}
roomId="roomId" roomId="roomId"
setLanguage={() => {}} setLanguage={() => {}}
language="en"
translations={{}} translations={{}}
/> />
</Provider>, </Provider>,
@ -56,12 +54,15 @@ describe('Settings component', () => {
<Settings <Settings
soundIsEnabled={true} soundIsEnabled={true}
toggleSoundEnabled={() => {}} toggleSoundEnabled={() => {}}
persistenceIsEnabled={true}
togglePersistenceEnabled={() => {}}
notificationIsEnabled={true} notificationIsEnabled={true}
notificationIsAllowed={false} notificationIsAllowed={false}
toggleNotificationEnabled={() => {}} toggleNotificationEnabled={() => {}}
toggleNotificationAllowed={vi.fn()} toggleNotificationAllowed={vi.fn()}
roomId="roomId" roomId="roomId"
setLanguage={() => {}} setLanguage={() => {}}
language="en"
translations={{}} translations={{}}
/> />
</Provider>, </Provider>,
@ -77,18 +78,20 @@ describe('Settings component', () => {
<Settings <Settings
soundIsEnabled={true} soundIsEnabled={true}
toggleSoundEnabled={toggleSound} toggleSoundEnabled={toggleSound}
persistenceIsEnabled={true}
togglePersistenceEnabled={() => {}}
notificationIsEnabled={true} notificationIsEnabled={true}
notificationIsAllowed={true} notificationIsAllowed={true}
toggleNotificationEnabled={() => {}} toggleNotificationEnabled={() => {}}
toggleNotificationAllowed={vi.fn()} toggleNotificationAllowed={vi.fn()}
roomId="roomId" roomId="roomId"
setLanguage={() => {}} setLanguage={() => {}}
language="en"
translations={{}} translations={{}}
/> />
</Provider>, </Provider>,
); );
//console.log(getAllByText(mockTranslations.sound)[1]);
fireEvent.click(getByText('Sound')); fireEvent.click(getByText('Sound'));
expect(toggleSound).toHaveBeenCalledWith(false); expect(toggleSound).toHaveBeenCalledWith(false);
@ -105,12 +108,15 @@ describe('Settings component', () => {
<Settings <Settings
soundIsEnabled={true} soundIsEnabled={true}
toggleSoundEnabled={() => {}} toggleSoundEnabled={() => {}}
persistenceIsEnabled={true}
togglePersistenceEnabled={() => {}}
notificationIsEnabled={true} notificationIsEnabled={true}
notificationIsAllowed={true} notificationIsAllowed={true}
toggleNotificationEnabled={toggleNotifications} toggleNotificationEnabled={toggleNotifications}
toggleNotificationAllowed={vi.fn()} toggleNotificationAllowed={vi.fn()}
roomId="roomId" roomId="roomId"
setLanguage={() => {}} setLanguage={() => {}}
language="en"
translations={{}} translations={{}}
/> />
</Provider>, </Provider>,
@ -137,12 +143,15 @@ describe('Settings component', () => {
<Settings <Settings
soundIsEnabled={true} soundIsEnabled={true}
toggleSoundEnabled={() => {}} toggleSoundEnabled={() => {}}
persistenceIsEnabled={true}
togglePersistenceEnabled={() => {}}
notificationIsEnabled={true} notificationIsEnabled={true}
notificationIsAllowed={true} notificationIsAllowed={true}
toggleNotificationEnabled={toggleNotifications} toggleNotificationEnabled={toggleNotifications}
toggleNotificationAllowed={toggleAllowed} toggleNotificationAllowed={toggleAllowed}
roomId="roomId" roomId="roomId"
setLanguage={() => {}} setLanguage={() => {}}
language="en"
translations={{}} translations={{}}
/> />
</Provider>, </Provider>,
@ -166,11 +175,14 @@ describe('Settings component', () => {
<Settings <Settings
soundIsEnabled={true} soundIsEnabled={true}
toggleSoundEnabled={() => {}} toggleSoundEnabled={() => {}}
persistenceIsEnabled={true}
togglePersistenceEnabled={() => {}}
notificationIsEnabled={true} notificationIsEnabled={true}
toggleNotificationEnabled={() => {}} toggleNotificationEnabled={() => {}}
toggleNotificationAllowed={vi.fn()} toggleNotificationAllowed={vi.fn()}
roomId="roomId" roomId="roomId"
setLanguage={changeLang} setLanguage={changeLang}
language="en"
translations={{}} translations={{}}
/> />
</Provider>, </Provider>,

View File

@ -50,6 +50,7 @@ exports[`Settings component > should display 1`] = `
for="persistence-control" for="persistence-control"
> >
<input <input
checked=""
class="form-check-input" class="form-check-input"
id="persistence-control" id="persistence-control"
type="checkbox" type="checkbox"
@ -261,6 +262,7 @@ exports[`Settings component > should display 2`] = `
for="persistence-control" for="persistence-control"
> >
<input <input
checked=""
class="form-check-input" class="form-check-input"
id="persistence-control" id="persistence-control"
type="checkbox" type="checkbox"

View File

@ -1,4 +1,3 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import RoomLink from '@/components/RoomLink'; import RoomLink from '@/components/RoomLink';
@ -6,33 +5,44 @@ import T from '@/components/T';
import classes from './styles.module.scss'; import classes from './styles.module.scss';
class Settings extends Component { const Settings = ({
handleSoundToggle() { soundIsEnabled,
this.props.toggleSoundEnabled(!this.props.soundIsEnabled); persistenceIsEnabled,
} toggleSoundEnabled,
notificationIsEnabled,
notificationIsAllowed,
toggleNotificationEnabled,
toggleNotificationAllowed,
togglePersistenceEnabled,
roomId,
setLanguage,
language,
translations,
}) => {
const handleSoundToggle = () => {
toggleSoundEnabled(!soundIsEnabled);
};
handlePersistenceToggle() { const handlePersistenceToggle = () => {
this.props.togglePersistenceEnabled(!this.props.persistenceIsEnabled); togglePersistenceEnabled(!persistenceIsEnabled);
} };
handleNotificationToggle() { const handleNotificationToggle = () => {
Notification.requestPermission().then(permission => { Notification.requestPermission().then(permission => {
if (permission === 'granted') { if (permission === 'granted') {
this.props.toggleNotificationEnabled(!this.props.notificationIsEnabled); toggleNotificationEnabled(!notificationIsEnabled);
this.props.toggleNotificationAllowed(true); toggleNotificationAllowed(true);
} }
if (permission === 'denied') { if (permission === 'denied') {
this.props.toggleNotificationAllowed(false); toggleNotificationAllowed(false);
} }
}); });
} };
handleLanguageChange(evt) { const handleLanguageChange = evt => {
const language = evt.target.value; setLanguage(evt.target.value);
this.props.setLanguage(language); };
}
render() {
return ( return (
<div className={classes.styles}> <div className={classes.styles}>
<section> <section>
@ -44,40 +54,40 @@ class Settings extends Component {
<label className="form-check-label" htmlFor="sound-control"> <label className="form-check-label" htmlFor="sound-control">
<input <input
id="sound-control" id="sound-control"
onChange={this.handleSoundToggle.bind(this)} onChange={handleSoundToggle}
className="form-check-input" className="form-check-input"
type="checkbox" type="checkbox"
checked={this.props.soundIsEnabled} checked={soundIsEnabled}
/> />
<T path="sound" /> <T path="sound" />
</label> </label>
</div> </div>
<div className="form-check"> <div className="form-check">
<label className="form-check-label" htmlFor="notif-control"> <label className="form-check-label" htmlFor="notif-control">
{this.props.notificationIsAllowed !== false && ( {notificationIsAllowed !== false && (
<> <>
<input <input
id="notif-control" id="notif-control"
onChange={this.handleNotificationToggle.bind(this)} onChange={handleNotificationToggle}
className="form-check-input" className="form-check-input"
type="checkbox" type="checkbox"
checked={this.props.notificationIsEnabled} checked={notificationIsEnabled}
disabled={this.props.notificationIsAllowed === false} // Important to keep '=== false' here disabled={notificationIsAllowed === false} // Important to keep '=== false' here
/> />
<T path="desktopNotification" /> <T path="desktopNotification" />
</> </>
)} )}
{this.props.notificationIsAllowed === false && <T path="desktopNotificationBlocked" />} {notificationIsAllowed === false && <T path="desktopNotificationBlocked" />}
</label> </label>
</div> </div>
<div className="form-check"> <div className="form-check">
<label className="form-check-label" htmlFor="persistence-control"> <label className="form-check-label" htmlFor="persistence-control">
<input <input
id="persistence-control" id="persistence-control"
onChange={this.handlePersistenceToggle.bind(this)} onChange={handlePersistenceToggle}
className="form-check-input" className="form-check-input"
type="checkbox" type="checkbox"
checked={this.props.persistenceIsEnabled} checked={persistenceIsEnabled}
/> />
<T path="persistence" /> <T path="persistence" />
</label> </label>
@ -89,7 +99,7 @@ class Settings extends Component {
<h4 className="mb-3"> <h4 className="mb-3">
<T path="copyRoomHeader" /> <T path="copyRoomHeader" />
</h4> </h4>
<RoomLink roomId={this.props.roomId} translations={this.props.translations} /> <RoomLink roomId={roomId} translations={translations} />
</section> </section>
<section> <section>
@ -106,11 +116,7 @@ class Settings extends Component {
</a> </a>
</p> </p>
<div className="form-group"> <div className="form-group">
<select <select value={language} className="form-control" onChange={handleLanguageChange}>
value={this.props.language}
className="form-control"
onChange={this.handleLanguageChange.bind(this)}
>
<option value="en">English</option> <option value="en">English</option>
<option value="fr">Français</option> <option value="fr">Français</option>
<option value="oc">Occitan</option> <option value="oc">Occitan</option>
@ -180,19 +186,20 @@ class Settings extends Component {
</section> </section>
</div> </div>
); );
} };
}
Settings.propTypes = { Settings.propTypes = {
soundIsEnabled: PropTypes.bool.isRequired, soundIsEnabled: PropTypes.bool.isRequired,
persistenceIsEnabled: PropTypes.bool.isRequired, persistenceIsEnabled: PropTypes.bool.isRequired,
toggleSoundEnabled: PropTypes.func.isRequired, toggleSoundEnabled: PropTypes.func.isRequired,
togglePersistenceEnabled: PropTypes.func.isRequired,
notificationIsEnabled: PropTypes.bool.isRequired, notificationIsEnabled: PropTypes.bool.isRequired,
notificationIsAllowed: PropTypes.bool, notificationIsAllowed: PropTypes.bool,
toggleNotificationEnabled: PropTypes.func.isRequired, toggleNotificationEnabled: PropTypes.func.isRequired,
toggleNotificationAllowed: PropTypes.func.isRequired, toggleNotificationAllowed: PropTypes.func.isRequired,
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
setLanguage: PropTypes.func.isRequired, setLanguage: PropTypes.func.isRequired,
language: PropTypes.string.isRequired,
translations: PropTypes.object.isRequired, translations: PropTypes.object.isRequired,
}; };

View File

@ -1,4 +1,3 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import _ from 'lodash'; import _ from 'lodash';
@ -6,24 +5,25 @@ import { getTranslations } from '@/i18n';
const regex = /{(.*?)}/g; const regex = /{(.*?)}/g;
class T extends Component { const T = ({ language, path, data }) => {
render() { const t = getTranslations(language);
const t = getTranslations(this.props.language);
const englishT = getTranslations('en'); const englishT = getTranslations('en');
const str = _.get(t, this.props.path, '') || _.get(englishT, this.props.path, ''); const str = _.get(t, path, '') || _.get(englishT, path, '');
let string = str.split(regex); let string = str.split(regex);
if (this.props.data) {
// Data for string interpolation
if (data) {
string = string.map(word => { string = string.map(word => {
if (this.props.data[word]) { if (data[word]) {
return this.props.data[word]; return data[word];
} }
return word; return word;
}); });
return <span>{string}</span>; return <span>{string}</span>;
} }
return string; return string;
} };
}
T.propTypes = { T.propTypes = {
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,

View File

@ -1,4 +1,3 @@
import React from 'react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { test, expect } from 'vitest'; import { test, expect } from 'vitest';

View File

@ -1,16 +1,13 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import randomColor from 'randomcolor'; import randomColor from 'randomcolor';
class Username extends Component { const Username = ({ username }) => {
render() {
return ( return (
<span className="username" style={{ color: randomColor({ seed: this.props.username, luminosity: 'light' }) }}> <span className="username" style={{ color: randomColor({ seed: username, luminosity: 'light' }) }}>
{this.props.username} {username}
</span> </span>
); );
} };
}
Username.propTypes = { Username.propTypes = {
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,

View File

@ -1,16 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import RoomLink from '@/components/RoomLink'; import RoomLink from '@/components/RoomLink';
class Welcome extends Component { const Welcome = ({ roomId, translations, close }) => {
constructor(props) {
super(props);
this.state = {
roomUrl: `https://darkwire.io/${props.roomId}`,
};
}
render() {
return ( return (
<div> <div>
<div> <div>
@ -32,16 +23,15 @@ class Welcome extends Component {
</div> </div>
<br /> <br />
<p className="mb-2">Others can join this room using the following URL:</p> <p className="mb-2">Others can join this room using the following URL:</p>
<RoomLink roomId={this.props.roomId} translations={this.props.translations} /> <RoomLink roomId={roomId} translations={translations} />
<div className="react-modal-footer"> <div className="react-modal-footer">
<button className="btn btn-primary btn-lg" onClick={this.props.close}> <button className="btn btn-primary btn-lg" onClick={close}>
{this.props.translations.welcomeModalCTA} {translations.welcomeModalCTA}
</button> </button>
</div> </div>
</div> </div>
); );
} };
}
Welcome.propTypes = { Welcome.propTypes = {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,

View File

@ -32,6 +32,7 @@
"lista totas las comandas" "lista totas las comandas"
], ],
"sound": "Son", "sound": "Son",
"persistence": "Memorizar la configuracion",
"newMessageNotification": "Notificacion de messatge novèl", "newMessageNotification": "Notificacion de messatge novèl",
"desktopNotification": "Notificacion de burèu", "desktopNotification": "Notificacion de burèu",
"welcomeModalCTA": "Dacòrdi", "welcomeModalCTA": "Dacòrdi",

View File

@ -5,9 +5,9 @@ import { nanoid } from 'nanoid';
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom'; import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import 'react-simple-dropdown/styles/Dropdown.css';
import './stylesheets/app.sass'; import './stylesheets/app.sass';
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; import 'bootstrap/dist/js/bootstrap.bundle.min.js';
import 'react-tooltip/dist/react-tooltip.css';
import configureStore from '@/store/'; import configureStore from '@/store/';
import Home from '@/components/Home/'; import Home from '@/components/Home/';

View File

@ -50,7 +50,7 @@ const room = (state = initialState, action) => {
return { return {
...state, ...state,
members: [ members: [
...state.members, ...state.members.filter(({ id })=> id !== action.payload.publicKey.n),
{ {
username: action.payload.username, username: action.payload.username,
publicKey: action.payload.publicKey, publicKey: action.payload.publicKey,

View File

@ -1,27 +1,28 @@
const initialState = { import { createSlice } from '@reduxjs/toolkit'
export const userSlice = createSlice({
name: 'user',
initialState: {
privateKey: {}, privateKey: {},
publicKey: {}, publicKey: {},
username: '', username: '',
id: '', id: '',
}; },
reducers: {
const user = (receivedState, action) => { createUser: (state, action) => {
const state = { ...initialState, ...receivedState }; const { privateKey, publicKey, username } = action.payload;
state.privateKey = privateKey;
switch (action.type) { state.publicKey = publicKey;
case 'CREATE_USER': state.username = username;
return { state.id = publicKey.n;
...action.payload, },
id: action.payload.publicKey.n, changeUsername: (state, action) => {
}; const { newUsername } = action.payload;
case 'SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME': state.username = newUsername;
return {
...state,
username: action.payload.newUsername,
};
default:
return state;
} }
}; },
})
export default user; export const { createUser,changeUsername } = userSlice.actions
export default userSlice.reducer

View File

@ -1,6 +1,6 @@
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import reducer from './user'; import reducer, { createUser, changeUsername} from './user';
vi.mock('@/i18n', () => { vi.mock('@/i18n', () => {
return { return {
@ -15,7 +15,8 @@ describe('User reducer', () => {
it('should handle CREATE_USER', () => { it('should handle CREATE_USER', () => {
const payload = { publicKey: { n: 'alicekey' }, username: 'alice' }; const payload = { publicKey: { n: 'alicekey' }, username: 'alice' };
expect(reducer({}, { type: 'CREATE_USER', payload })).toEqual({
expect(reducer({},createUser(payload) )).toEqual({
id: 'alicekey', id: 'alicekey',
publicKey: { n: 'alicekey' }, publicKey: { n: 'alicekey' },
username: 'alice', username: 'alice',
@ -24,10 +25,8 @@ describe('User reducer', () => {
it('should handle SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME', () => { it('should handle SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME', () => {
const payload = { newUsername: 'alice' }; const payload = { newUsername: 'alice' };
expect(reducer({ username: 'polux' }, { type: 'SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME', payload })).toEqual({
id: '', expect(reducer({ username: 'polux' }, changeUsername(payload))).toEqual({
privateKey: {},
publicKey: {},
username: 'alice', username: 'alice',
}); });
}); });

View File

@ -1,17 +1,10 @@
/* istanbul ignore file */ /* istanbul ignore file */
import { createStore, applyMiddleware, compose } from 'redux'; import { configureStore } from '@reduxjs/toolkit'
import thunk from 'redux-thunk';
import reducers from '@/reducers'; import reducers from '@/reducers';
const composeEnhancers =
import.meta.env.NODE_ENV === 'production' ? compose : window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const enabledMiddlewares = [thunk]; export default function confStore(preloadedState) {
const store = configureStore({reducer:reducers, preloadedState })
const middlewares = applyMiddleware(...enabledMiddlewares);
export default function configureStore(preloadedState) {
const store = createStore(reducers, preloadedState, composeEnhancers(middlewares));
return store; return store;
} }

View File

@ -75,20 +75,19 @@ button.lock-room
padding: 0px 1rem padding: 0px 1rem
background: black background: black
.members-dropdown .members-menu
margin-left: 10px position: relative
.dropdown__trigger
.members-action .members-action
margin-right: 5px margin-right: 5px
.dropdown__content .member-list
position: absolute
background: black background: black
border-radius: 3px border-radius: 3px
border: 1px solid $inactive border: 1px solid $inactive
padding: 15px padding: 15px
width: 175px width: 175px
left: -10px left: -5px
top: 35px top: 35px
ul
li li
overflow: auto overflow: auto
height: 25px height: 25px

View File

@ -15,6 +15,7 @@
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }

View File

@ -345,6 +345,18 @@
minimatch "^3.1.2" minimatch "^3.1.2"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@floating-ui/core@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.0.4.tgz#03066eaea8e9b2a2cd3f5aaa60f1e0f580ebe88e"
integrity sha512-FPFLbg2b06MIw1dqk2SOEMAMX3xlrreGjcui5OTxfBDtaKTmh0kioOVjT8gcfl58juawL/yF+S+gnq8aUYQx/Q==
"@floating-ui/dom@^1.0.4":
version "1.0.12"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.0.12.tgz#07c870a05d9b825a6d7657524f48fe6761722800"
integrity sha512-HeG/wHoa2laUHlDX3xkzqlUqliAfa+zqV04LaKIwNCmCNaW2p0fQi4/Kd0LB4GdFoJ2UllLFq5gWnXAd67lg7w==
dependencies:
"@floating-ui/core" "^1.0.4"
"@humanwhocodes/config-array@^0.11.6": "@humanwhocodes/config-array@^0.11.6":
version "0.11.7" version "0.11.7"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.7.tgz#38aec044c6c828f6ed51d5d7ae3d9b9faf6dbb0f" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.7.tgz#38aec044c6c828f6ed51d5d7ae3d9b9faf6dbb0f"
@ -477,6 +489,28 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@react-hookz/deep-equal@^1.0.3":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@react-hookz/deep-equal/-/deep-equal-1.0.4.tgz#68a71f36cbc88724b3ce6f4036183778b6e7f282"
integrity sha512-N56fTrAPUDz/R423pag+n6TXWbvlBZDtTehaGFjK0InmN+V2OFWLE/WmORhmn6Ce7dlwH5+tQN1LJFw3ngTJVg==
"@react-hookz/web@^20.0.2":
version "20.0.2"
resolved "https://registry.yarnpkg.com/@react-hookz/web/-/web-20.0.2.tgz#6aab268ec74fedc3c3e43ee659b59dc40c2d9d05"
integrity sha512-OJdpUnF+0Ywg65LFTVmQz3uwscGG2CiDGUazda7YKwClyQVFjcEFNEJzkzfuBNvVLIgeRPllRfurMn8uoqTquQ==
dependencies:
"@react-hookz/deep-equal" "^1.0.3"
"@reduxjs/toolkit@^1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.1.tgz#4c34dc4ddcec161535288c60da5c19c3ef15180e"
integrity sha512-HikrdY+IDgRfRYlCTGUQaiCxxDDgM1mQrRbZ6S1HFZX5ZYuJ4o8EstNmhTwHdPl2rTmLxzwSu0b3AyeyTlR+RA==
dependencies:
immer "^9.0.16"
redux "^4.2.0"
redux-thunk "^2.4.2"
reselect "^4.1.7"
"@remix-run/router@1.0.4": "@remix-run/router@1.0.4":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.0.4.tgz#cbfbec6735711e7c2fc93b9b40adf70ef5a39990" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.0.4.tgz#cbfbec6735711e7c2fc93b9b40adf70ef5a39990"
@ -895,11 +929,43 @@ aria-query@^5.0.0:
dependencies: dependencies:
deep-equal "^2.0.5" deep-equal "^2.0.5"
array-includes@^3.1.5, array-includes@^3.1.6:
version "3.1.6"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f"
integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.4"
es-abstract "^1.20.4"
get-intrinsic "^1.1.3"
is-string "^1.0.7"
array-union@^2.1.0: array-union@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
array.prototype.flatmap@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183"
integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.4"
es-abstract "^1.20.4"
es-shim-unscopables "^1.0.0"
array.prototype.tosorted@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532"
integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.4"
es-abstract "^1.20.4"
es-shim-unscopables "^1.0.0"
get-intrinsic "^1.1.3"
assertion-error@^1.1.0: assertion-error@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
@ -1046,20 +1112,11 @@ ci-info@^3.2.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.0.tgz#6d01b3696c59915b6ce057e4aa4adfc2fa25f5ef" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.0.tgz#6d01b3696c59915b6ce057e4aa4adfc2fa25f5ef"
integrity sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog== integrity sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==
classnames@^2.1.2, classnames@^2.3.2: classnames@^2.3.2:
version "2.3.2" version "2.3.2"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
clipboard@^2.0.11:
version "2.0.11"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5"
integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==
dependencies:
good-listener "^1.2.2"
select "^1.1.2"
tiny-emitter "^2.0.0"
color-convert@^1.9.0: color-convert@^1.9.0:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@ -1216,11 +1273,6 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
delegate@^3.1.2:
version "3.2.0"
resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
diff-sequences@^29.3.1: diff-sequences@^29.3.1:
version "29.3.1" version "29.3.1"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e"
@ -1233,6 +1285,13 @@ dir-glob@^3.0.1:
dependencies: dependencies:
path-type "^4.0.0" path-type "^4.0.0"
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
dependencies:
esutils "^2.0.2"
doctrine@^3.0.0: doctrine@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
@ -1308,6 +1367,37 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
es-abstract@^1.19.0, es-abstract@^1.20.4:
version "1.20.5"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.5.tgz#e6dc99177be37cacda5988e692c3fa8b218e95d2"
integrity sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ==
dependencies:
call-bind "^1.0.2"
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
function.prototype.name "^1.1.5"
get-intrinsic "^1.1.3"
get-symbol-description "^1.0.0"
gopd "^1.0.1"
has "^1.0.3"
has-property-descriptors "^1.0.0"
has-symbols "^1.0.3"
internal-slot "^1.0.3"
is-callable "^1.2.7"
is-negative-zero "^2.0.2"
is-regex "^1.1.4"
is-shared-array-buffer "^1.0.2"
is-string "^1.0.7"
is-weakref "^1.0.2"
object-inspect "^1.12.2"
object-keys "^1.1.1"
object.assign "^4.1.4"
regexp.prototype.flags "^1.4.3"
safe-regex-test "^1.0.0"
string.prototype.trimend "^1.0.6"
string.prototype.trimstart "^1.0.6"
unbox-primitive "^1.0.2"
es-get-iterator@^1.1.2: es-get-iterator@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7"
@ -1322,6 +1412,22 @@ es-get-iterator@^1.1.2:
is-string "^1.0.5" is-string "^1.0.5"
isarray "^2.0.5" isarray "^2.0.5"
es-shim-unscopables@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241"
integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==
dependencies:
has "^1.0.3"
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
dependencies:
is-callable "^1.1.4"
is-date-object "^1.0.1"
is-symbol "^1.0.2"
esbuild@^0.16.3: esbuild@^0.16.3:
version "0.16.4" version "0.16.4"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.4.tgz#06c86298d233386f5e41bcc14d36086daf3f40bd" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.4.tgz#06c86298d233386f5e41bcc14d36086daf3f40bd"
@ -1396,6 +1502,32 @@ eslint-plugin-prettier@^3.1.3:
dependencies: dependencies:
prettier-linter-helpers "^1.0.0" prettier-linter-helpers "^1.0.0"
eslint-plugin-react-hooks@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3"
integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==
eslint-plugin-react@^7.31.11:
version "7.31.11"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.11.tgz#011521d2b16dcf95795df688a4770b4eaab364c8"
integrity sha512-TTvq5JsT5v56wPa9OYHzsrOlHzKZKjV+aLgS+55NJP/cuzdiQPC7PfYoUjMoxlffKtvijpk7vA/jmuqRb9nohw==
dependencies:
array-includes "^3.1.6"
array.prototype.flatmap "^1.3.1"
array.prototype.tosorted "^1.1.1"
doctrine "^2.1.0"
estraverse "^5.3.0"
jsx-ast-utils "^2.4.1 || ^3.0.0"
minimatch "^3.1.2"
object.entries "^1.1.6"
object.fromentries "^2.0.6"
object.hasown "^1.1.2"
object.values "^1.1.6"
prop-types "^15.8.1"
resolve "^2.0.0-next.3"
semver "^6.3.0"
string.prototype.matchall "^4.0.8"
eslint-scope@^5.1.1: eslint-scope@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
@ -1507,7 +1639,7 @@ estraverse@^4.1.1:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.1.0, estraverse@^5.2.0: estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
@ -1637,6 +1769,16 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
function.prototype.name@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621"
integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
es-abstract "^1.19.0"
functions-have-names "^1.2.2"
functions-have-names@^1.2.2: functions-have-names@^1.2.2:
version "1.2.3" version "1.2.3"
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
@ -1666,6 +1808,14 @@ get-stdin@^6.0.0:
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==
get-symbol-description@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==
dependencies:
call-bind "^1.0.2"
get-intrinsic "^1.1.1"
glob-parent@^5.1.2, glob-parent@~5.1.2: glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@ -1716,13 +1866,6 @@ globby@^11.1.0:
merge2 "^1.4.1" merge2 "^1.4.1"
slash "^3.0.0" slash "^3.0.0"
good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==
dependencies:
delegate "^3.1.2"
gopd@^1.0.1: gopd@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
@ -1740,7 +1883,7 @@ grapheme-splitter@^1.0.4:
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
has-bigints@^1.0.1: has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
@ -1839,6 +1982,11 @@ ignore@^5.2.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c"
integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==
immer@^9.0.16:
version "9.0.16"
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.16.tgz#8e7caab80118c2b54b37ad43e05758cdefad0198"
integrity sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ==
immutable@^4.0.0: immutable@^4.0.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
@ -1875,6 +2023,15 @@ inherits@2:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
internal-slot@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==
dependencies:
get-intrinsic "^1.1.0"
has "^1.0.3"
side-channel "^1.0.4"
is-arguments@^1.1.0, is-arguments@^1.1.1: is-arguments@^1.1.0, is-arguments@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
@ -1905,7 +2062,7 @@ is-boolean-object@^1.1.0:
call-bind "^1.0.2" call-bind "^1.0.2"
has-tostringtag "^1.0.0" has-tostringtag "^1.0.0"
is-callable@^1.1.3: is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7:
version "1.2.7" version "1.2.7"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
@ -1924,7 +2081,7 @@ is-core-module@^2.9.0:
dependencies: dependencies:
has "^1.0.3" has "^1.0.3"
is-date-object@^1.0.5: is-date-object@^1.0.1, is-date-object@^1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
@ -1948,6 +2105,11 @@ is-map@^2.0.1, is-map@^2.0.2:
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==
is-negative-zero@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==
is-number-object@^1.0.4: is-number-object@^1.0.4:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc"
@ -1988,14 +2150,21 @@ is-set@^2.0.1, is-set@^2.0.2:
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec"
integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==
is-string@^1.0.5: is-shared-array-buffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==
dependencies:
call-bind "^1.0.2"
is-string@^1.0.5, is-string@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
dependencies: dependencies:
has-tostringtag "^1.0.0" has-tostringtag "^1.0.0"
is-symbol@^1.0.3: is-symbol@^1.0.2, is-symbol@^1.0.3:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
@ -2018,6 +2187,13 @@ is-weakmap@^2.0.1:
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
is-weakref@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==
dependencies:
call-bind "^1.0.2"
is-weakset@^2.0.1: is-weakset@^2.0.1:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d"
@ -2255,6 +2431,14 @@ json5@^2.2.1:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
"jsx-ast-utils@^2.4.1 || ^3.0.0":
version "3.3.3"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea"
integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==
dependencies:
array-includes "^3.1.5"
object.assign "^4.1.3"
levn@^0.4.1: levn@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
@ -2441,7 +2625,7 @@ object-assign@^4.1.1:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-inspect@^1.9.0: object-inspect@^1.12.2, object-inspect@^1.9.0:
version "1.12.2" version "1.12.2"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
@ -2459,7 +2643,7 @@ object-keys@^1.1.1:
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
object.assign@^4.1.4: object.assign@^4.1.3, object.assign@^4.1.4:
version "4.1.4" version "4.1.4"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f"
integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==
@ -2469,6 +2653,41 @@ object.assign@^4.1.4:
has-symbols "^1.0.3" has-symbols "^1.0.3"
object-keys "^1.1.1" object-keys "^1.1.1"
object.entries@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.6.tgz#9737d0e5b8291edd340a3e3264bb8a3b00d5fa23"
integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.4"
es-abstract "^1.20.4"
object.fromentries@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73"
integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.4"
es-abstract "^1.20.4"
object.hasown@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.2.tgz#f919e21fad4eb38a57bc6345b3afd496515c3f92"
integrity sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==
dependencies:
define-properties "^1.1.4"
es-abstract "^1.20.4"
object.values@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d"
integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.4"
es-abstract "^1.20.4"
once@^1.3.0: once@^1.3.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@ -2634,7 +2853,7 @@ promise-polyfill@^8.1.3:
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.3.tgz#2edc7e4b81aff781c88a0d577e5fe9da822107c6" resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.3.tgz#2edc7e4b81aff781c88a0d577e5fe9da822107c6"
integrity sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg== integrity sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==
prop-types@^15.5.8, prop-types@^15.7.2: prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -2753,13 +2972,13 @@ react-router@6.4.4, react-router@^6.4.4:
dependencies: dependencies:
"@remix-run/router" "1.0.4" "@remix-run/router" "1.0.4"
react-simple-dropdown@^3.2.3: react-tooltip@^5.2.0:
version "3.2.3" version "5.2.0"
resolved "https://registry.yarnpkg.com/react-simple-dropdown/-/react-simple-dropdown-3.2.3.tgz#c9737bcb7a54c7de267a1afeeec04de588a3fa7b" resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.2.0.tgz#e10e7de2385e8fe6bf3438739c574558b455de3b"
integrity sha512-NmyyvA0D4wph5ctzkn8U4wmblOacavJMl9gTOhQR3v8I997mc1FL1NFKkj3Mx+HNysBKRD/HI+kpxXCAgXumPw== integrity sha512-EH6XIg2MDbMTEElSAZQVXMVeFoOhTgQuea2or0iwyzsr9v8rJf3ImMhOtq7Xe/BPlougxC+PmOibazodLdaRoA==
dependencies: dependencies:
classnames "^2.1.2" "@floating-ui/dom" "^1.0.4"
prop-types "^15.5.8" classnames "^2.3.2"
react@^18.2.0: react@^18.2.0:
version "18.2.0" version "18.2.0"
@ -2819,6 +3038,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
reselect@^4.1.7:
version "4.1.7"
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.7.tgz#56480d9ff3d3188970ee2b76527bd94a95567a42"
integrity sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==
resolve-from@^4.0.0: resolve-from@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@ -2833,6 +3057,15 @@ resolve@^1.22.1:
path-parse "^1.0.7" path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0" supports-preserve-symlinks-flag "^1.0.0"
resolve@^2.0.0-next.3:
version "2.0.0-next.4"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660"
integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==
dependencies:
is-core-module "^2.9.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
reusify@^1.0.4: reusify@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
@ -2859,6 +3092,15 @@ run-parallel@^1.1.9:
dependencies: dependencies:
queue-microtask "^1.2.2" queue-microtask "^1.2.2"
safe-regex-test@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295"
integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==
dependencies:
call-bind "^1.0.2"
get-intrinsic "^1.1.3"
is-regex "^1.1.4"
"safer-buffer@>= 2.1.2 < 3": "safer-buffer@>= 2.1.2 < 3":
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
@ -2899,11 +3141,6 @@ scheduler@^0.23.0:
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
select@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==
semver@^6.0.0, semver@^6.3.0: semver@^6.0.0, semver@^6.3.0:
version "6.3.0" version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
@ -2984,6 +3221,38 @@ stack-utils@^2.0.3:
dependencies: dependencies:
escape-string-regexp "^2.0.0" escape-string-regexp "^2.0.0"
string.prototype.matchall@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3"
integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.4"
es-abstract "^1.20.4"
get-intrinsic "^1.1.3"
has-symbols "^1.0.3"
internal-slot "^1.0.3"
regexp.prototype.flags "^1.4.3"
side-channel "^1.0.4"
string.prototype.trimend@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533"
integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.4"
es-abstract "^1.20.4"
string.prototype.trimstart@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4"
integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.4"
es-abstract "^1.20.4"
strip-ansi@^6.0.1: strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@ -3048,11 +3317,6 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
tiny-emitter@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
tinybench@^2.3.1: tinybench@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.3.1.tgz#14f64e6b77d7ef0b1f6ab850c7a808c6760b414d" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.3.1.tgz#14f64e6b77d7ef0b1f6ab850c7a808c6760b414d"
@ -3158,6 +3422,16 @@ uc.micro@^1.0.1:
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
unbox-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==
dependencies:
call-bind "^1.0.2"
has-bigints "^1.0.2"
has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
universalify@^0.2.0: universalify@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"

View File

@ -1,7 +1,5 @@
# Darkwire.io # Darkwire.io
[![CircleCI](https://circleci.com/gh/darkwire/darkwire.io.svg?style=svg)](https://circleci.com/gh/darkwire/darkwire.io)
Simple encrypted web chat. Powered by [socket.io](http://socket.io), the [web cryptography API](https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto). This project is an example of how client side encryption works and how you can integrate it as a chat service. Simple encrypted web chat. Powered by [socket.io](http://socket.io), the [web cryptography API](https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto). This project is an example of how client side encryption works and how you can integrate it as a chat service.
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
@ -86,7 +84,7 @@ Then run it. Example:
$ docker run --init --name darkwire.io --rm -p 3001:3001 darkwire.io $ docker run --init --name darkwire.io --rm -p 3001:3001 darkwire.io
``` ```
You are able to use any of the enviroment variables available in `server/.env.dist` and `client/.env.dist`. The defaults are available in [Dockerfile](Dockerfile) You are able to use any of the environment variables available in `server/.env.dist` and `client/.env.dist`. The defaults are available in [Dockerfile](Dockerfile)
### Security ### Security