Compare commits

..

No commits in common. "047c4549888c8c5ad2be39a8260554a0215520ce" and "08071fba4483fef3901e5a5d8d9023d7e03c6b6e" have entirely different histories.

47 changed files with 1860 additions and 2340 deletions

49
.circleci/config.yml Normal file
View File

@ -0,0 +1,49 @@
# 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

View File

@ -1,35 +0,0 @@
# 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,11 +1,5 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended'
],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
root: true,
@ -13,14 +7,4 @@ module.exports = {
browser: 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,8 +8,7 @@
<meta name="robots" content="index,nofollow" />
<meta name="googlebot" content="index,nofollow" />
<meta name="description" content="darkwire.io is the simplest way to chat with encryption online." />
<title>Darkwire</title>
<link rel="manifest" href="/manifest.json" />
<title>Darkwire.io - instant encrypted web chat</title>
</head>
<body class="h-100">
<div id="root" class="h-100"></div>

9
client/jsconfig.json Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,434 +1,448 @@
/* eslint-disable */
import React from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { COMMIT_SHA } from '@/config/env';
import apiUrlGenerator from '@/api/generator';
import styles from './styles.module.scss';
const About = ({ roomId: roomIdProp }) => {
const [roomId, setRoomId] = React.useState(roomIdProp);
const [abuseReported, setAbuseReported] = React.useState(false);
class About extends Component {
constructor(props) {
super(props);
this.state = {
roomId: props.roomId,
abuseReported: false,
};
}
const handleUpdateRoomId = evt => {
setRoomId(evt.target.value);
};
handleUpdateRoomId(evt) {
this.setState({
roomId: evt.target.value,
});
}
const handleReportAbuse = evt => {
handleReportAbuse(evt) {
evt.preventDefault();
fetch(`${apiUrlGenerator('abuse')}/${roomId}`, {
fetch(`${apiUrlGenerator('abuse')}/${this.state.roomId}`, {
method: 'POST',
});
setAbuseReported(true);
};
this.setState({
abuseReported: true,
});
}
return (
<div className={styles.base}>
<div className={styles.links}>
<div>
<a href="#version">Version</a>
</div>
<div>
<a href="#software">Software</a>
</div>
<div>
<a href="#report-abuse">Report Abuse</a>
</div>
<div>
<a href="#acceptable-use">Acceptable Use Policy</a>
</div>
<div>
<a href="#disclaimer">Disclaimer</a>
</div>
<div>
<a href="#terms">Terms of Service</a>
</div>
<div>
<a href="#contact">Contact</a>
</div>
<div>
<a href="#donate">Donate</a>
</div>
</div>
<section id="version">
<h4>Version</h4>
<p>
Commit SHA:{' '}
<a target="_blank" href={`https://github.com/darkwire/darkwire.io/commit/${COMMIT_SHA}`}>
{COMMIT_SHA}
</a>
</p>
</section>
<section id="software">
<h4>Software</h4>
<p>
This software uses the{' '}
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Crypto" target="_blank" rel="noopener noreferrer">
Web Cryptography API
</a>{' '}
to encrypt data which is transferred using{' '}
<a href="https://en.wikipedia.org/wiki/WebSocket" target="_blank" rel="noopener noreferrer">
secure WebSockets
</a>
. Messages are never stored on a server or sent over the wire in plain-text.
</p>
<p>
We believe in privacy and transparency. &nbsp;
<a href="https://github.com/darkwire/darkwire.io" target="_blank" rel="noopener noreferrer">
View the source code and documentation on GitHub.
</a>
</p>
</section>
<section id="report-abuse">
<h4>Report Abuse</h4>
<p>
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, which
limits our ability to verify the report and take action.
</p>
<p>
When needed, you can take a screenshot of the content and share it, along with any available contact info,
with appropriate law enforcement authorities.
</p>
<p>
To report any content, email us at abuse[at]darkwire.io or submit the room ID below to report anonymously.
</p>
<form onSubmit={handleReportAbuse}>
{abuseReported && <div>Thank you!</div>}
render() {
return (
<div className={styles.base}>
<div className={styles.links}>
<div>
<div className="input-group">
<input
className="form-control"
placeholder="Room ID"
onChange={handleUpdateRoomId}
value={roomId}
type="text"
/>
<div className="input-group-append">
<button className="btn btn-secondary" type="submit">
Submit
</button>
<a href="#version">Version</a>
</div>
<div>
<a href="#software">Software</a>
</div>
<div>
<a href="#report-abuse">Report Abuse</a>
</div>
<div>
<a href="#acceptable-use">Acceptable Use Policy</a>
</div>
<div>
<a href="#disclaimer">Disclaimer</a>
</div>
<div>
<a href="#terms">Terms of Service</a>
</div>
<div>
<a href="#contact">Contact</a>
</div>
<div>
<a href="#donate">Donate</a>
</div>
</div>
<section id="version">
<h4>Version</h4>
<p>
Commit SHA:{' '}
<a target="_blank" href={`https://github.com/darkwire/darkwire.io/commit/${COMMIT_SHA}`}>
{COMMIT_SHA}
</a>
</p>
</section>
<section id="software">
<h4>Software</h4>
<p>
This software uses the{' '}
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Crypto" target="_blank" rel="noopener noreferrer">
Web Cryptography API
</a>{' '}
to encrypt data which is transferred using{' '}
<a href="https://en.wikipedia.org/wiki/WebSocket" target="_blank" rel="noopener noreferrer">
secure WebSockets
</a>
. Messages are never stored on a server or sent over the wire in plain-text.
</p>
<p>
We believe in privacy and transparency. &nbsp;
<a href="https://github.com/darkwire/darkwire.io" target="_blank" rel="noopener noreferrer">
View the source code and documentation on GitHub.
</a>
</p>
</section>
<section id="report-abuse">
<h4>Report Abuse</h4>
<p>
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,
which limits our ability to verify the report and take action.
</p>
<p>
When needed, you can take a screenshot of the content and share it, along with any available contact info,
with appropriate law enforcement authorities.
</p>
<p>
To report any content, email us at abuse[at]darkwire.io or submit the room ID below to report anonymously.
</p>
<form onSubmit={this.handleReportAbuse.bind(this)}>
{this.state.abuseReported && <div>Thank you!</div>}
<div>
<div className="input-group">
<input
className="form-control"
placeholder="Room ID"
onChange={this.handleUpdateRoomId.bind(this)}
value={this.state.roomId}
type="text"
/>
<div className="input-group-append">
<button className="btn btn-secondary" type="submit">
Submit
</button>
</div>
</div>
</div>
</div>
</form>
<br />
<p>If you feel you or anyone else is in immediate danger, please contact your local emergency services.</p>
<p>
If you receive content from someone who wishes to hurt themselves, and you're concerned for their safety,
please contact your local emergency services or a{' '}
<a href="https://faq.whatsapp.com/en/general/28030010">suicide prevention hotline</a>.
</p>
<p>
If you receive or encounter content indicating abuse or exploitation of a child, please contact the{' '}
<a href="http://www.missingkids.com">National Center for Missing and Exploited Children (NCMEC)</a>.
</p>
</section>
</form>
<br />
<p>If you feel you or anyone else is in immediate danger, please contact your local emergency services.</p>
<p>
If you receive content from someone who wishes to hurt themselves, and you're concerned for their safety,
please contact your local emergency services or a{' '}
<a href="https://faq.whatsapp.com/en/general/28030010">suicide prevention hotline</a>.
</p>
<p>
If you receive or encounter content indicating abuse or exploitation of a child, please contact the{' '}
<a href="http://www.missingkids.com">National Center for Missing and Exploited Children (NCMEC)</a>.
</p>
</section>
<section id="acceptable-use">
<h4>Acceptable Use Policy</h4>
<p>
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). The
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
latest version of this Policy. If you violate the Policy or authorize or help others to do so, we may suspend
or terminate your use of the Services.
</p>
<strong>No Illegal, Harmful, or Offensive Use or Content</strong>
<p>
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, distribute
or otherwise make available content that is illegal, harmful, fraudulent, infringing or offensive. Prohibited
activities or content include:
</p>
<ul>
<li>
<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
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 pharming.
</li>
<section id="acceptable-use">
<h4>Acceptable Use Policy</h4>
<p>
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).
The 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
latest version of this Policy. If you violate the Policy or authorize or help others to do so, we may
suspend or terminate your use of the Services.
</p>
<strong>No Illegal, Harmful, or Offensive Use or Content</strong>
<p>
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,
distribute or otherwise make available content that is illegal, harmful, fraudulent, infringing or
offensive. Prohibited activities or content include:
</p>
<ul>
<li>
<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
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
pharming.
</li>
<li>
<strong>Infringing Content.</strong> Content that infringes or misappropriates the intellectual property or
proprietary rights of others.
</li>
<li>
<strong>Infringing Content.</strong> Content that infringes or misappropriates the intellectual property
or proprietary rights of others.
</li>
<li>
<strong>Offensive Content.</strong> Content that is defamatory, obscene, abusive, invasive of privacy, or
otherwise objectionable, including content that constitutes child pornography, relates to bestiality, or
depicts non-consensual sex acts.
</li>
<li>
<strong>Offensive Content.</strong> Content that is defamatory, obscene, abusive, invasive of privacy, or
otherwise objectionable, including content that constitutes child pornography, relates to bestiality, or
depicts non-consensual sex acts.
</li>
<li>
<strong>Harmful Content.</strong> Content or other computer technology that may damage, interfere with,
surreptitiously intercept, or expropriate any system, program, or data, including viruses, Trojan horses,
worms, time bombs, or cancelbots.
</li>
</ul>
<strong>No Security Violations</strong>
<br />
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 include:
<ul>
<li>
<strong>Unauthorized Access.</strong> Accessing or using any System without permission, including attempting
to probe, scan, or test the vulnerability of a System or to breach any security or authentication measures
used by a System.
</li>
<li>
<strong>Harmful Content.</strong> Content or other computer technology that may damage, interfere with,
surreptitiously intercept, or expropriate any system, program, or data, including viruses, Trojan horses,
worms, time bombs, or cancelbots.
</li>
</ul>
<strong>No Security Violations</strong>
<br />
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
include:
<ul>
<li>
<strong>Unauthorized Access.</strong> Accessing or using any System without permission, including
attempting to probe, scan, or test the vulnerability of a System or to breach any security or
authentication measures used by a System.
</li>
<li>
<strong>Interception.</strong> Monitoring of data or traffic on a System without permission.
</li>
<li>
<strong>Interception.</strong> Monitoring of data or traffic on a System without permission.
</li>
<li>
<strong>Falsification of Origin.</strong> Forging TCP-IP packet headers, e-mail headers, or any part of a
message describing its origin or route. The legitimate use of aliases and anonymous remailers is not
prohibited by this provision.
</li>
</ul>
<strong>No Network Abuse</strong>
<br />
You may not make network connections to any users, hosts, or networks unless you have permission to communicate
with them. Prohibited activities include:
<ul>
<li>
<strong>Monitoring or Crawling.</strong> Monitoring or crawling of a System that impairs or disrupts the
System being monitored or crawled.
</li>
<li>
<strong>Falsification of Origin.</strong> Forging TCP-IP packet headers, e-mail headers, or any part of a
message describing its origin or route. The legitimate use of aliases and anonymous remailers is not
prohibited by this provision.
</li>
</ul>
<strong>No Network Abuse</strong>
<br />
You may not make network connections to any users, hosts, or networks unless you have permission to
communicate with them. Prohibited activities include:
<ul>
<li>
<strong>Monitoring or Crawling.</strong> Monitoring or crawling of a System that impairs or disrupts the
System being monitored or crawled.
</li>
<li>
<strong>Denial of Service (DoS).</strong> Inundating a target with communications requests so the target
either cannot respond to legitimate traffic or responds so slowly that it becomes ineffective.
</li>
<li>
<strong>Denial of Service (DoS).</strong> Inundating a target with communications requests so the target
either cannot respond to legitimate traffic or responds so slowly that it becomes ineffective.
</li>
<li>
<strong>Intentional Interference.</strong> Interfering with the proper functioning of any System, including
any deliberate attempt to overload a system by mail bombing, news bombing, broadcast attacks, or flooding
techniques.
</li>
<li>
<strong>Intentional Interference.</strong> Interfering with the proper functioning of any System,
including any deliberate attempt to overload a system by mail bombing, news bombing, broadcast attacks, or
flooding techniques.
</li>
<li>
<strong>Operation of Certain Network Services.</strong> Operating network services like open proxies, open
mail relays, or open recursive domain name servers.
</li>
<li>
<strong>Operation of Certain Network Services.</strong> Operating network services like open proxies, open
mail relays, or open recursive domain name servers.
</li>
<li>
<strong>Avoiding System Restrictions.</strong> Using manual or electronic means to avoid any use limitations
placed on a System, such as access and storage restrictions.
</li>
</ul>
<strong>No E-Mail or Other Message Abuse</strong>
<br />
You will not distribute, publish, send, or facilitate the sending of unsolicited mass e-mail or other messages,
promotions, advertising, or solicitations (like spam), including commercial advertising and informational
announcements. You will not alter or obscure mail headers or assume a senders identity without the senders
explicit permission. You will not collect replies to messages sent from another internet service provider if
those messages violate this Policy or the acceptable use policy of that provider.
<strong>Our Monitoring and Enforcement</strong>
<br />
We reserve the right, but do not assume the obligation, to investigate any violation of this Policy or misuse of
the Services or Darkwire Site. We may:
<ul>
<li>investigate violations of this Policy or misuse of the Services or Darkwire Site; or</li>
<li>
remove, disable access to, or modify any content or resource that violates this Policy or any other
agreement we have with you for use of the Services or the Darkwire Site.
</li>
<li>
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 appropriate
customer information. We also may cooperate with appropriate law enforcement agencies, regulators, or other
appropriate third parties to help with the investigation and prosecution of illegal conduct by providing
network and systems information related to alleged violations of this Policy.
</li>
</ul>
Reporting of Violations of this Policy
<br />
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 follow
our abuse reporting process.
</section>
<li>
<strong>Avoiding System Restrictions.</strong> Using manual or electronic means to avoid any use
limitations placed on a System, such as access and storage restrictions.
</li>
</ul>
<strong>No E-Mail or Other Message Abuse</strong>
<br />
You will not distribute, publish, send, or facilitate the sending of unsolicited mass e-mail or other
messages, promotions, advertising, or solicitations (like spam), including commercial advertising and
informational announcements. You will not alter or obscure mail headers or assume a senders identity without
the senders explicit permission. You will not collect replies to messages sent from another internet service
provider if those messages violate this Policy or the acceptable use policy of that provider.
<strong>Our Monitoring and Enforcement</strong>
<br />
We reserve the right, but do not assume the obligation, to investigate any violation of this Policy or misuse
of the Services or Darkwire Site. We may:
<ul>
<li>investigate violations of this Policy or misuse of the Services or Darkwire Site; or</li>
<li>
remove, disable access to, or modify any content or resource that violates this Policy or any other
agreement we have with you for use of the Services or the Darkwire Site.
</li>
<li>
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
appropriate customer information. We also may cooperate with appropriate law enforcement agencies,
regulators, or other appropriate third parties to help with the investigation and prosecution of illegal
conduct by providing network and systems information related to alleged violations of this Policy.
</li>
</ul>
Reporting of Violations of this Policy
<br />
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
follow our abuse reporting process.
</section>
<section id="terms">
<h4>Terms of Service ("Terms")</h4>
<p>Last updated: December 11, 2017</p>
<p>
Please read these Terms of Service ("Terms", "Terms of Service") carefully before using the
https://darkwire.io website (the "Service") operated by Darkwire ("us", "we", or "our").
</p>
<p>
Your access to and use of the Service is conditioned on your acceptance of and compliance with these Terms.
These Terms apply to all visitors, users and others who access or use the Service.
</p>
<p>
By accessing or using the Service you agree to be bound by these Terms. If you disagree with any part of the
terms then you may not access the Service.
</p>
<strong>Links To Other Web Sites</strong>
<p>
Our Service may contain links to third-party web sites or services that are not owned or controlled by
Darkwire.
</p>
<p>
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
responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in
connection with use of or reliance on any such content, goods or services available on or through any such web
sites or services.
</p>
<p>
We strongly advise you to read the terms and conditions and privacy policies of any third-party web sites or
services that you visit.
</p>
<strong>Termination</strong>
<p>
We may terminate or suspend access to our Service immediately, without prior notice or liability, for any
reason whatsoever, including without limitation if you breach the Terms.
</p>
<p>
All provisions of the Terms which by their nature should survive termination shall survive termination,
including, without limitation, ownership provisions, warranty disclaimers, indemnity and limitations of
liability.
</p>
<strong>Governing Law</strong>
<p>
These Terms shall be governed and construed in accordance with the laws of New York, United States, without
regard to its conflict of law provisions.
</p>
<p>
Our failure to enforce any right or provision of these Terms will not be considered a waiver of those rights.
If any provision of these Terms is held to be invalid or unenforceable by a court, the remaining provisions of
these Terms will remain in effect. These Terms constitute the entire agreement between us regarding our
Service, and supersede and replace any prior agreements we might have between us regarding the Service.
</p>
</section>
<section id="terms">
<h4>Terms of Service ("Terms")</h4>
<p>Last updated: December 11, 2017</p>
<p>
Please read these Terms of Service ("Terms", "Terms of Service") carefully before using the
https://darkwire.io website (the "Service") operated by Darkwire ("us", "we", or "our").
</p>
<p>
Your access to and use of the Service is conditioned on your acceptance of and compliance with these Terms.
These Terms apply to all visitors, users and others who access or use the Service.
</p>
<p>
By accessing or using the Service you agree to be bound by these Terms. If you disagree with any part of the
terms then you may not access the Service.
</p>
<strong>Links To Other Web Sites</strong>
<p>
Our Service may contain links to third-party web sites or services that are not owned or controlled by
Darkwire.
</p>
<p>
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
responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or
in connection with use of or reliance on any such content, goods or services available on or through any
such web sites or services.
</p>
<p>
We strongly advise you to read the terms and conditions and privacy policies of any third-party web sites or
services that you visit.
</p>
<strong>Termination</strong>
<p>
We may terminate or suspend access to our Service immediately, without prior notice or liability, for any
reason whatsoever, including without limitation if you breach the Terms.
</p>
<p>
All provisions of the Terms which by their nature should survive termination shall survive termination,
including, without limitation, ownership provisions, warranty disclaimers, indemnity and limitations of
liability.
</p>
<strong>Governing Law</strong>
<p>
These Terms shall be governed and construed in accordance with the laws of New York, United States, without
regard to its conflict of law provisions.
</p>
<p>
Our failure to enforce any right or provision of these Terms will not be considered a waiver of those
rights. If any provision of these Terms is held to be invalid or unenforceable by a court, the remaining
provisions of these Terms will remain in effect. These Terms constitute the entire agreement between us
regarding our Service, and supersede and replace any prior agreements we might have between us regarding the
Service.
</p>
</section>
<section id="disclaimer">
<h4>Disclaimer</h4>
<p className="bold">
WARNING: Darkwire does not mask IP addresses nor can verify the integrity of parties recieving messages.
&nbsp;Proceed with caution and always confirm recipients beforre starting a chat session.
</p>
<p>
Please also note that <strong>ALL CHATROOMS</strong> are public. &nbsp;Anyone can guess your room URL. If you
need a more-private room, use the lock feature or set the URL manually by entering a room ID after
&quot;darkwire.io/&quot;.
</p>
<br />
<strong>No Warranties; Exclusion of Liability; Indemnification</strong>
<p>
<strong>
OUR WEBSITE IS OPERATED BY Darkwire ON AN "AS IS," "AS AVAILABLE" BASIS, WITHOUT REPRESENTATIONS OR
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,
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 OMISSIONS
IN THE CONTENT OF OUR WEBSITE, FOR CONTRACTS OR SERVICES SOLD THROUGH OUR WEBSITE, FOR YOUR ACTION OR
INACTION IN CONNECTION WITH OUR WEBSITE OR FOR ANY DAMAGE TO YOUR COMPUTER OR DATA OR ANY OTHER DAMAGE YOU
MAY INCUR IN CONNECTION WITH OUR WEBSITE. YOUR USE OF OUR WEBSITE AND ANY CONTRACTS OR SERVICES ARE AT YOUR
OWN RISK. IN NO EVENT SHALL EITHER Darkwire OR THEIR AGENTS BE LIABLE FOR ANY DIRECT, INDIRECT, PUNITIVE,
INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR IN ANY WAY CONNECTED WITH THE USE OF OUR
WEBSITE, CONTRACTS AND SERVICES PURCHASED THROUGH OUR WEBSITE, THE DELAY OR INABILITY TO USE OUR WEBSITE OR
OTHERWISE ARISING IN CONNECTION WITH OUR WEBSITE, CONTRACTS OR RELATED SERVICES, WHETHER BASED ON CONTRACT,
TORT, STRICT LIABILITY OR OTHERWISE, EVEN IF ADVISED OF THE POSSIBILITY OF ANY SUCH DAMAGES. IN NO EVENT
SHALL Darkwires LIABILITY FOR ANY DAMAGE CLAIM EXCEED THE AMOUNT PAID BY YOU TO Darkwire FOR THE
TRANSACTION GIVING RISE TO SUCH DAMAGE CLAIM.
</strong>
</p>
<p>
<strong>
SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THE
ABOVE EXCLUSION MAY NOT APPLY TO YOU.
</strong>
</p>
<p>
<strong>
WITHOUT LIMITING THE FOREGOING, Darkwire DO NOT REPRESENT OR WARRANT THAT THE INFORMATION ON THE WEBITE IS
ACCURATE, COMPLETE, RELIABLE, USEFUL, TIMELY OR CURRENT OR THAT OUR WEBSITE WILL OPERATE WITHOUT
INTERRUPTION OR ERROR.
</strong>
</p>
<p>
<strong>
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 REPRESENTATION
OR GUARANTEES ABOUT ANY CONTRACTS AND SERVICES OFFERED THROUGH OUR WEBSITE.
</strong>
</p>
<p>
<strong>
Darkwire MAKES NO REPRESENTATION THAT CONTENT PROVIDED ON OUR WEBSITE, CONTRACTS, OR RELATED SERVICES ARE
APPLICABLE OR APPROPRIATE FOR USE IN ALL JURISDICTIONS.
</strong>
</p>
<strong>Indemnification</strong>
<p>
You agree to defend, indemnify and hold Darkwire harmless from and against any and all claims, damages, costs
and expenses, including attorneys' fees, arising from or related to your use of our Website or any Contracts
or Services you purchase through it.
</p>
<strong>Changes</strong>
<p>
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 constitutes
a material change will be determined at our sole discretion.
</p>
<p>
By continuing to access or use our Service after those revisions become effective, you agree to be bound by
the revised terms. If you do not agree to the new terms, please stop using the Service.
</p>
<strong>Contact Us</strong>
<p>If you have any questions about these Terms, please contact us at hello[at]darkwire.io.</p>
</section>
<section id="disclaimer">
<h4>Disclaimer</h4>
<p className="bold">
WARNING: Darkwire does not mask IP addresses nor can verify the integrity of parties recieving messages.
&nbsp;Proceed with caution and always confirm recipients beforre starting a chat session.
</p>
<p>
Please also note that <strong>ALL CHATROOMS</strong> are public. &nbsp;Anyone can guess your room URL. If
you need a more-private room, use the lock feature or set the URL manually by entering a room ID after
&quot;darkwire.io/&quot;.
</p>
<br />
<strong>No Warranties; Exclusion of Liability; Indemnification</strong>
<p>
<strong>
OUR WEBSITE IS OPERATED BY Darkwire ON AN "AS IS," "AS AVAILABLE" BASIS, WITHOUT REPRESENTATIONS OR
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,
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
OMISSIONS IN THE CONTENT OF OUR WEBSITE, FOR CONTRACTS OR SERVICES SOLD THROUGH OUR WEBSITE, FOR YOUR
ACTION OR INACTION IN CONNECTION WITH OUR WEBSITE OR FOR ANY DAMAGE TO YOUR COMPUTER OR DATA OR ANY OTHER
DAMAGE YOU MAY INCUR IN CONNECTION WITH OUR WEBSITE. YOUR USE OF OUR WEBSITE AND ANY CONTRACTS OR SERVICES
ARE AT YOUR OWN RISK. IN NO EVENT SHALL EITHER Darkwire OR THEIR AGENTS BE LIABLE FOR ANY DIRECT,
INDIRECT, PUNITIVE, INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR IN ANY WAY CONNECTED
WITH THE USE OF OUR WEBSITE, CONTRACTS AND SERVICES PURCHASED THROUGH OUR WEBSITE, THE DELAY OR INABILITY
TO USE OUR WEBSITE OR OTHERWISE ARISING IN CONNECTION WITH OUR WEBSITE, CONTRACTS OR RELATED SERVICES,
WHETHER BASED ON CONTRACT, TORT, STRICT LIABILITY OR OTHERWISE, EVEN IF ADVISED OF THE POSSIBILITY OF ANY
SUCH DAMAGES. IN NO EVENT SHALL Darkwires LIABILITY FOR ANY DAMAGE CLAIM EXCEED THE AMOUNT PAID BY YOU TO
Darkwire FOR THE TRANSACTION GIVING RISE TO SUCH DAMAGE CLAIM.
</strong>
</p>
<p>
<strong>
SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THE
ABOVE EXCLUSION MAY NOT APPLY TO YOU.
</strong>
</p>
<p>
<strong>
WITHOUT LIMITING THE FOREGOING, Darkwire DO NOT REPRESENT OR WARRANT THAT THE INFORMATION ON THE WEBITE IS
ACCURATE, COMPLETE, RELIABLE, USEFUL, TIMELY OR CURRENT OR THAT OUR WEBSITE WILL OPERATE WITHOUT
INTERRUPTION OR ERROR.
</strong>
</p>
<p>
<strong>
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
REPRESENTATION OR GUARANTEES ABOUT ANY CONTRACTS AND SERVICES OFFERED THROUGH OUR WEBSITE.
</strong>
</p>
<p>
<strong>
Darkwire MAKES NO REPRESENTATION THAT CONTENT PROVIDED ON OUR WEBSITE, CONTRACTS, OR RELATED SERVICES ARE
APPLICABLE OR APPROPRIATE FOR USE IN ALL JURISDICTIONS.
</strong>
</p>
<strong>Indemnification</strong>
<p>
You agree to defend, indemnify and hold Darkwire harmless from and against any and all claims, damages,
costs and expenses, including attorneys' fees, arising from or related to your use of our Website or any
Contracts or Services you purchase through it.
</p>
<strong>Changes</strong>
<p>
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
constitutes a material change will be determined at our sole discretion.
</p>
<p>
By continuing to access or use our Service after those revisions become effective, you agree to be bound by
the revised terms. If you do not agree to the new terms, please stop using the Service.
</p>
<strong>Contact Us</strong>
<p>If you have any questions about these Terms, please contact us at hello[at]darkwire.io.</p>
</section>
<section id="contact">
<h4>Contact</h4>
<p>Questions/comments? Email us at hello[at]darkwire.io</p>
<p>
Found a bug or want a new feature?{' '}
<a href="https://github.com/darkwire/darkwire.io/issues" target="_blank" rel="noopener noreferrer">
Open a ticket on Github
</a>
.
</p>
</section>
<section id="contact">
<h4>Contact</h4>
<p>Questions/comments? Email us at hello[at]darkwire.io</p>
<p>
Found a bug or want a new feature?{' '}
<a href="https://github.com/darkwire/darkwire.io/issues" target="_blank" rel="noopener noreferrer">
Open a ticket on Github
</a>
.
</p>
</section>
<section id="donate">
<h4>Donate</h4>
<p>
Darkwire is maintained and hosted by two developers with full-time jobs. If you get some value from this
service we would appreciate any donation you can afford. We use these funds for server and DNS costs. Thank
you!
</p>
<strong>Bitcoin</strong>
<p>189sPnHGcjP5uteg2UuNgcJ5eoaRAP4Bw4</p>
<strong>Ethereum</strong>
<p>0x36dc407bB28aA1EE6AafBee0379Fe6Cff881758E</p>
<strong>Litecoin</strong>
<p>LUViQeSggBBtYoN2qNtXSuxYoRMzRY8CSX</p>
<strong>PayPal:</strong>
<br />
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_blank">
<input type="hidden" name="cmd" value="_s-xclick" />
<input type="hidden" name="hosted_button_id" value="UAH5BCLA9Y8VW" />
<input
type="image"
src="https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif"
border="0"
name="submit"
alt="PayPal - The safer, easier way to pay online!"
/>
<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1" />
</form>
</section>
</div>
);
};
<section id="donate">
<h4>Donate</h4>
<p>
Darkwire is maintained and hosted by two developers with full-time jobs. If you get some value from this
service we would appreciate any donation you can afford. We use these funds for server and DNS costs. Thank
you!
</p>
<strong>Bitcoin</strong>
<p>189sPnHGcjP5uteg2UuNgcJ5eoaRAP4Bw4</p>
<strong>Ethereum</strong>
<p>0x36dc407bB28aA1EE6AafBee0379Fe6Cff881758E</p>
<strong>Litecoin</strong>
<p>LUViQeSggBBtYoN2qNtXSuxYoRMzRY8CSX</p>
<strong>PayPal:</strong>
<br />
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_blank">
<input type="hidden" name="cmd" value="_s-xclick" />
<input type="hidden" name="hosted_button_id" value="UAH5BCLA9Y8VW" />
<input
type="image"
src="https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif"
border="0"
name="submit"
alt="PayPal - The safer, easier way to pay online!"
/>
<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1" />
</form>
</section>
</div>
);
}
}
About.propTypes = {
roomId: PropTypes.string.isRequired,

View File

@ -1,134 +1,171 @@
import React from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import sanitizeHtml from 'sanitize-html';
import { CornerDownRight } from 'react-feather';
import { hasTouchSupport } from '@/utils/dom';
import { getSelectedText, hasTouchSupport } from '@/utils/dom';
import FileTransfer from '@/components/FileTransfer';
export const Chat = ({ sendEncryptedMessage, showNotice, userId, username, clearActivities, translations }) => {
const [message, setMessage] = React.useState('');
const [shiftKeyDown, setShiftKeyDown] = React.useState(false);
const textInputRef = React.useRef();
export class Chat extends Component {
constructor(props) {
super(props);
this.state = {
message: '',
touchSupport: hasTouchSupport,
shiftKeyDown: false,
};
const touchSupport = hasTouchSupport;
this.commands = [
{
command: 'nick',
description: 'Changes nickname.',
parameters: ['{username}'],
usage: '/nick {username}',
scope: 'global',
action: params => {
// eslint-disable-line
let newUsername = params.join(' ') || ''; // eslint-disable-line
const canSend = message.trim().length;
// Remove things that aren't digits or chars
newUsername = newUsername.replace(/[^A-Za-z0-9]/g, '-');
const commands = [
{
command: 'nick',
description: 'Changes nickname.',
parameters: ['{username}'],
usage: '/nick {username}',
scope: 'global',
action: params => {
// eslint-disable-line
let newUsername = params.join(' ') || ''; // eslint-disable-line
const errors = [];
// Remove things that aren't digits or chars
newUsername = newUsername.replace(/[^A-Za-z0-9]/g, '-');
if (!newUsername.trim().length) {
errors.push('Username cannot be blank');
}
const errors = [];
if (newUsername.toString().length > 16) {
errors.push('Username cannot be greater than 16 characters');
}
if (!newUsername.trim().length) {
errors.push('Username cannot be blank');
}
if (!newUsername.match(/^[A-Z]/i)) {
errors.push('Username must start with a letter');
}
if (newUsername.toString().length > 16) {
errors.push('Username cannot be greater than 16 characters');
}
if (errors.length) {
return this.props.showNotice({
message: `${errors.join(', ')}`,
level: 'error',
});
}
if (!newUsername.match(/^[A-Z]/i)) {
errors.push('Username must start with a letter');
}
if (errors.length) {
return showNotice({
message: `${errors.join(', ')}`,
level: 'error',
this.props.sendEncryptedMessage({
type: 'CHANGE_USERNAME',
payload: {
id: this.props.userId,
newUsername,
currentUsername: this.props.username,
},
});
}
},
},
{
command: 'help',
description: 'Shows a list of commands.',
paramaters: [],
usage: '/help',
scope: 'local',
action: params => {
// eslint-disable-line
const validCommands = this.commands.map(command => `/${command.command}`);
this.props.showNotice({
message: `Valid commands: ${validCommands.sort().join(', ')}`,
level: 'info',
});
},
},
{
command: 'me',
description: 'Invoke virtual action',
paramaters: ['{action}'],
usage: '/me {action}',
scope: 'global',
action: params => {
// eslint-disable-line
const actionMessage = params.join(' ');
if (!actionMessage.trim().length) {
return false;
}
sendEncryptedMessage({
type: 'CHANGE_USERNAME',
payload: {
id: userId,
newUsername,
currentUsername: username,
},
});
this.props.sendEncryptedMessage({
type: 'USER_ACTION',
payload: {
action: actionMessage,
},
});
},
},
},
{
command: 'help',
description: 'Shows a list of commands.',
parameters: [],
usage: '/help',
scope: 'local',
action: () => {
const validCommands = commands.map(command => `/${command.command}`);
showNotice({
message: `Valid commands: ${validCommands.sort().join(', ')}`,
level: 'info',
});
{
command: 'clear',
description: 'Clears the chat screen',
paramaters: [],
usage: '/clear',
scope: 'local',
action: (params = null) => {
// eslint-disable-line
this.props.clearActivities();
},
},
},
{
command: 'me',
description: 'Invoke virtual action',
parameters: ['{action}'],
usage: '/me {action}',
scope: 'global',
action: params => {
const actionMessage = params.join(' ');
if (!actionMessage.trim().length) {
return false;
}
];
}
sendEncryptedMessage({
type: 'USER_ACTION',
payload: {
action: actionMessage,
},
});
},
},
{
command: 'clear',
description: 'Clears the chat screen',
parameters: [],
usage: '/clear',
scope: 'local',
action: () => {
clearActivities();
},
},
];
const handleKeyUp = e => {
if (e.key === 'Shift') {
setShiftKeyDown(false);
componentDidMount() {
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();
});
}
};
}
const handleKeyPress = e => {
if (e.key === 'Shift') {
setShiftKeyDown(true);
}
if (e.key === 'Enter' && !hasTouchSupport && !shiftKeyDown) {
e.preventDefault();
if (canSend) {
sendMessage();
} else {
setMessage('');
componentWillReceiveProps(nextProps) {
if (nextProps.focusChat) {
if (!getSelectedText()) {
// Don't focus for now, evaluate UX benfits
// this.textInput.focus()
}
}
};
}
const executeCommand = command => {
const commandToExecute = commands.find(cmnd => cmnd.command === command.command);
componentDidUpdate(nextProps, nextState) {
if (!nextState.message.trim().length) {
// autosize.update(this.textInput)
}
}
handleKeyUp(e) {
if (e.key === 'Shift') {
this.setState({
shiftKeyDown: false,
});
}
}
handleKeyPress(e) {
if (e.key === 'Shift') {
this.setState({
shiftKeyDown: true,
});
}
// Fix when autosize is enabled - line breaks require shift+enter twice
if (e.key === 'Enter' && !hasTouchSupport && !this.state.shiftKeyDown) {
e.preventDefault();
if (this.canSend()) {
this.sendMessage();
} else {
this.setState({
message: '',
});
}
}
}
executeCommand(command) {
const commandToExecute = this.commands.find(cmnd => cmnd.command === command.command);
if (commandToExecute) {
const { params } = command;
@ -138,20 +175,19 @@ export const Chat = ({ sendEncryptedMessage, showNotice, userId, username, clear
}
return null;
};
}
const handleSendClick = evt => {
handleSendClick() {
this.sendMessage.bind(this);
this.textInput.focus();
}
handleFormSubmit(evt) {
evt.preventDefault();
sendMessage();
textInputRef.current.focus();
};
this.sendMessage();
}
const handleFormSubmit = evt => {
evt.preventDefault();
sendMessage();
};
const parseCommand = message => {
parseCommand(message) {
const commandTrigger = {
command: null,
params: [],
@ -171,22 +207,23 @@ export const Chat = ({ sendEncryptedMessage, showNotice, userId, username, clear
}
return false;
};
}
const sendMessage = () => {
if (!canSend) {
sendMessage() {
if (!this.canSend()) {
return;
}
const isCommand = parseCommand(message);
const { message } = this.state;
const isCommand = this.parseCommand(message);
if (isCommand) {
const res = executeCommand(isCommand);
const res = this.executeCommand(isCommand);
if (res === false) {
return;
}
} else {
sendEncryptedMessage({
this.props.sendEncryptedMessage({
type: 'TEXT_MESSAGE',
payload: {
text: message,
@ -195,41 +232,55 @@ export const Chat = ({ sendEncryptedMessage, showNotice, userId, username, clear
});
}
setMessage('');
};
this.setState({
message: '',
});
}
const handleInputChange = evt => {
setMessage(evt.target.value);
};
handleInputChange(evt) {
this.setState({
message: evt.target.value,
});
}
return (
<form onSubmit={handleFormSubmit} className="chat-preflight-container">
<textarea
rows="1"
onKeyUp={handleKeyUp}
onKeyDown={handleKeyPress}
ref={textInputRef}
autoFocus
className="chat"
value={message}
placeholder={translations.typePlaceholder}
onChange={handleInputChange}
/>
<div className="input-controls">
<FileTransfer sendEncryptedMessage={sendEncryptedMessage} />
{touchSupport && (
<button
onClick={handleSendClick}
className={`icon is-right send btn btn-link ${canSend ? 'active' : ''}`}
title="Send"
>
<CornerDownRight className={canSend ? '' : 'disabled'} />
</button>
)}
</div>
</form>
);
};
canSend() {
return this.state.message.trim().length;
}
render() {
const touchSupport = this.state.touchSupport;
return (
<form onSubmit={this.handleFormSubmit.bind(this)} className="chat-preflight-container">
<textarea
rows="1"
onKeyUp={this.handleKeyUp.bind(this)}
onKeyDown={this.handleKeyPress.bind(this)}
ref={input => {
this.textInput = input;
}}
autoFocus
className="chat"
value={this.state.message}
placeholder={this.props.translations.typePlaceholder}
onChange={this.handleInputChange.bind(this)}
/>
<div className="input-controls">
<FileTransfer sendEncryptedMessage={this.props.sendEncryptedMessage} />
{touchSupport && (
<button
onClick={this.handleSendClick.bind(this)}
className={`icon is-right send btn btn-link ${this.canSend() ? 'active' : ''}`}
title="Send"
>
<CornerDownRight className={this.canSend() ? '' : 'disabled'} />
</button>
)}
</div>
</form>
);
}
}
Chat.propTypes = {
sendEncryptedMessage: PropTypes.func.isRequired,

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,9 @@
import React from 'react';
import ReactModal from 'react-modal';
import React, { Component } from 'react';
import Modal from 'react-modal';
import PropTypes from 'prop-types';
import { nanoid } from 'nanoid';
import { X, AlertCircle } from 'react-feather';
import classNames from 'classnames';
import { useSelector, useDispatch } from 'react-redux';
import { createUser } from '@/reducers/user';
import Crypto from '@/utils/crypto';
import { connect as connectSocket } from '@/utils/socket';
@ -23,26 +21,68 @@ import styles from './styles.module.scss';
const crypto = new Crypto();
ReactModal.setAppElement('#root');
Modal.setAppElement('#root');
const Modal = ({
closeModal,
modalComponent,
roomId,
translations,
toggleSoundEnabled,
togglePersistenceEnabled,
soundIsEnabled,
persistenceIsEnabled,
toggleNotificationEnabled,
toggleNotificationAllowed,
notificationIsEnabled,
notificationIsAllowed,
setLanguage,
language,
}) => {
const getModal = () => {
switch (modalComponent) {
class Home extends Component {
async componentWillMount() {
const user = await this.createUser();
const socket = connectSocket(this.props.socketId);
this.socket = socket;
socket.on('disconnect', () => {
this.props.toggleSocketConnected(false);
});
socket.on('connect', () => {
this.initApp(user);
this.props.toggleSocketConnected(true);
});
socket.on('USER_ENTER', payload => {
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':
return {
component: <Connecting />,
@ -51,38 +91,40 @@ const Modal = ({
};
case 'About':
return {
component: <About roomId={roomId} />,
title: translations.aboutHeader,
component: <About roomId={this.props.roomId} />,
title: this.props.translations.aboutHeader,
};
case 'Settings':
return {
component: (
<Settings
roomId={roomId}
toggleSoundEnabled={toggleSoundEnabled}
togglePersistenceEnabled={togglePersistenceEnabled}
soundIsEnabled={soundIsEnabled}
persistenceIsEnabled={persistenceIsEnabled}
toggleNotificationEnabled={toggleNotificationEnabled}
toggleNotificationAllowed={toggleNotificationAllowed}
notificationIsEnabled={notificationIsEnabled}
notificationIsAllowed={notificationIsAllowed}
setLanguage={setLanguage}
language={language}
translations={translations}
roomId={this.props.roomId}
toggleSoundEnabled={this.props.toggleSoundEnabled}
togglePersistenceEnabled={this.props.togglePersistenceEnabled}
soundIsEnabled={this.props.soundIsEnabled}
persistenceIsEnabled={this.props.persistenceIsEnabled}
toggleNotificationEnabled={this.props.toggleNotificationEnabled}
toggleNotificationAllowed={this.props.toggleNotificationAllowed}
notificationIsEnabled={this.props.notificationIsEnabled}
notificationIsAllowed={this.props.notificationIsAllowed}
setLanguage={this.props.setLanguage}
language={this.props.language}
translations={this.props.translations}
/>
),
title: translations.settingsHeader,
title: this.props.translations.settingsHeader,
};
case 'Welcome':
return {
component: <Welcome roomId={roomId} close={closeModal} translations={translations} />,
title: translations.welcomeHeader,
component: (
<Welcome roomId={this.props.roomId} close={this.props.closeModal} translations={this.props.translations} />
),
title: this.props.translations.welcomeHeader,
};
case 'Room Locked':
return {
component: <RoomLocked modalContent={translations.lockedRoomHeader} />,
title: translations.lockedRoomHeader,
component: <RoomLocked modalContent={this.props.translations.lockedRoomHeader} />,
title: this.props.translations.lockedRoomHeader,
preventClose: true,
};
default:
@ -91,268 +133,112 @@ const Modal = ({
title: null,
};
}
};
}
const modalOpts = getModal();
return (
<ReactModal
isOpen={Boolean(modalComponent)}
contentLabel="Modal"
style={{ overlay: { zIndex: 10 } }}
className={{
base: 'react-modal-content',
afterOpen: 'react-modal-content_after-open',
beforeClose: 'react-modal-content_before-close',
}}
overlayClassName={{
base: 'react-modal-overlay',
afterOpen: 'react-modal-overlay_after-open',
beforeClose: 'react-modal-overlay_before-close',
}}
shouldCloseOnOverlayClick={!modalOpts.preventClose}
onRequestClose={closeModal}
>
<div className="react-modal-header">
{!modalOpts.preventClose && (
<button onClick={closeModal} className="btn btn-link btn-plain close-modal">
<X />
</button>
)}
<h3 className="react-modal-title">{modalOpts.title}</h3>
</div>
<div className="react-modal-component">{modalOpts.component}</div>
</ReactModal>
);
};
initApp(user) {
this.socket.emit('USER_ENTER', {
publicKey: user.publicKey,
});
}
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);
bindEvents() {
window.onfocus = () => {
this.props.toggleWindowFocus(true);
};
window.addEventListener('focus', onFocus);
window.addEventListener('blur', onBlur);
return () => {
window.removeEventListener('focus', onFocus);
window.removeEventListener('blur', onBlur);
window.onblur = () => {
this.props.toggleWindowFocus(false);
};
}, [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>
);
};
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();
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);
if (!mounted) {
loading.current = false;
return;
}
const payload = {
username: localUsername,
this.props.createUser({
username,
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;
resolve({
publicKey: exportedEncryptDecryptPublicKey,
});
});
}
return <Home username={user.username} publicKey={user.publicKey} userId={user.id} {...rest} />;
};
render() {
const modalOpts = this.getModal();
return (
<div className={classNames(styles.styles, 'h-100')}>
<div className="nav-container">
{!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"
style={{ overlay: { zIndex: 10 } }}
className={{
base: 'react-modal-content',
afterOpen: 'react-modal-content_after-open',
beforeClose: 'react-modal-content_before-close',
}}
overlayClassName={{
base: 'react-modal-overlay',
afterOpen: 'react-modal-overlay_after-open',
beforeClose: 'react-modal-overlay_before-close',
}}
shouldCloseOnOverlayClick={!modalOpts.preventClose}
onRequestClose={this.props.closeModal}
>
<div className="react-modal-header">
{!modalOpts.preventClose && (
<button onClick={this.props.closeModal} className="btn btn-link btn-plain close-modal">
<X />
</button>
)}
<h3 className="react-modal-title">{modalOpts.title}</h3>
</div>
<div className="react-modal-component">{modalOpts.component}</div>
</Modal>
</div>
);
}
}
WithUser.defaultProps = {
Home.defaultProps = {
modalComponent: null,
};
WithUser.propTypes = {
Home.propTypes = {
receiveEncryptedMessage: PropTypes.func.isRequired,
receiveUnencryptedMessage: PropTypes.func.isRequired,
createUser: PropTypes.func.isRequired,
activities: PropTypes.array.isRequired,
username: PropTypes.string.isRequired,
publicKey: PropTypes.object.isRequired,
members: PropTypes.array.isRequired,
socketId: PropTypes.string.isRequired,
roomId: PropTypes.string.isRequired,
@ -361,7 +247,9 @@ WithUser.propTypes = {
openModal: PropTypes.func.isRequired,
closeModal: PropTypes.func.isRequired,
iAmOwner: PropTypes.bool.isRequired,
userId: PropTypes.string.isRequired,
toggleWindowFocus: PropTypes.func.isRequired,
faviconCount: PropTypes.number.isRequired,
soundIsEnabled: PropTypes.bool.isRequired,
persistenceIsEnabled: PropTypes.bool.isRequired,
toggleSoundEnabled: PropTypes.func.isRequired,
@ -374,9 +262,6 @@ WithUser.propTypes = {
socketConnected: PropTypes.bool.isRequired,
sendUnencryptedMessage: PropTypes.func.isRequired,
sendEncryptedMessage: PropTypes.func.isRequired,
setLanguage: PropTypes.func.isRequired,
language: PropTypes.string.isRequired,
translations: PropTypes.object.isRequired,
};
export default WithUser;
export default Home;

View File

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

View File

@ -1,9 +1,9 @@
import React from 'react';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Tinycon from 'tinycon';
import { notify, beep } from '@/utils/notifications';
import { toggleNotificationAllowed } from '@/actions';
import { toggleNotificationAllowed, toggleNotificationEnabled } from '@/actions';
const mapStateToProps = state => {
return {
@ -19,99 +19,109 @@ const mapStateToProps = state => {
const mapDispatchToProps = {
toggleNotificationAllowed,
toggleNotificationEnabled,
};
const WithNewMessageNotification = ({
room: { id: roomId },
activities,
notificationIsEnabled,
notificationIsAllowed,
soundIsEnabled,
unreadMessageCount,
windowIsFocused,
toggleNotificationAllowed,
children,
}) => {
const [lastMessage, setLastMessage] = React.useState(null);
const [lastUnreadMessageCount, setLastUnreadMessageCount] = React.useState(0);
const WithNewMessageNotification = WrappedComponent => {
return connect(
mapStateToProps,
mapDispatchToProps,
)(
class WithNotificationHOC extends Component {
state = { lastMessage: null, unreadMessageCount: 0 };
React.useEffect(() => {
if (activities.length === 0) {
return;
}
const currentLastMessage = activities[activities.length - 1];
const { username, type, text, fileName, locked, newUsername, currentUsername, action } = currentLastMessage;
static getDerivedStateFromProps(nextProps, prevState) {
const {
room: { id: roomId },
activities,
notificationIsEnabled,
notificationIsAllowed,
soundIsEnabled,
unreadMessageCount,
windowIsFocused,
} = nextProps;
if (currentLastMessage !== lastMessage && !windowIsFocused) {
if (notificationIsAllowed && notificationIsEnabled) {
// Generate the proper notification according to the message type
switch (type) {
case 'USER_ENTER':
notify(`User ${username} joined`);
break;
case 'USER_EXIT':
notify(`User ${username} left`);
break;
case 'RECEIVE_FILE':
notify(`${username} sent file <${fileName}>`);
break;
case 'TEXT_MESSAGE':
notify(`${username} said:`, text);
break;
case 'USER_ACTION':
notify(`${username} ${action}`);
break;
case 'CHANGE_USERNAME':
notify(`${currentUsername} changed their name to ${newUsername}`);
break;
case 'TOGGLE_LOCK_ROOM':
if (locked) {
notify(`Room ${roomId} is now locked`);
} else {
notify(`Room ${roomId} is now unlocked`);
if (activities.length === 0) {
return null;
}
const lastMessage = activities[activities.length - 1];
const { username, type, text, fileName, locked, newUsername, currentUsername, action } = lastMessage;
if (lastMessage !== prevState.lastMessage && !windowIsFocused) {
if (notificationIsAllowed && notificationIsEnabled) {
// Generate the proper notification according to the message type
switch (type) {
case 'USER_ENTER':
notify(`User ${username} joined`);
break;
case 'USER_EXIT':
notify(`User ${username} left`);
break;
case 'RECEIVE_FILE':
notify(`${username} sent file <${fileName}>`);
break;
case 'TEXT_MESSAGE':
notify(`${username} said:`, text);
break;
case 'USER_ACTION':
notify(`${username} ${action}`);
break;
case 'CHANGE_USERNAME':
notify(`${currentUsername} changed their name to ${newUsername}`);
break;
case 'TOGGLE_LOCK_ROOM':
if (locked) {
notify(`Room ${roomId} is now locked`);
} else {
notify(`Room ${roomId} is now unlocked`);
}
break;
default:
break;
}
}
if (soundIsEnabled) beep.play();
}
if (unreadMessageCount !== prevState.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:
break;
this.props.toggleNotificationAllowed(null);
}
}
if (soundIsEnabled) beep.play();
}
setLastMessage(currentLastMessage);
if (unreadMessageCount !== lastUnreadMessageCount) {
setLastUnreadMessageCount(unreadMessageCount);
Tinycon.setBubble(unreadMessageCount);
}
}, [
activities,
lastMessage,
lastUnreadMessageCount,
notificationIsAllowed,
notificationIsEnabled,
roomId,
soundIsEnabled,
unreadMessageCount,
windowIsFocused,
]);
React.useEffect(() => {
switch (Notification.permission) {
case 'granted':
toggleNotificationAllowed(true);
break;
case 'denied':
toggleNotificationAllowed(false);
break;
default:
toggleNotificationAllowed(null);
}
}, [toggleNotificationAllowed]);
return <>{children}</>;
render() {
// Filter props
const {
room,
activities,
notificationIsEnabled,
motificationIsAllowed,
soundIsEnabled,
unreadMessageCount,
windowIsFocused,
toggleNotificationAllowed,
toggleNotificationnEnabled,
...rest
} = this.props;
return <WrappedComponent {...rest} />;
}
},
);
};
const ConnectedWithNewMessageNotification = connect(mapStateToProps, mapDispatchToProps)(WithNewMessageNotification);
export default ConnectedWithNewMessageNotification;
export default WithNewMessageNotification;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,9 @@ exports[`Nav component is displaying 1`] = `
/>
<button
class="btn btn-plain btn-link clipboard-trigger room-id ellipsis"
id="copy-room-url-button"
data-clipboard-text="http://localhost:3000/testRoom"
data-placement="bottom"
data-toggle="tooltip"
>
/testRoom
</button>
@ -24,8 +26,9 @@ exports[`Nav component is displaying 1`] = `
>
<button
class="lock-room btn btn-link btn-plain"
data-testid="lock-room-button"
id="lock-room-button"
data-placement="bottom"
data-toggle="tooltip"
title="You must be the owner to lock or unlock the room"
>
<svg
class="muted"
@ -54,43 +57,54 @@ exports[`Nav component is displaying 1`] = `
</button>
</span>
<div
class="members-menu"
class="dropdown members-dropdown"
>
<button
class="btn btn-link btn-plain members-action"
title="Users"
<a
class="dropdown__trigger "
>
<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"
<button
class="btn btn-link btn-plain members-action"
title="Users"
>
<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>
0
</span>
<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>
0
</span>
</a>
<div
class="dropdown__content "
>
<ul
class="plain"
/>
</div>
</div>
</div>
<button
@ -243,7 +257,9 @@ exports[`Nav component is displaying with another configuration and can rerender
/>
<button
class="btn btn-plain btn-link clipboard-trigger room-id ellipsis"
id="copy-room-url-button"
data-clipboard-text="http://localhost:3000/testRoom_2"
data-placement="bottom"
data-toggle="tooltip"
>
/testRoom_2
</button>
@ -252,8 +268,9 @@ exports[`Nav component is displaying with another configuration and can rerender
>
<button
class="lock-room btn btn-link btn-plain"
data-testid="lock-room-button"
id="lock-room-button"
data-placement="bottom"
data-toggle="tooltip"
title="You must be the owner to lock or unlock the room"
>
<svg
fill="none"
@ -281,270 +298,101 @@ exports[`Nav component is displaying with another configuration and can rerender
</button>
</span>
<div
class="members-menu"
class="dropdown members-dropdown"
>
<button
class="btn btn-link btn-plain members-action"
title="Users"
<a
class="dropdown__trigger "
>
<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"
<button
class="btn btn-link btn-plain members-action"
title="Users"
>
<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
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>
<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>
</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>
<button

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import RoomLink from '@/components/RoomLink';
@ -5,201 +6,193 @@ import T from '@/components/T';
import classes from './styles.module.scss';
const Settings = ({
soundIsEnabled,
persistenceIsEnabled,
toggleSoundEnabled,
notificationIsEnabled,
notificationIsAllowed,
toggleNotificationEnabled,
toggleNotificationAllowed,
togglePersistenceEnabled,
roomId,
setLanguage,
language,
translations,
}) => {
const handleSoundToggle = () => {
toggleSoundEnabled(!soundIsEnabled);
};
class Settings extends Component {
handleSoundToggle() {
this.props.toggleSoundEnabled(!this.props.soundIsEnabled);
}
const handlePersistenceToggle = () => {
togglePersistenceEnabled(!persistenceIsEnabled);
};
handlePersistenceToggle() {
this.props.togglePersistenceEnabled(!this.props.persistenceIsEnabled);
}
const handleNotificationToggle = () => {
handleNotificationToggle() {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
toggleNotificationEnabled(!notificationIsEnabled);
toggleNotificationAllowed(true);
this.props.toggleNotificationEnabled(!this.props.notificationIsEnabled);
this.props.toggleNotificationAllowed(true);
}
if (permission === 'denied') {
toggleNotificationAllowed(false);
this.props.toggleNotificationAllowed(false);
}
});
};
}
const handleLanguageChange = evt => {
setLanguage(evt.target.value);
};
handleLanguageChange(evt) {
const language = evt.target.value;
this.props.setLanguage(language);
}
return (
<div className={classes.styles}>
<section>
<h4>
<T path="newMessageNotification" />
</h4>
<form>
<div className="form-check">
<label className="form-check-label" htmlFor="sound-control">
<input
id="sound-control"
onChange={handleSoundToggle}
className="form-check-input"
type="checkbox"
checked={soundIsEnabled}
/>
<T path="sound" />
</label>
render() {
return (
<div className={classes.styles}>
<section>
<h4>
<T path="newMessageNotification" />
</h4>
<form>
<div className="form-check">
<label className="form-check-label" htmlFor="sound-control">
<input
id="sound-control"
onChange={this.handleSoundToggle.bind(this)}
className="form-check-input"
type="checkbox"
checked={this.props.soundIsEnabled}
/>
<T path="sound" />
</label>
</div>
<div className="form-check">
<label className="form-check-label" htmlFor="notif-control">
{this.props.notificationIsAllowed !== false && (
<>
<input
id="notif-control"
onChange={this.handleNotificationToggle.bind(this)}
className="form-check-input"
type="checkbox"
checked={this.props.notificationIsEnabled}
disabled={this.props.notificationIsAllowed === false} // Important to keep '=== false' here
/>
<T path="desktopNotification" />
</>
)}
{this.props.notificationIsAllowed === false && <T path="desktopNotificationBlocked" />}
</label>
</div>
<div className="form-check">
<label className="form-check-label" htmlFor="persistence-control">
<input
id="persistence-control"
onChange={this.handlePersistenceToggle.bind(this)}
className="form-check-input"
type="checkbox"
checked={this.props.persistenceIsEnabled}
/>
<T path="persistence" />
</label>
</div>
</form>
</section>
<section>
<h4 className="mb-3">
<T path="copyRoomHeader" />
</h4>
<RoomLink roomId={this.props.roomId} translations={this.props.translations} />
</section>
<section>
<h4 className="mb-3">
<T path="languageDropdownHeader" />
</h4>
<p>
<a
href="https://github.com/darkwire/darkwire.io/blob/master/client/README.md#translations"
rel="noopener noreferrer"
target="_blank"
>
<T path="helpTranslate" />
</a>
</p>
<div className="form-group">
<select
value={this.props.language}
className="form-control"
onChange={this.handleLanguageChange.bind(this)}
>
<option value="en">English</option>
<option value="fr">Français</option>
<option value="oc">Occitan</option>
<option value="de">Deutsch</option>
<option value="esAR">Español (Argentina)</option>
<option value="nl">Nederlands</option>
<option value="it">Italiano</option>
<option value="ru">Русский</option>
<option value="pl">Polish</option>
<option value="zhCN">中文</option>
<option value="ja">日本語</option>
<option value="tr">Türkçe</option>
<option value="ko">한국어</option>
</select>
</div>
<div className="form-check">
<label className="form-check-label" htmlFor="notif-control">
{notificationIsAllowed !== false && (
<>
<input
id="notif-control"
onChange={handleNotificationToggle}
className="form-check-input"
type="checkbox"
checked={notificationIsEnabled}
disabled={notificationIsAllowed === false} // Important to keep '=== false' here
/>
<T path="desktopNotification" />
</>
)}
{notificationIsAllowed === false && <T path="desktopNotificationBlocked" />}
</label>
</div>
<div className="form-check">
<label className="form-check-label" htmlFor="persistence-control">
<input
id="persistence-control"
onChange={handlePersistenceToggle}
className="form-check-input"
type="checkbox"
checked={persistenceIsEnabled}
/>
<T path="persistence" />
</label>
</div>
</form>
</section>
</section>
<section>
<h4 className="mb-3">
<T path="copyRoomHeader" />
</h4>
<RoomLink roomId={roomId} translations={translations} />
</section>
<section>
<h4 className="mb-3">
<T path="languageDropdownHeader" />
</h4>
<p>
<a
href="https://github.com/darkwire/darkwire.io/blob/master/client/README.md#translations"
rel="noopener noreferrer"
target="_blank"
>
<T path="helpTranslate" />
</a>
</p>
<div className="form-group">
<select value={language} className="form-control" onChange={handleLanguageChange}>
<option value="en">English</option>
<option value="fr">Français</option>
<option value="oc">Occitan</option>
<option value="de">Deutsch</option>
<option value="esAR">Español (Argentina)</option>
<option value="nl">Nederlands</option>
<option value="it">Italiano</option>
<option value="ru">Русский</option>
<option value="pl">Polish</option>
<option value="zhCN">中文</option>
<option value="ja">日本語</option>
<option value="tr">Türkçe</option>
<option value="ko">한국어</option>
</select>
</div>
</section>
<section>
<h4>
<T path="roomOwnerHeader" />
</h4>
<p>
<T path="roomOwnerText" />
</p>
</section>
<section>
<h4>
<T path="lockRoomHeader" />
</h4>
<p>
<T path="lockRoomText" />
</p>
</section>
<section>
<h4>
<T path="slashCommandsHeader" />
</h4>
<p>
<T path="slashCommandsText" />
</p>
<ul>
<li>
/nick [username]{' '}
<span className="text-muted">
<T path="slashCommandsBullets.0" />
</span>
</li>
<li>
/me [action]{' '}
<span className="text-muted">
<T path="slashCommandsBullets.1" />
</span>
</li>
<li>
/clear{' '}
<span className="text-muted">
<T path="slashCommandsBullets.2" />
</span>
</li>
<li>
/help{' '}
<span className="text-muted">
<T path="slashCommandsBullets.3" />
</span>
</li>
</ul>
</section>
</div>
);
};
<section>
<h4>
<T path="roomOwnerHeader" />
</h4>
<p>
<T path="roomOwnerText" />
</p>
</section>
<section>
<h4>
<T path="lockRoomHeader" />
</h4>
<p>
<T path="lockRoomText" />
</p>
</section>
<section>
<h4>
<T path="slashCommandsHeader" />
</h4>
<p>
<T path="slashCommandsText" />
</p>
<ul>
<li>
/nick [username]{' '}
<span className="text-muted">
<T path="slashCommandsBullets.0" />
</span>
</li>
<li>
/me [action]{' '}
<span className="text-muted">
<T path="slashCommandsBullets.1" />
</span>
</li>
<li>
/clear{' '}
<span className="text-muted">
<T path="slashCommandsBullets.2" />
</span>
</li>
<li>
/help{' '}
<span className="text-muted">
<T path="slashCommandsBullets.3" />
</span>
</li>
</ul>
</section>
</div>
);
}
}
Settings.propTypes = {
soundIsEnabled: PropTypes.bool.isRequired,
persistenceIsEnabled: PropTypes.bool.isRequired,
toggleSoundEnabled: PropTypes.func.isRequired,
togglePersistenceEnabled: PropTypes.func.isRequired,
notificationIsEnabled: PropTypes.bool.isRequired,
notificationIsAllowed: PropTypes.bool,
toggleNotificationEnabled: PropTypes.func.isRequired,
toggleNotificationAllowed: PropTypes.func.isRequired,
roomId: PropTypes.string.isRequired,
setLanguage: PropTypes.func.isRequired,
language: PropTypes.string.isRequired,
translations: PropTypes.object.isRequired,
};

View File

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

View File

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

View File

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

View File

@ -1,37 +1,47 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import RoomLink from '@/components/RoomLink';
const Welcome = ({ roomId, translations, close }) => {
return (
<div>
class Welcome extends Component {
constructor(props) {
super(props);
this.state = {
roomUrl: `https://darkwire.io/${props.roomId}`,
};
}
render() {
return (
<div>
v2.0 is a complete rewrite and includes several new features. Here are some highlights:
<ul className="native">
<li>Support on all modern browsers (Chrome, Firefox, Safari, Safari iOS, Android)</li>
<li>Slash commands (/nick, /me, /clear)</li>
<li>Room owners can lock the room, preventing anyone else from joining</li>
<li>Front-end rewritten in React.js and Redux</li>
<li>Send files up to 4 MB</li>
</ul>
<div>
You can learn more{' '}
<a href="https://github.com/darkwire/darkwire.io" target="_blank" rel="noopener noreferrer">
here
</a>
.
v2.0 is a complete rewrite and includes several new features. Here are some highlights:
<ul className="native">
<li>Support on all modern browsers (Chrome, Firefox, Safari, Safari iOS, Android)</li>
<li>Slash commands (/nick, /me, /clear)</li>
<li>Room owners can lock the room, preventing anyone else from joining</li>
<li>Front-end rewritten in React.js and Redux</li>
<li>Send files up to 4 MB</li>
</ul>
<div>
You can learn more{' '}
<a href="https://github.com/darkwire/darkwire.io" target="_blank" rel="noopener noreferrer">
here
</a>
.
</div>
</div>
<br />
<p className="mb-2">Others can join this room using the following URL:</p>
<RoomLink roomId={this.props.roomId} translations={this.props.translations} />
<div className="react-modal-footer">
<button className="btn btn-primary btn-lg" onClick={this.props.close}>
{this.props.translations.welcomeModalCTA}
</button>
</div>
</div>
<br />
<p className="mb-2">Others can join this room using the following URL:</p>
<RoomLink roomId={roomId} translations={translations} />
<div className="react-modal-footer">
<button className="btn btn-primary btn-lg" onClick={close}>
{translations.welcomeModalCTA}
</button>
</div>
</div>
);
};
);
}
}
Welcome.propTypes = {
roomId: PropTypes.string.isRequired,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -345,18 +345,6 @@
minimatch "^3.1.2"
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":
version "0.11.7"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.7.tgz#38aec044c6c828f6ed51d5d7ae3d9b9faf6dbb0f"
@ -489,28 +477,6 @@
"@nodelib/fs.scandir" "2.1.5"
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":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.0.4.tgz#cbfbec6735711e7c2fc93b9b40adf70ef5a39990"
@ -929,43 +895,11 @@ aria-query@^5.0.0:
dependencies:
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:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
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:
version "1.1.0"
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
@ -1112,11 +1046,20 @@ ci-info@^3.2.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.0.tgz#6d01b3696c59915b6ce057e4aa4adfc2fa25f5ef"
integrity sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==
classnames@^2.3.2:
classnames@^2.1.2, classnames@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
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:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@ -1273,6 +1216,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
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:
version "29.3.1"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e"
@ -1285,13 +1233,6 @@ dir-glob@^3.0.1:
dependencies:
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:
version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
@ -1367,37 +1308,6 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
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:
version "1.1.2"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7"
@ -1412,22 +1322,6 @@ es-get-iterator@^1.1.2:
is-string "^1.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:
version "0.16.4"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.4.tgz#06c86298d233386f5e41bcc14d36086daf3f40bd"
@ -1502,32 +1396,6 @@ eslint-plugin-prettier@^3.1.3:
dependencies:
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:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
@ -1639,7 +1507,7 @@ estraverse@^4.1.1:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
estraverse@^5.1.0, estraverse@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
@ -1769,16 +1637,6 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
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:
version "1.2.3"
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
@ -1808,14 +1666,6 @@ get-stdin@^6.0.0:
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
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:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@ -1866,6 +1716,13 @@ globby@^11.1.0:
merge2 "^1.4.1"
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:
version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
@ -1883,7 +1740,7 @@ grapheme-splitter@^1.0.4:
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
has-bigints@^1.0.1, has-bigints@^1.0.2:
has-bigints@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
@ -1982,11 +1839,6 @@ ignore@^5.2.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c"
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:
version "4.1.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
@ -2023,15 +1875,6 @@ inherits@2:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
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:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
@ -2062,7 +1905,7 @@ is-boolean-object@^1.1.0:
call-bind "^1.0.2"
has-tostringtag "^1.0.0"
is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7:
is-callable@^1.1.3:
version "1.2.7"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
@ -2081,7 +1924,7 @@ is-core-module@^2.9.0:
dependencies:
has "^1.0.3"
is-date-object@^1.0.1, is-date-object@^1.0.5:
is-date-object@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
@ -2105,11 +1948,6 @@ is-map@^2.0.1, is-map@^2.0.2:
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
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:
version "1.0.7"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc"
@ -2150,21 +1988,14 @@ is-set@^2.0.1, is-set@^2.0.2:
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec"
integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==
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:
is-string@^1.0.5:
version "1.0.7"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
dependencies:
has-tostringtag "^1.0.0"
is-symbol@^1.0.2, is-symbol@^1.0.3:
is-symbol@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
@ -2187,13 +2018,6 @@ is-weakmap@^2.0.1:
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
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:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d"
@ -2431,14 +2255,6 @@ json5@^2.2.1:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
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:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
@ -2625,7 +2441,7 @@ object-assign@^4.1.1:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-inspect@^1.12.2, object-inspect@^1.9.0:
object-inspect@^1.9.0:
version "1.12.2"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
@ -2643,7 +2459,7 @@ object-keys@^1.1.1:
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
object.assign@^4.1.3, object.assign@^4.1.4:
object.assign@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f"
integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==
@ -2653,41 +2469,6 @@ object.assign@^4.1.3, object.assign@^4.1.4:
has-symbols "^1.0.3"
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:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@ -2853,7 +2634,7 @@ promise-polyfill@^8.1.3:
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.3.tgz#2edc7e4b81aff781c88a0d577e5fe9da822107c6"
integrity sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==
prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@^15.5.8, prop-types@^15.7.2:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -2972,13 +2753,13 @@ react-router@6.4.4, react-router@^6.4.4:
dependencies:
"@remix-run/router" "1.0.4"
react-tooltip@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.2.0.tgz#e10e7de2385e8fe6bf3438739c574558b455de3b"
integrity sha512-EH6XIg2MDbMTEElSAZQVXMVeFoOhTgQuea2or0iwyzsr9v8rJf3ImMhOtq7Xe/BPlougxC+PmOibazodLdaRoA==
react-simple-dropdown@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/react-simple-dropdown/-/react-simple-dropdown-3.2.3.tgz#c9737bcb7a54c7de267a1afeeec04de588a3fa7b"
integrity sha512-NmyyvA0D4wph5ctzkn8U4wmblOacavJMl9gTOhQR3v8I997mc1FL1NFKkj3Mx+HNysBKRD/HI+kpxXCAgXumPw==
dependencies:
"@floating-ui/dom" "^1.0.4"
classnames "^2.3.2"
classnames "^2.1.2"
prop-types "^15.5.8"
react@^18.2.0:
version "18.2.0"
@ -3038,11 +2819,6 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
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:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@ -3057,15 +2833,6 @@ resolve@^1.22.1:
path-parse "^1.0.7"
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:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
@ -3092,15 +2859,6 @@ run-parallel@^1.1.9:
dependencies:
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":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
@ -3141,6 +2899,11 @@ scheduler@^0.23.0:
dependencies:
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:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
@ -3221,38 +2984,6 @@ stack-utils@^2.0.3:
dependencies:
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:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@ -3317,6 +3048,11 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
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:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.3.1.tgz#14f64e6b77d7ef0b1f6ab850c7a808c6760b414d"
@ -3422,16 +3158,6 @@ uc.micro@^1.0.1:
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
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:
version "0.2.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"

View File

@ -1,5 +1,7 @@
# 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.
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
@ -84,7 +86,7 @@ Then run it. Example:
$ docker run --init --name darkwire.io --rm -p 3001:3001 darkwire.io
```
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)
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)
### Security