mirror of
https://github.com/darkwire/darkwire.io.git
synced 2025-07-18 10:49:02 +00:00
Compare commits
4 Commits
08071fba44
...
047c454988
Author | SHA1 | Date | |
---|---|---|---|
|
047c454988 | ||
|
eede08a840 | ||
|
d67bc945ce | ||
|
d875eefed0 |
@ -1,49 +0,0 @@
|
||||
# Javascript Node CircleCI 2.0 configuration file
|
||||
#
|
||||
|
||||
jobs:
|
||||
test-job:
|
||||
docker:
|
||||
- image: "cimg/node:lts"
|
||||
|
||||
working_directory: ~/repo
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
# Download and cache dependencies
|
||||
- restore_cache:
|
||||
keys:
|
||||
- dependencies-{{ checksum "yarn.lock" }}
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- dependencies-
|
||||
|
||||
- run: yarn setup
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
- client/node_modules
|
||||
- server/node_modules
|
||||
key: dependencies-{{ checksum "yarn.lock" }}
|
||||
|
||||
- run:
|
||||
command: yarn lint
|
||||
|
||||
- run:
|
||||
command: yarn test
|
||||
environment:
|
||||
TZ: UTC
|
||||
VITE_COMMIT_SHA: some_sha
|
||||
|
||||
- store_artifacts: # For coverage report
|
||||
path: client/coverage
|
||||
|
||||
orbs: # declare what orbs we are going to use
|
||||
node: circleci/node@2.0.2 # the node orb provides common node-related configuration
|
||||
|
||||
version: 2.1
|
||||
|
||||
workflows:
|
||||
tests:
|
||||
jobs:
|
||||
- test-job
|
35
.github/workflows/lint-test.yml
vendored
Normal file
35
.github/workflows/lint-test.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x, 18.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm i yarn -g
|
||||
- run: yarn setup
|
||||
env:
|
||||
TZ: UTC
|
||||
VITE_COMMIT_SHA: some_sha
|
||||
- run: yarn lint
|
||||
- run: yarn test
|
||||
env:
|
||||
TZ: UTC
|
||||
VITE_COMMIT_SHA: some_sha
|
@ -1,5 +1,11 @@
|
||||
module.exports = {
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
root: true,
|
||||
@ -7,4 +13,14 @@ 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"
|
||||
}
|
||||
};
|
||||
|
@ -8,7 +8,8 @@
|
||||
<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.io - instant encrypted web chat</title>
|
||||
<title>Darkwire</title>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
</head>
|
||||
<body class="h-100">
|
||||
<div id="root" class="h-100"></div>
|
||||
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,10 @@
|
||||
],
|
||||
"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",
|
||||
@ -28,7 +29,7 @@
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router": "^6.4.4",
|
||||
"react-router-dom": "^6.4.4",
|
||||
"react-simple-dropdown": "^3.2.3",
|
||||
"react-tooltip": "^5.2.0",
|
||||
"redux": "^4.2.0",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"sanitize-html": "^2.7.3",
|
||||
@ -54,6 +55,8 @@
|
||||
"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",
|
||||
|
@ -1,15 +1,17 @@
|
||||
{
|
||||
"short_name": "Darkwire",
|
||||
"name": "Darkwire.io - encrypted web chat",
|
||||
"name": "Darkwire.io - Encrypted Web Chat",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"display": "fullscreen",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
"background_color": "#ffffff",
|
||||
"scope": "/",
|
||||
"description": "Secure and encrypted web chat with Darkwire.io"
|
||||
}
|
||||
|
@ -31,10 +31,6 @@ 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' });
|
||||
};
|
||||
|
@ -40,7 +40,6 @@ 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'],
|
||||
];
|
||||
|
||||
|
@ -1,10 +1,18 @@
|
||||
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);
|
||||
dispatch({ type: `SEND_ENCRYPTED_MESSAGE_${msg.original.type}`, payload: msg.original.payload });
|
||||
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 });
|
||||
}
|
||||
getSocket().emit('ENCRYPTED_MESSAGE', msg.toSend);
|
||||
};
|
||||
|
||||
|
@ -1,448 +1,434 @@
|
||||
/* eslint-disable */
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { COMMIT_SHA } from '@/config/env';
|
||||
import apiUrlGenerator from '@/api/generator';
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
class About extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
roomId: props.roomId,
|
||||
abuseReported: false,
|
||||
};
|
||||
}
|
||||
const About = ({ roomId: roomIdProp }) => {
|
||||
const [roomId, setRoomId] = React.useState(roomIdProp);
|
||||
const [abuseReported, setAbuseReported] = React.useState(false);
|
||||
|
||||
handleUpdateRoomId(evt) {
|
||||
this.setState({
|
||||
roomId: evt.target.value,
|
||||
});
|
||||
}
|
||||
const handleUpdateRoomId = evt => {
|
||||
setRoomId(evt.target.value);
|
||||
};
|
||||
|
||||
handleReportAbuse(evt) {
|
||||
const handleReportAbuse = evt => {
|
||||
evt.preventDefault();
|
||||
fetch(`${apiUrlGenerator('abuse')}/${this.state.roomId}`, {
|
||||
fetch(`${apiUrlGenerator('abuse')}/${roomId}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
this.setState({
|
||||
abuseReported: true,
|
||||
});
|
||||
}
|
||||
setAbuseReported(true);
|
||||
};
|
||||
|
||||
render() {
|
||||
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>
|
||||
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="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.
|
||||
<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="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.
|
||||
<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>
|
||||
<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>}
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
<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 sender’s identity without
|
||||
the sender’s 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 sender’s identity without the sender’s
|
||||
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.
|
||||
Proceed with caution and always confirm recipients beforre starting a chat session.
|
||||
</p>
|
||||
<p>
|
||||
Please also note that <strong>ALL CHATROOMS</strong> are public. 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
|
||||
"darkwire.io/".
|
||||
</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 Darkwire’s 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.
|
||||
Proceed with caution and always confirm recipients beforre starting a chat session.
|
||||
</p>
|
||||
<p>
|
||||
Please also note that <strong>ALL CHATROOMS</strong> are public. 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
|
||||
"darkwire.io/".
|
||||
</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 Darkwire’s 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,
|
||||
|
@ -1,171 +1,134 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { CornerDownRight } from 'react-feather';
|
||||
|
||||
import { getSelectedText, hasTouchSupport } from '@/utils/dom';
|
||||
import { hasTouchSupport } from '@/utils/dom';
|
||||
|
||||
import FileTransfer from '@/components/FileTransfer';
|
||||
|
||||
export class Chat extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
message: '',
|
||||
touchSupport: hasTouchSupport,
|
||||
shiftKeyDown: false,
|
||||
};
|
||||
export const Chat = ({ sendEncryptedMessage, showNotice, userId, username, clearActivities, translations }) => {
|
||||
const [message, setMessage] = React.useState('');
|
||||
const [shiftKeyDown, setShiftKeyDown] = React.useState(false);
|
||||
const textInputRef = React.useRef();
|
||||
|
||||
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 touchSupport = hasTouchSupport;
|
||||
|
||||
// Remove things that aren't digits or chars
|
||||
newUsername = newUsername.replace(/[^A-Za-z0-9]/g, '-');
|
||||
const canSend = message.trim().length;
|
||||
|
||||
const errors = [];
|
||||
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
|
||||
|
||||
if (!newUsername.trim().length) {
|
||||
errors.push('Username cannot be blank');
|
||||
}
|
||||
// Remove things that aren't digits or chars
|
||||
newUsername = newUsername.replace(/[^A-Za-z0-9]/g, '-');
|
||||
|
||||
if (newUsername.toString().length > 16) {
|
||||
errors.push('Username cannot be greater than 16 characters');
|
||||
}
|
||||
const errors = [];
|
||||
|
||||
if (!newUsername.match(/^[A-Z]/i)) {
|
||||
errors.push('Username must start with a letter');
|
||||
}
|
||||
if (!newUsername.trim().length) {
|
||||
errors.push('Username cannot be blank');
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
return this.props.showNotice({
|
||||
message: `${errors.join(', ')}`,
|
||||
level: 'error',
|
||||
});
|
||||
}
|
||||
if (newUsername.toString().length > 16) {
|
||||
errors.push('Username cannot be greater than 16 characters');
|
||||
}
|
||||
|
||||
this.props.sendEncryptedMessage({
|
||||
type: 'CHANGE_USERNAME',
|
||||
payload: {
|
||||
id: this.props.userId,
|
||||
newUsername,
|
||||
currentUsername: this.props.username,
|
||||
},
|
||||
if (!newUsername.match(/^[A-Z]/i)) {
|
||||
errors.push('Username must start with a letter');
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
return showNotice({
|
||||
message: `${errors.join(', ')}`,
|
||||
level: 'error',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
this.props.sendEncryptedMessage({
|
||||
type: 'USER_ACTION',
|
||||
payload: {
|
||||
action: actionMessage,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
command: 'clear',
|
||||
description: 'Clears the chat screen',
|
||||
paramaters: [],
|
||||
usage: '/clear',
|
||||
scope: 'local',
|
||||
action: (params = null) => {
|
||||
// eslint-disable-line
|
||||
this.props.clearActivities();
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.focusChat) {
|
||||
if (!getSelectedText()) {
|
||||
// Don't focus for now, evaluate UX benfits
|
||||
// this.textInput.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(nextProps, nextState) {
|
||||
if (!nextState.message.trim().length) {
|
||||
// autosize.update(this.textInput)
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp(e) {
|
||||
if (e.key === 'Shift') {
|
||||
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: '',
|
||||
sendEncryptedMessage({
|
||||
type: 'CHANGE_USERNAME',
|
||||
payload: {
|
||||
id: userId,
|
||||
newUsername,
|
||||
currentUsername: username,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
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: '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);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = e => {
|
||||
if (e.key === 'Shift') {
|
||||
setShiftKeyDown(true);
|
||||
}
|
||||
if (e.key === 'Enter' && !hasTouchSupport && !shiftKeyDown) {
|
||||
e.preventDefault();
|
||||
if (canSend) {
|
||||
sendMessage();
|
||||
} else {
|
||||
setMessage('');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
executeCommand(command) {
|
||||
const commandToExecute = this.commands.find(cmnd => cmnd.command === command.command);
|
||||
const executeCommand = command => {
|
||||
const commandToExecute = commands.find(cmnd => cmnd.command === command.command);
|
||||
|
||||
if (commandToExecute) {
|
||||
const { params } = command;
|
||||
@ -175,19 +138,20 @@ export class Chat extends Component {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
handleSendClick() {
|
||||
this.sendMessage.bind(this);
|
||||
this.textInput.focus();
|
||||
}
|
||||
|
||||
handleFormSubmit(evt) {
|
||||
const handleSendClick = evt => {
|
||||
evt.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
sendMessage();
|
||||
textInputRef.current.focus();
|
||||
};
|
||||
|
||||
parseCommand(message) {
|
||||
const handleFormSubmit = evt => {
|
||||
evt.preventDefault();
|
||||
sendMessage();
|
||||
};
|
||||
|
||||
const parseCommand = message => {
|
||||
const commandTrigger = {
|
||||
command: null,
|
||||
params: [],
|
||||
@ -207,23 +171,22 @@ export class Chat extends Component {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
sendMessage() {
|
||||
if (!this.canSend()) {
|
||||
const sendMessage = () => {
|
||||
if (!canSend) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { message } = this.state;
|
||||
const isCommand = this.parseCommand(message);
|
||||
const isCommand = parseCommand(message);
|
||||
|
||||
if (isCommand) {
|
||||
const res = this.executeCommand(isCommand);
|
||||
const res = executeCommand(isCommand);
|
||||
if (res === false) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.props.sendEncryptedMessage({
|
||||
sendEncryptedMessage({
|
||||
type: 'TEXT_MESSAGE',
|
||||
payload: {
|
||||
text: message,
|
||||
@ -232,55 +195,41 @@ export class Chat extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
message: '',
|
||||
});
|
||||
}
|
||||
setMessage('');
|
||||
};
|
||||
|
||||
handleInputChange(evt) {
|
||||
this.setState({
|
||||
message: evt.target.value,
|
||||
});
|
||||
}
|
||||
const handleInputChange = evt => {
|
||||
setMessage(evt.target.value);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
Chat.propTypes = {
|
||||
sendEncryptedMessage: PropTypes.func.isRequired,
|
||||
|
@ -207,7 +207,7 @@ describe('Chat component', () => {
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' });
|
||||
});
|
||||
|
||||
it('should work with touch support', () => {
|
||||
it('should work with touch support', async () => {
|
||||
// Enable touch support
|
||||
dom.hasTouchSupport = true;
|
||||
|
||||
@ -239,15 +239,15 @@ describe('Chat component', () => {
|
||||
fireEvent.change(textarea, { target: { value: 'test' } });
|
||||
|
||||
// Touch send button
|
||||
fireEvent.click(getByTitle('Send'));
|
||||
await fireEvent.click(getByTitle('Send'));
|
||||
|
||||
expect(sendEncryptedMessage).toHaveBeenLastCalledWith({
|
||||
payload: { text: 'test', timestamp: 1584183718135 },
|
||||
type: 'TEXT_MESSAGE',
|
||||
});
|
||||
|
||||
// Should not send message
|
||||
fireEvent.click(getByTitle('Send'));
|
||||
// Should not send message because of the empty message
|
||||
await fireEvent.click(getByTitle('Send'));
|
||||
|
||||
expect(sendEncryptedMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
@ -1,7 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
const Connecting = () => {
|
||||
return <div>Please wait while we secure a connection to Darkwire...</div>;
|
||||
};
|
||||
|
||||
export default class Connecting extends Component {
|
||||
render() {
|
||||
return <div>Please wait while we secure a connection to Darkwire...</div>;
|
||||
}
|
||||
}
|
||||
export default Connecting;
|
||||
|
@ -1,6 +1,7 @@
|
||||
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';
|
||||
@ -10,39 +11,28 @@ import styles from './styles.module.scss';
|
||||
|
||||
const ActivityList = ({ activities, openModal }) => {
|
||||
const [focusChat, setFocusChat] = React.useState(false);
|
||||
const [scrolledToBottom, setScrolledToBottom] = React.useState(true);
|
||||
const [scrolledToBottom, setScrolledToBottom] = useSafeState(true);
|
||||
const messageStream = React.useRef(null);
|
||||
const activitiesList = React.useRef(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const currentMessageStream = messageStream.current;
|
||||
useEventListener(messageStream, 'scroll', () => {
|
||||
const messageStreamHeight = messageStream.current.clientHeight;
|
||||
const activitiesListHeight = activitiesList.current.clientHeight;
|
||||
|
||||
// Update scrolledToBottom state if we scroll the activity stream
|
||||
const onScroll = () => {
|
||||
const messageStreamHeight = messageStream.current.clientHeight;
|
||||
const activitiesListHeight = activitiesList.current.clientHeight;
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const elemRect = activitiesList.current.getBoundingClientRect();
|
||||
const offset = elemRect.top - bodyRect.top;
|
||||
const activitiesListYPos = offset;
|
||||
|
||||
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);
|
||||
const newScrolledToBottom = activitiesListHeight + (activitiesListYPos - 60) <= messageStreamHeight;
|
||||
if (newScrolledToBottom) {
|
||||
if (!scrolledToBottom) {
|
||||
setScrolledToBottom(true);
|
||||
}
|
||||
};
|
||||
|
||||
currentMessageStream.addEventListener('scroll', onScroll);
|
||||
return () => {
|
||||
// Unbind event if component unmounted
|
||||
currentMessageStream.removeEventListener('scroll', onScroll);
|
||||
};
|
||||
}, [scrolledToBottom]);
|
||||
} else if (scrolledToBottom) {
|
||||
setScrolledToBottom(false);
|
||||
}
|
||||
});
|
||||
|
||||
const scrollToBottomIfShould = React.useCallback(() => {
|
||||
if (scrolledToBottom) {
|
||||
@ -57,7 +47,7 @@ const ActivityList = ({ activities, openModal }) => {
|
||||
const scrollToBottom = React.useCallback(() => {
|
||||
messageStream.current.scrollTop = messageStream.current.scrollHeight;
|
||||
setScrolledToBottom(true);
|
||||
}, []);
|
||||
}, [setScrolledToBottom]);
|
||||
|
||||
const handleChatClick = () => {
|
||||
setFocusChat(true);
|
||||
|
@ -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,21 +58,25 @@ describe('ActivityList component', () => {
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
fireEvent.click(getByText('By using Darkwire, you are agreeing to our Acceptable Use Policy and Terms of Service'));
|
||||
vi.runAllTimers();
|
||||
await fireEvent.click(
|
||||
getByText('By using Darkwire, you are agreeing to our Acceptable Use Policy and Terms of Service'),
|
||||
);
|
||||
await act(() => vi.runAllTimers());
|
||||
|
||||
expect(mockOpenModal.mock.calls[0][0]).toBe('About');
|
||||
vi.runAllTimers();
|
||||
await act(() => vi.runAllTimers());
|
||||
});
|
||||
|
||||
it('should focus chat', () => {
|
||||
it('should focus chat', async () => {
|
||||
const { getByTestId } = render(
|
||||
<Provider store={store}>
|
||||
<ActivityList openModal={vi.fn()} activities={[]} />
|
||||
</Provider>,
|
||||
);
|
||||
fireEvent.click(getByTestId('main-div'));
|
||||
vi.runAllTimers();
|
||||
|
||||
await fireEvent.click(getByTestId('main-div'));
|
||||
|
||||
await act(() => vi.runAllTimers());
|
||||
});
|
||||
|
||||
it('should scroll to bottom on new message if not scrolled', () => {
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import React from 'react';
|
||||
import ReactModal 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';
|
||||
@ -21,68 +23,26 @@ import styles from './styles.module.scss';
|
||||
|
||||
const crypto = new Crypto();
|
||||
|
||||
Modal.setAppElement('#root');
|
||||
ReactModal.setAppElement('#root');
|
||||
|
||||
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) {
|
||||
const Modal = ({
|
||||
closeModal,
|
||||
modalComponent,
|
||||
roomId,
|
||||
translations,
|
||||
toggleSoundEnabled,
|
||||
togglePersistenceEnabled,
|
||||
soundIsEnabled,
|
||||
persistenceIsEnabled,
|
||||
toggleNotificationEnabled,
|
||||
toggleNotificationAllowed,
|
||||
notificationIsEnabled,
|
||||
notificationIsAllowed,
|
||||
setLanguage,
|
||||
language,
|
||||
}) => {
|
||||
const getModal = () => {
|
||||
switch (modalComponent) {
|
||||
case 'Connecting':
|
||||
return {
|
||||
component: <Connecting />,
|
||||
@ -91,40 +51,38 @@ class Home extends Component {
|
||||
};
|
||||
case 'About':
|
||||
return {
|
||||
component: <About roomId={this.props.roomId} />,
|
||||
title: this.props.translations.aboutHeader,
|
||||
component: <About roomId={roomId} />,
|
||||
title: translations.aboutHeader,
|
||||
};
|
||||
case 'Settings':
|
||||
return {
|
||||
component: (
|
||||
<Settings
|
||||
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}
|
||||
roomId={roomId}
|
||||
toggleSoundEnabled={toggleSoundEnabled}
|
||||
togglePersistenceEnabled={togglePersistenceEnabled}
|
||||
soundIsEnabled={soundIsEnabled}
|
||||
persistenceIsEnabled={persistenceIsEnabled}
|
||||
toggleNotificationEnabled={toggleNotificationEnabled}
|
||||
toggleNotificationAllowed={toggleNotificationAllowed}
|
||||
notificationIsEnabled={notificationIsEnabled}
|
||||
notificationIsAllowed={notificationIsAllowed}
|
||||
setLanguage={setLanguage}
|
||||
language={language}
|
||||
translations={translations}
|
||||
/>
|
||||
),
|
||||
title: this.props.translations.settingsHeader,
|
||||
title: translations.settingsHeader,
|
||||
};
|
||||
case 'Welcome':
|
||||
return {
|
||||
component: (
|
||||
<Welcome roomId={this.props.roomId} close={this.props.closeModal} translations={this.props.translations} />
|
||||
),
|
||||
title: this.props.translations.welcomeHeader,
|
||||
component: <Welcome roomId={roomId} close={closeModal} translations={translations} />,
|
||||
title: translations.welcomeHeader,
|
||||
};
|
||||
case 'Room Locked':
|
||||
return {
|
||||
component: <RoomLocked modalContent={this.props.translations.lockedRoomHeader} />,
|
||||
title: this.props.translations.lockedRoomHeader,
|
||||
component: <RoomLocked modalContent={translations.lockedRoomHeader} />,
|
||||
title: translations.lockedRoomHeader,
|
||||
preventClose: true,
|
||||
};
|
||||
default:
|
||||
@ -133,112 +91,268 @@ class Home extends Component {
|
||||
title: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initApp(user) {
|
||||
this.socket.emit('USER_ENTER', {
|
||||
publicKey: user.publicKey,
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
const Home = ({
|
||||
receiveEncryptedMessage,
|
||||
receiveUnencryptedMessage,
|
||||
activities,
|
||||
username,
|
||||
publicKey,
|
||||
members,
|
||||
socketId,
|
||||
roomId,
|
||||
roomLocked,
|
||||
modalComponent,
|
||||
openModal,
|
||||
closeModal,
|
||||
iAmOwner,
|
||||
userId,
|
||||
toggleWindowFocus,
|
||||
soundIsEnabled,
|
||||
persistenceIsEnabled,
|
||||
toggleSoundEnabled,
|
||||
togglePersistenceEnabled,
|
||||
notificationIsEnabled,
|
||||
notificationIsAllowed,
|
||||
toggleNotificationEnabled,
|
||||
toggleNotificationAllowed,
|
||||
toggleSocketConnected,
|
||||
socketConnected,
|
||||
sendUnencryptedMessage,
|
||||
sendEncryptedMessage,
|
||||
translations,
|
||||
setLanguage,
|
||||
language,
|
||||
}) => {
|
||||
const socketPayloadRef = React.useRef({
|
||||
username: username,
|
||||
publicKey: publicKey,
|
||||
isOwner: iAmOwner,
|
||||
id: userId,
|
||||
});
|
||||
socketPayloadRef.current = {
|
||||
username: username,
|
||||
publicKey: publicKey,
|
||||
isOwner: iAmOwner,
|
||||
id: userId,
|
||||
};
|
||||
|
||||
// Add blur et focus listeners
|
||||
React.useEffect(() => {
|
||||
const onFocus = () => {
|
||||
toggleWindowFocus(true);
|
||||
};
|
||||
const onBlur = () => {
|
||||
toggleWindowFocus(false);
|
||||
};
|
||||
|
||||
window.addEventListener('focus', onFocus);
|
||||
window.addEventListener('blur', onBlur);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('focus', onFocus);
|
||||
window.removeEventListener('blur', onBlur);
|
||||
};
|
||||
}, [toggleWindowFocus]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const socket = connectSocket(socketId);
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
toggleSocketConnected(false);
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
window.onfocus = () => {
|
||||
this.props.toggleWindowFocus(true);
|
||||
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.onblur = () => {
|
||||
this.props.toggleWindowFocus(false);
|
||||
};
|
||||
}
|
||||
window.addEventListener('beforeunload', onUnload);
|
||||
|
||||
createUser() {
|
||||
return new Promise(async resolve => {
|
||||
const username = this.props.username || nanoid();
|
||||
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();
|
||||
|
||||
const encryptDecryptKeys = await crypto.createEncryptDecryptKeys();
|
||||
const exportedEncryptDecryptPrivateKey = await crypto.exportKey(encryptDecryptKeys.privateKey);
|
||||
const exportedEncryptDecryptPublicKey = await crypto.exportKey(encryptDecryptKeys.publicKey);
|
||||
|
||||
this.props.createUser({
|
||||
username,
|
||||
if (!mounted) {
|
||||
loading.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
username: localUsername,
|
||||
publicKey: exportedEncryptDecryptPublicKey,
|
||||
privateKey: exportedEncryptDecryptPrivateKey,
|
||||
});
|
||||
};
|
||||
dispatch(createUser(payload));
|
||||
|
||||
resolve({
|
||||
publicKey: exportedEncryptDecryptPublicKey,
|
||||
});
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <Home username={user.username} publicKey={user.publicKey} userId={user.id} {...rest} />;
|
||||
};
|
||||
|
||||
Home.defaultProps = {
|
||||
WithUser.defaultProps = {
|
||||
modalComponent: null,
|
||||
};
|
||||
|
||||
Home.propTypes = {
|
||||
WithUser.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,
|
||||
@ -247,9 +361,7 @@ Home.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,
|
||||
@ -262,6 +374,9 @@ Home.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 Home;
|
||||
export default WithUser;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { render } from '@testing-library/react';
|
||||
import { test, expect, vi } from 'vitest';
|
||||
@ -20,10 +19,18 @@ 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
|
||||
@ -45,14 +52,13 @@ vi.mock('../../utils/crypto', () => {
|
||||
});
|
||||
|
||||
test('Home component is displaying', async () => {
|
||||
const { asFragment } = render(
|
||||
const { asFragment, findByText } = render(
|
||||
<Provider store={store}>
|
||||
<Home
|
||||
translations={{}}
|
||||
members={[]}
|
||||
openModal={() => {}}
|
||||
activities={[]}
|
||||
match={{ params: { roomId: 'roomTest' } }}
|
||||
createUser={() => {}}
|
||||
toggleSocketConnected={() => {}}
|
||||
receiveEncryptedMessage={() => {}}
|
||||
@ -76,9 +82,16 @@ 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();
|
||||
});
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Tinycon from 'tinycon';
|
||||
|
||||
import { notify, beep } from '@/utils/notifications';
|
||||
import { toggleNotificationAllowed, toggleNotificationEnabled } from '@/actions';
|
||||
import { toggleNotificationAllowed } from '@/actions';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
@ -19,109 +19,99 @@ const mapStateToProps = state => {
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleNotificationAllowed,
|
||||
toggleNotificationEnabled,
|
||||
};
|
||||
|
||||
const WithNewMessageNotification = WrappedComponent => {
|
||||
return connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(
|
||||
class WithNotificationHOC extends Component {
|
||||
state = { lastMessage: null, unreadMessageCount: 0 };
|
||||
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);
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
const {
|
||||
room: { id: roomId },
|
||||
activities,
|
||||
notificationIsEnabled,
|
||||
notificationIsAllowed,
|
||||
soundIsEnabled,
|
||||
unreadMessageCount,
|
||||
windowIsFocused,
|
||||
} = nextProps;
|
||||
React.useEffect(() => {
|
||||
if (activities.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentLastMessage = activities[activities.length - 1];
|
||||
const { username, type, text, fileName, locked, newUsername, currentUsername, action } = currentLastMessage;
|
||||
|
||||
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);
|
||||
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 'denied':
|
||||
this.props.toggleNotificationAllowed(false);
|
||||
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:
|
||||
this.props.toggleNotificationAllowed(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (soundIsEnabled) beep.play();
|
||||
}
|
||||
|
||||
render() {
|
||||
// Filter props
|
||||
const {
|
||||
room,
|
||||
activities,
|
||||
notificationIsEnabled,
|
||||
motificationIsAllowed,
|
||||
soundIsEnabled,
|
||||
unreadMessageCount,
|
||||
windowIsFocused,
|
||||
toggleNotificationAllowed,
|
||||
toggleNotificationnEnabled,
|
||||
...rest
|
||||
} = this.props;
|
||||
return <WrappedComponent {...rest} />;
|
||||
}
|
||||
},
|
||||
);
|
||||
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}</>;
|
||||
};
|
||||
|
||||
export default WithNewMessageNotification;
|
||||
const ConnectedWithNewMessageNotification = connect(mapStateToProps, mapDispatchToProps)(WithNewMessageNotification);
|
||||
|
||||
export default ConnectedWithNewMessageNotification;
|
||||
|
@ -62,10 +62,7 @@ exports[`Connected Home component > should display 1`] = `
|
||||
/>
|
||||
<button
|
||||
class="btn btn-plain btn-link clipboard-trigger room-id ellipsis"
|
||||
data-clipboard-text="http://localhost:3000/"
|
||||
data-placement="bottom"
|
||||
data-toggle="tooltip"
|
||||
title="Copied"
|
||||
id="copy-room-url-button"
|
||||
>
|
||||
/
|
||||
</button>
|
||||
@ -74,9 +71,8 @@ exports[`Connected Home component > should display 1`] = `
|
||||
>
|
||||
<button
|
||||
class="lock-room btn btn-link btn-plain"
|
||||
data-placement="bottom"
|
||||
data-toggle="tooltip"
|
||||
title="You must be the owner to lock or unlock the room"
|
||||
data-testid="lock-room-button"
|
||||
id="lock-room-button"
|
||||
>
|
||||
<svg
|
||||
class="muted"
|
||||
@ -105,54 +101,43 @@ exports[`Connected Home component > should display 1`] = `
|
||||
</button>
|
||||
</span>
|
||||
<div
|
||||
class="dropdown members-dropdown"
|
||||
class="members-menu"
|
||||
>
|
||||
<a
|
||||
class="dropdown__trigger "
|
||||
<button
|
||||
class="btn btn-link btn-plain members-action"
|
||||
title="Users"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
@ -3,7 +3,6 @@ import { useLoaderData } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
receiveEncryptedMessage,
|
||||
createUser,
|
||||
openModal,
|
||||
closeModal,
|
||||
toggleWindowFocus,
|
||||
@ -26,8 +25,6 @@ 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),
|
||||
@ -48,7 +45,6 @@ const mapStateToProps = state => {
|
||||
|
||||
const mapDispatchToProps = {
|
||||
receiveEncryptedMessage,
|
||||
createUser,
|
||||
openModal,
|
||||
closeModal,
|
||||
toggleWindowFocus,
|
||||
@ -63,11 +59,19 @@ const mapDispatchToProps = {
|
||||
setLanguage,
|
||||
};
|
||||
|
||||
export const ConnectedHome = WithNewMessageNotification(connect(mapStateToProps, mapDispatchToProps)(Home));
|
||||
export const ConnectedHome = connect(mapStateToProps, mapDispatchToProps)(Home);
|
||||
|
||||
export const ConnectedHomeWithNotification = ({ socketId, ...props }) => {
|
||||
return (
|
||||
<WithNewMessageNotification>
|
||||
<ConnectedHome socketId={socketId} {...props} />
|
||||
</WithNewMessageNotification>
|
||||
);
|
||||
};
|
||||
|
||||
const HomeWithParams = ({ ...props }) => {
|
||||
const socketId = useLoaderData();
|
||||
return <ConnectedHome socketId={socketId} {...props} />;
|
||||
return <ConnectedHomeWithNotification socketId={socketId} {...props} />;
|
||||
};
|
||||
|
||||
export default HomeWithParams;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import Tinycon from 'tinycon';
|
||||
@ -9,8 +8,9 @@ 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 { ConnectedHome } from './';
|
||||
import { ConnectedHomeWithNotification } from './';
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
@ -45,12 +45,14 @@ 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(),
|
||||
};
|
||||
}),
|
||||
};
|
||||
@ -102,68 +104,85 @@ describe('Connected Home component', () => {
|
||||
delete global.Notification;
|
||||
});
|
||||
|
||||
it('should display', () => {
|
||||
const { asFragment } = render(
|
||||
it('should display', async () => {
|
||||
const { asFragment, findByText } = render(
|
||||
<Provider store={store}>
|
||||
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" />
|
||||
<ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
await findByText('Disconnected');
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should set notification', () => {
|
||||
render(
|
||||
it('should detect notification granted', async () => {
|
||||
const { findByText } = render(
|
||||
<Provider store={store}>
|
||||
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" />
|
||||
<ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
|
||||
</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',
|
||||
};
|
||||
|
||||
render(
|
||||
const { findByText } = render(
|
||||
<Provider store={store}>
|
||||
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" />
|
||||
<ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(store.getState().app.notificationIsAllowed).toBe(false);
|
||||
await findByText('Disconnected');
|
||||
|
||||
expect(store.getState().app.notificationIsAllowed).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect notification default', async () => {
|
||||
global.Notification = {
|
||||
permission: 'default',
|
||||
};
|
||||
|
||||
render(
|
||||
const { findByText } = render(
|
||||
<Provider store={store}>
|
||||
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" />
|
||||
<ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
|
||||
</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 } = render(
|
||||
|
||||
const { rerender, findByText } = render(
|
||||
<Provider store={store}>
|
||||
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" roomId={'testId'} />
|
||||
<ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
await findByText('Disconnected');
|
||||
|
||||
// Test with window focused
|
||||
await receiveEncryptedMessage({
|
||||
type: 'TEXT_MESSAGE',
|
||||
payload: {},
|
||||
})(store.dispatch, store.getState);
|
||||
await act(() =>
|
||||
receiveEncryptedMessage({
|
||||
type: 'TEXT_MESSAGE',
|
||||
payload: {},
|
||||
})(store.dispatch, store.getState),
|
||||
);
|
||||
|
||||
rerender(
|
||||
<Provider store={store}>
|
||||
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" roomId={'testId'} />
|
||||
<ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
@ -173,39 +192,48 @@ describe('Connected Home component', () => {
|
||||
expect(Tinycon.setBubble).not.toHaveBeenCalled();
|
||||
|
||||
// Test with window unfocused
|
||||
await toggleWindowFocus(false)(store.dispatch);
|
||||
await receiveEncryptedMessage({
|
||||
type: 'TEXT_MESSAGE',
|
||||
payload: {},
|
||||
})(store.dispatch, store.getState);
|
||||
await act(() => toggleWindowFocus(false)(store.dispatch));
|
||||
await act(() =>
|
||||
receiveEncryptedMessage({
|
||||
type: 'TEXT_MESSAGE',
|
||||
payload: {},
|
||||
})(store.dispatch, store.getState),
|
||||
);
|
||||
|
||||
rerender(
|
||||
<Provider store={store}>
|
||||
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" roomId={'testId'} />
|
||||
<ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
|
||||
</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 toggleNotificationEnabled(false)(store.dispatch);
|
||||
await toggleSoundEnabled(false)(store.dispatch);
|
||||
await receiveEncryptedMessage({
|
||||
type: 'TEXT_MESSAGE',
|
||||
payload: {},
|
||||
})(store.dispatch, store.getState);
|
||||
await act(() => toggleNotificationEnabled(false)(store.dispatch));
|
||||
await act(() => toggleSoundEnabled(false)(store.dispatch));
|
||||
await act(() =>
|
||||
receiveEncryptedMessage({
|
||||
type: 'TEXT_MESSAGE',
|
||||
payload: {},
|
||||
})(store.dispatch, store.getState),
|
||||
);
|
||||
|
||||
rerender(
|
||||
<Provider store={store}>
|
||||
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" roomId={'testId'} />
|
||||
<ConnectedHomeWithNotification userId="testUserId" socketId="roomTest" />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(store.getState().app.unreadMessageCount).toBe(2);
|
||||
expect(notify).toHaveBeenCalledTimes(1);
|
||||
expect(beep.play).toHaveBeenCalledTimes(1);
|
||||
expect(notify).toHaveBeenCalledTimes(0);
|
||||
expect(beep.play).toHaveBeenCalledTimes(0);
|
||||
expect(Tinycon.setBubble).toHaveBeenLastCalledWith(2);
|
||||
});
|
||||
});
|
||||
|
@ -1,34 +1,31 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import Linkify from 'react-linkify';
|
||||
|
||||
import Username from '@/components/Username';
|
||||
|
||||
class Message extends Component {
|
||||
render() {
|
||||
const msg = decodeURI(this.props.message);
|
||||
const Message = ({ message, timestamp, sender }) => {
|
||||
const msg = decodeURI(message);
|
||||
|
||||
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>
|
||||
return (
|
||||
<div>
|
||||
<div className="chat-meta">
|
||||
<Username username={sender} />
|
||||
<span className="muted timestamp">{moment(timestamp).format('LT')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
<div className="chat">
|
||||
<Linkify
|
||||
properties={{
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
}}
|
||||
>
|
||||
{msg}
|
||||
</Linkify>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Message.propTypes = {
|
||||
sender: PropTypes.string.isRequired,
|
||||
|
@ -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', () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.useFakeTimers();
|
||||
const mockClipboardWriteTest = vi.fn();
|
||||
|
||||
const mockTranslations = {
|
||||
newRoomButton: 'new room',
|
||||
@ -43,6 +43,8 @@ const mockTranslations = {
|
||||
aboutButton: 'about',
|
||||
};
|
||||
|
||||
vi.useFakeTimers();
|
||||
|
||||
test('Nav component is displaying', async () => {
|
||||
const { asFragment } = render(
|
||||
<Nav
|
||||
@ -58,10 +60,6 @@ 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 () => {
|
||||
@ -99,16 +97,15 @@ test('Nav component is displaying with another configuration and can rerender',
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(mock$).toHaveBeenCalledWith('.me-icon-wrap');
|
||||
expect(mock$).toHaveBeenLastCalledWith('.owner-icon-wrap');
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Can copy room url', async () => {
|
||||
document.execCommand = vi.fn(() => true);
|
||||
navigator.clipboard = { writeText: mockClipboardWriteTest };
|
||||
|
||||
const toggleLockRoom = vi.fn();
|
||||
|
||||
const { getByText } = render(
|
||||
const { getByText, queryByText } = render(
|
||||
<Nav
|
||||
members={[
|
||||
{ id: 'id1', username: 'alan', isOwner: true },
|
||||
@ -120,27 +117,28 @@ test('Can copy room url', async () => {
|
||||
toggleLockRoom={toggleLockRoom}
|
||||
openModal={() => {}}
|
||||
iAmOwner={false}
|
||||
translations={{}}
|
||||
translations={{ copyButtonTooltip: 'Copied' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(getByText(`/testRoom`));
|
||||
await act(async () => {
|
||||
await fireEvent.click(getByText('/testRoom'));
|
||||
});
|
||||
|
||||
expect(document.execCommand).toHaveBeenLastCalledWith('copy');
|
||||
expect(mock$).toHaveBeenCalledTimes(15);
|
||||
expect(mockTooltip).toHaveBeenLastCalledWith('show');
|
||||
expect(mockClipboardWriteTest).toHaveBeenLastCalledWith('http://localhost:3000/testRoom');
|
||||
|
||||
await getByText('Copied');
|
||||
|
||||
// Wait tooltip closing
|
||||
vi.runAllTimers();
|
||||
await act(() => vi.runAllTimers());
|
||||
|
||||
expect(mock$).toHaveBeenCalledTimes(18);
|
||||
expect(mockTooltip).toHaveBeenLastCalledWith('hide');
|
||||
expect(queryByText('Copied')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Can lock/unlock room is room owner only', async () => {
|
||||
const toggleLockRoom = vi.fn();
|
||||
|
||||
const { rerender, getByTitle } = render(
|
||||
const { rerender, getByTestId, getByText, queryByText } = render(
|
||||
<Nav
|
||||
members={[
|
||||
{ id: 'id1', username: 'alan', isOwner: true },
|
||||
@ -156,13 +154,13 @@ test('Can lock/unlock room is room owner only', async () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const toggleLockRoomButton = getByTitle('You must be the owner to lock or unlock the room');
|
||||
const toggleLockRoomButton = getByTestId('lock-room-button');
|
||||
|
||||
fireEvent.click(toggleLockRoomButton);
|
||||
await fireEvent.click(toggleLockRoomButton);
|
||||
|
||||
expect(toggleLockRoom).toHaveBeenCalledWith();
|
||||
|
||||
fireEvent.click(toggleLockRoomButton);
|
||||
await fireEvent.click(toggleLockRoomButton);
|
||||
|
||||
expect(toggleLockRoom).toHaveBeenCalledTimes(2);
|
||||
|
||||
@ -183,11 +181,16 @@ test('Can lock/unlock room is room owner only', async () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(toggleLockRoomButton);
|
||||
await fireEvent.click(toggleLockRoomButton);
|
||||
|
||||
expect(toggleLockRoom).toHaveBeenCalledTimes(2);
|
||||
expect(mock$).toHaveBeenLastCalledWith('.lock-room');
|
||||
expect(mockTooltip).toHaveBeenLastCalledWith('show');
|
||||
|
||||
await getByText('You must be the owner to lock or unlock the room');
|
||||
|
||||
// Wait tooltip closing
|
||||
await act(() => vi.runAllTimers());
|
||||
|
||||
expect(queryByText('You must be the owner to lock or unlock the room')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Can show user list', async () => {
|
||||
@ -227,8 +230,10 @@ 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();
|
||||
});
|
||||
|
@ -15,9 +15,7 @@ exports[`Nav component is displaying 1`] = `
|
||||
/>
|
||||
<button
|
||||
class="btn btn-plain btn-link clipboard-trigger room-id ellipsis"
|
||||
data-clipboard-text="http://localhost:3000/testRoom"
|
||||
data-placement="bottom"
|
||||
data-toggle="tooltip"
|
||||
id="copy-room-url-button"
|
||||
>
|
||||
/testRoom
|
||||
</button>
|
||||
@ -26,9 +24,8 @@ exports[`Nav component is displaying 1`] = `
|
||||
>
|
||||
<button
|
||||
class="lock-room btn btn-link btn-plain"
|
||||
data-placement="bottom"
|
||||
data-toggle="tooltip"
|
||||
title="You must be the owner to lock or unlock the room"
|
||||
data-testid="lock-room-button"
|
||||
id="lock-room-button"
|
||||
>
|
||||
<svg
|
||||
class="muted"
|
||||
@ -57,54 +54,43 @@ exports[`Nav component is displaying 1`] = `
|
||||
</button>
|
||||
</span>
|
||||
<div
|
||||
class="dropdown members-dropdown"
|
||||
class="members-menu"
|
||||
>
|
||||
<a
|
||||
class="dropdown__trigger "
|
||||
<button
|
||||
class="btn btn-link btn-plain members-action"
|
||||
title="Users"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@ -257,9 +243,7 @@ exports[`Nav component is displaying with another configuration and can rerender
|
||||
/>
|
||||
<button
|
||||
class="btn btn-plain btn-link clipboard-trigger room-id ellipsis"
|
||||
data-clipboard-text="http://localhost:3000/testRoom_2"
|
||||
data-placement="bottom"
|
||||
data-toggle="tooltip"
|
||||
id="copy-room-url-button"
|
||||
>
|
||||
/testRoom_2
|
||||
</button>
|
||||
@ -268,9 +252,8 @@ exports[`Nav component is displaying with another configuration and can rerender
|
||||
>
|
||||
<button
|
||||
class="lock-room btn btn-link btn-plain"
|
||||
data-placement="bottom"
|
||||
data-toggle="tooltip"
|
||||
title="You must be the owner to lock or unlock the room"
|
||||
data-testid="lock-room-button"
|
||||
id="lock-room-button"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
@ -298,101 +281,270 @@ exports[`Nav component is displaying with another configuration and can rerender
|
||||
</button>
|
||||
</span>
|
||||
<div
|
||||
class="dropdown members-dropdown"
|
||||
class="members-menu"
|
||||
>
|
||||
<a
|
||||
class="dropdown__trigger "
|
||||
<button
|
||||
class="btn btn-link btn-plain members-action"
|
||||
title="Users"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
@ -1,160 +1,167 @@
|
||||
import React, { Component } from 'react';
|
||||
import React 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';
|
||||
|
||||
class Nav extends Component {
|
||||
componentDidMount() {
|
||||
const clip = new Clipboard('.clipboard-trigger');
|
||||
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}`;
|
||||
|
||||
clip.on('success', () => {
|
||||
$('.room-id').tooltip('show');
|
||||
setTimeout(() => {
|
||||
$('.room-id').tooltip('hide');
|
||||
}, 3000);
|
||||
});
|
||||
useClickOutside(userListRef, () => {
|
||||
setShowMemberList(false);
|
||||
});
|
||||
|
||||
$(() => {
|
||||
$('.room-id').tooltip({
|
||||
trigger: 'manual',
|
||||
});
|
||||
$('.lock-room').tooltip({
|
||||
trigger: 'manual',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
$(() => {
|
||||
$('.me-icon-wrap').tooltip();
|
||||
$('.owner-icon-wrap').tooltip();
|
||||
});
|
||||
}
|
||||
|
||||
newRoom() {
|
||||
const newRoom = () => {
|
||||
$('.navbar-collapse').collapse('hide');
|
||||
window.open(`/${nanoid()}`);
|
||||
}
|
||||
};
|
||||
|
||||
handleSettingsClick() {
|
||||
const handleSettingsClick = () => {
|
||||
$('.navbar-collapse').collapse('hide');
|
||||
this.props.openModal('Settings');
|
||||
}
|
||||
openModal('Settings');
|
||||
};
|
||||
|
||||
handleAboutClick() {
|
||||
const handleAboutClick = () => {
|
||||
$('.navbar-collapse').collapse('hide');
|
||||
this.props.openModal('About');
|
||||
}
|
||||
openModal('About');
|
||||
};
|
||||
|
||||
handleToggleLock() {
|
||||
if (!this.props.iAmOwner) {
|
||||
$('.lock-room').tooltip('show');
|
||||
setTimeout(() => $('.lock-room').tooltip('hide'), 3000);
|
||||
return;
|
||||
const handleToggleLock = () => {
|
||||
if (!iAmOwner) {
|
||||
setShowLockedTooltip(true);
|
||||
setTimeout(() => {
|
||||
setShowLockedTooltip(false);
|
||||
}, 2000);
|
||||
} else {
|
||||
toggleLockRoom();
|
||||
}
|
||||
this.props.toggleLockRoom();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<nav className="navbar navbar-expand-md navbar-dark">
|
||||
<div className="meta">
|
||||
<img src={logoImg} alt="Darkwire" className="logo" />
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(roomUrl);
|
||||
setShowCopyTooltip(true);
|
||||
setTimeout(() => {
|
||||
setShowCopyTooltip(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
<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>
|
||||
return (
|
||||
<nav className="navbar navbar-expand-md navbar-dark">
|
||||
<div className="meta">
|
||||
<img src={logoImg} alt="Darkwire" className="logo" />
|
||||
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
id="copy-room-url-button"
|
||||
className="btn btn-plain btn-link clipboard-trigger room-id ellipsis"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<span className="navbar-toggler-icon" />
|
||||
{`/${roomId}`}
|
||||
</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={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>
|
||||
{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>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
</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.propTypes = {
|
||||
members: PropTypes.array.isRequired,
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import RoomLink from '.';
|
||||
import mock$ from 'jquery';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
const mockTooltip = vi.fn().mockImplementation(param => {});
|
||||
|
||||
@ -26,43 +25,31 @@ const mockTranslations = {
|
||||
};
|
||||
|
||||
describe('RoomLink', () => {
|
||||
afterEach(() => {
|
||||
mock$.mockClear();
|
||||
});
|
||||
|
||||
it('should display', async () => {
|
||||
const { asFragment, unmount } = render(<RoomLink roomId="roomId" translations={mockTranslations} />);
|
||||
const { asFragment } = render(<RoomLink roomId="roomId" translations={mockTranslations} />);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
expect(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 () => {
|
||||
// Mock execCommand for paste
|
||||
document.execCommand = vi.fn(() => true);
|
||||
const mockClipboardWriteTest = vi.fn();
|
||||
navigator.clipboard = { writeText: mockClipboardWriteTest };
|
||||
|
||||
const { getByTitle } = render(<RoomLink roomId="roomId" translations={mockTranslations} />);
|
||||
const { getByTestId, queryByText, getByText } = render(
|
||||
<RoomLink roomId="roomId" translations={mockTranslations} />,
|
||||
);
|
||||
|
||||
await fireEvent.click(getByTitle(mockTranslations.copyButtonTooltip));
|
||||
await act(async () => {
|
||||
await fireEvent.click(getByTestId('copy-room-button'));
|
||||
});
|
||||
|
||||
expect(document.execCommand).toHaveBeenLastCalledWith('copy');
|
||||
expect(mock$).toHaveBeenCalledTimes(4);
|
||||
expect(mock$).toHaveBeenLastCalledWith('.copy-room');
|
||||
expect(mockTooltip).toHaveBeenLastCalledWith('show');
|
||||
expect(mockClipboardWriteTest).toHaveBeenLastCalledWith('http://localhost:3000/roomId');
|
||||
|
||||
// Wait for tooltip to close
|
||||
vi.runAllTimers();
|
||||
await getByText(mockTranslations.copyButtonTooltip);
|
||||
|
||||
expect(mock$).toHaveBeenCalledTimes(6);
|
||||
expect(mock$).toHaveBeenLastCalledWith('.copy-room');
|
||||
expect(mockTooltip).toHaveBeenLastCalledWith('hide');
|
||||
// Wait tooltip closing
|
||||
await act(() => vi.runAllTimers());
|
||||
|
||||
expect(queryByText('Copied')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -21,10 +21,8 @@ exports[`RoomLink > should display 1`] = `
|
||||
>
|
||||
<button
|
||||
class="copy-room btn btn-secondary"
|
||||
data-clipboard-text="http://localhost:3000/roomId"
|
||||
data-placement="bottom"
|
||||
data-toggle="tooltip"
|
||||
title="copyButton"
|
||||
data-testid="copy-room-button"
|
||||
id="copy-room"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
|
@ -1,62 +1,61 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Copy } from 'react-feather';
|
||||
import Clipboard from 'clipboard';
|
||||
import $ from 'jquery';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
|
||||
class RoomLink extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
roomUrl: `${window.location.origin}/${props.roomId}`,
|
||||
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;
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
componentDidMount() {
|
||||
const clip = new Clipboard('.copy-room');
|
||||
const handleClick = async () => {
|
||||
await navigator.clipboard.writeText(roomUrl);
|
||||
setShowTooltip(true);
|
||||
setTimeout(() => {
|
||||
if (mountedRef.current) {
|
||||
setShowTooltip(false);
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
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>
|
||||
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>
|
||||
</div>
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
anchorId="copy-room"
|
||||
content={translations.copyButtonTooltip}
|
||||
place="top"
|
||||
events={[]}
|
||||
isOpen={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
RoomLink.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
|
@ -1,7 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
const RoomLocked = ({ modalContent }) => {
|
||||
return <div>{modalContent}</div>;
|
||||
};
|
||||
|
||||
export default class RoomLocked extends Component {
|
||||
render() {
|
||||
return <div>{this.props.modalContent}</div>;
|
||||
}
|
||||
}
|
||||
export default RoomLocked;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
@ -9,10 +8,6 @@ import Settings from '.';
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
const mockTranslations = {
|
||||
sound: 'soundCheck',
|
||||
};
|
||||
|
||||
vi.useFakeTimers();
|
||||
|
||||
vi.mock('@/components/RoomLink');
|
||||
@ -39,11 +34,14 @@ describe('Settings component', () => {
|
||||
<Settings
|
||||
soundIsEnabled={true}
|
||||
toggleSoundEnabled={() => {}}
|
||||
persistenceIsEnabled={true}
|
||||
togglePersistenceEnabled={() => {}}
|
||||
notificationIsEnabled={true}
|
||||
toggleNotificationEnabled={() => {}}
|
||||
toggleNotificationAllowed={vi.fn()}
|
||||
roomId="roomId"
|
||||
setLanguage={() => {}}
|
||||
language="en"
|
||||
translations={{}}
|
||||
/>
|
||||
</Provider>,
|
||||
@ -56,12 +54,15 @@ 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>,
|
||||
@ -77,18 +78,20 @@ 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);
|
||||
@ -105,12 +108,15 @@ 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>,
|
||||
@ -137,12 +143,15 @@ 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>,
|
||||
@ -166,11 +175,14 @@ describe('Settings component', () => {
|
||||
<Settings
|
||||
soundIsEnabled={true}
|
||||
toggleSoundEnabled={() => {}}
|
||||
persistenceIsEnabled={true}
|
||||
togglePersistenceEnabled={() => {}}
|
||||
notificationIsEnabled={true}
|
||||
toggleNotificationEnabled={() => {}}
|
||||
toggleNotificationAllowed={vi.fn()}
|
||||
roomId="roomId"
|
||||
setLanguage={changeLang}
|
||||
language="en"
|
||||
translations={{}}
|
||||
/>
|
||||
</Provider>,
|
||||
|
@ -50,6 +50,7 @@ exports[`Settings component > should display 1`] = `
|
||||
for="persistence-control"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="form-check-input"
|
||||
id="persistence-control"
|
||||
type="checkbox"
|
||||
@ -261,6 +262,7 @@ exports[`Settings component > should display 2`] = `
|
||||
for="persistence-control"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="form-check-input"
|
||||
id="persistence-control"
|
||||
type="checkbox"
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import RoomLink from '@/components/RoomLink';
|
||||
@ -6,193 +5,201 @@ import T from '@/components/T';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
class Settings extends Component {
|
||||
handleSoundToggle() {
|
||||
this.props.toggleSoundEnabled(!this.props.soundIsEnabled);
|
||||
}
|
||||
const Settings = ({
|
||||
soundIsEnabled,
|
||||
persistenceIsEnabled,
|
||||
toggleSoundEnabled,
|
||||
notificationIsEnabled,
|
||||
notificationIsAllowed,
|
||||
toggleNotificationEnabled,
|
||||
toggleNotificationAllowed,
|
||||
togglePersistenceEnabled,
|
||||
roomId,
|
||||
setLanguage,
|
||||
language,
|
||||
translations,
|
||||
}) => {
|
||||
const handleSoundToggle = () => {
|
||||
toggleSoundEnabled(!soundIsEnabled);
|
||||
};
|
||||
|
||||
handlePersistenceToggle() {
|
||||
this.props.togglePersistenceEnabled(!this.props.persistenceIsEnabled);
|
||||
}
|
||||
const handlePersistenceToggle = () => {
|
||||
togglePersistenceEnabled(!persistenceIsEnabled);
|
||||
};
|
||||
|
||||
handleNotificationToggle() {
|
||||
const handleNotificationToggle = () => {
|
||||
Notification.requestPermission().then(permission => {
|
||||
if (permission === 'granted') {
|
||||
this.props.toggleNotificationEnabled(!this.props.notificationIsEnabled);
|
||||
this.props.toggleNotificationAllowed(true);
|
||||
toggleNotificationEnabled(!notificationIsEnabled);
|
||||
toggleNotificationAllowed(true);
|
||||
}
|
||||
if (permission === 'denied') {
|
||||
this.props.toggleNotificationAllowed(false);
|
||||
toggleNotificationAllowed(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleLanguageChange(evt) {
|
||||
const language = evt.target.value;
|
||||
this.props.setLanguage(language);
|
||||
}
|
||||
const handleLanguageChange = evt => {
|
||||
setLanguage(evt.target.value);
|
||||
};
|
||||
|
||||
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>
|
||||
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>
|
||||
</div>
|
||||
</section>
|
||||
<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>
|
||||
<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 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>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
|
||||
@ -6,24 +5,25 @@ import { getTranslations } from '@/i18n';
|
||||
|
||||
const regex = /{(.*?)}/g;
|
||||
|
||||
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;
|
||||
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>;
|
||||
}
|
||||
}
|
||||
return string;
|
||||
};
|
||||
|
||||
T.propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { test, expect } from 'vitest';
|
||||
|
||||
|
@ -1,16 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import randomColor from 'randomcolor';
|
||||
|
||||
class Username extends Component {
|
||||
render() {
|
||||
return (
|
||||
<span className="username" style={{ color: randomColor({ seed: this.props.username, luminosity: 'light' }) }}>
|
||||
{this.props.username}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
const Username = ({ username }) => {
|
||||
return (
|
||||
<span className="username" style={{ color: randomColor({ seed: username, luminosity: 'light' }) }}>
|
||||
{username}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
Username.propTypes = {
|
||||
username: PropTypes.string.isRequired,
|
||||
|
@ -1,47 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import RoomLink from '@/components/RoomLink';
|
||||
|
||||
class Welcome extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
roomUrl: `https://darkwire.io/${props.roomId}`,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
const Welcome = ({ roomId, translations, close }) => {
|
||||
return (
|
||||
<div>
|
||||
<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>
|
||||
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>
|
||||
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={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,
|
||||
|
@ -32,6 +32,7 @@
|
||||
"lista totas las comandas"
|
||||
],
|
||||
"sound": "Son",
|
||||
"persistence": "Memorizar la configuracion",
|
||||
"newMessageNotification": "Notificacion de messatge novèl",
|
||||
"desktopNotification": "Notificacion de burèu",
|
||||
"welcomeModalCTA": "D’acòrdi",
|
||||
|
@ -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/';
|
||||
|
@ -50,7 +50,7 @@ const room = (state = initialState, action) => {
|
||||
return {
|
||||
...state,
|
||||
members: [
|
||||
...state.members,
|
||||
...state.members.filter(({ id })=> id !== action.payload.publicKey.n),
|
||||
{
|
||||
username: action.payload.username,
|
||||
publicKey: action.payload.publicKey,
|
||||
|
@ -1,27 +1,28 @@
|
||||
const initialState = {
|
||||
privateKey: {},
|
||||
publicKey: {},
|
||||
username: '',
|
||||
id: '',
|
||||
};
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
|
||||
const user = (receivedState, action) => {
|
||||
const state = { ...initialState, ...receivedState };
|
||||
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;
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
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 const { createUser,changeUsername } = userSlice.actions
|
||||
|
||||
export default user;
|
||||
export default userSlice.reducer
|
@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
import reducer from './user';
|
||||
import reducer, { createUser, changeUsername} from './user';
|
||||
|
||||
vi.mock('@/i18n', () => {
|
||||
return {
|
||||
@ -15,7 +15,8 @@ describe('User reducer', () => {
|
||||
|
||||
it('should handle CREATE_USER', () => {
|
||||
const payload = { publicKey: { n: 'alicekey' }, username: 'alice' };
|
||||
expect(reducer({}, { type: 'CREATE_USER', payload })).toEqual({
|
||||
|
||||
expect(reducer({},createUser(payload) )).toEqual({
|
||||
id: 'alicekey',
|
||||
publicKey: { n: 'alicekey' },
|
||||
username: 'alice',
|
||||
@ -24,10 +25,8 @@ describe('User reducer', () => {
|
||||
|
||||
it('should handle SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME', () => {
|
||||
const payload = { newUsername: 'alice' };
|
||||
expect(reducer({ username: 'polux' }, { type: 'SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME', payload })).toEqual({
|
||||
id: '',
|
||||
privateKey: {},
|
||||
publicKey: {},
|
||||
|
||||
expect(reducer({ username: 'polux' }, changeUsername(payload))).toEqual({
|
||||
username: 'alice',
|
||||
});
|
||||
});
|
||||
|
@ -1,17 +1,10 @@
|
||||
/* istanbul ignore file */
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
|
||||
import reducers from '@/reducers';
|
||||
|
||||
const composeEnhancers =
|
||||
import.meta.env.NODE_ENV === 'production' ? compose : window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
const enabledMiddlewares = [thunk];
|
||||
|
||||
const middlewares = applyMiddleware(...enabledMiddlewares);
|
||||
|
||||
export default function configureStore(preloadedState) {
|
||||
const store = createStore(reducers, preloadedState, composeEnhancers(middlewares));
|
||||
export default function confStore(preloadedState) {
|
||||
const store = configureStore({reducer:reducers, preloadedState })
|
||||
return store;
|
||||
}
|
||||
|
@ -75,20 +75,19 @@ button.lock-room
|
||||
padding: 0px 1rem
|
||||
background: black
|
||||
|
||||
.members-dropdown
|
||||
margin-left: 10px
|
||||
.dropdown__trigger
|
||||
.members-action
|
||||
margin-right: 5px
|
||||
.dropdown__content
|
||||
.members-menu
|
||||
position: relative
|
||||
.members-action
|
||||
margin-right: 5px
|
||||
.member-list
|
||||
position: absolute
|
||||
background: black
|
||||
border-radius: 3px
|
||||
border: 1px solid $inactive
|
||||
padding: 15px
|
||||
width: 175px
|
||||
left: -10px
|
||||
left: -5px
|
||||
top: 35px
|
||||
ul
|
||||
li
|
||||
overflow: auto
|
||||
height: 25px
|
||||
@ -101,4 +100,4 @@ button.lock-room
|
||||
.icon-container
|
||||
float: right
|
||||
.me-icon, .owner-icon
|
||||
width: 15px
|
||||
width: 15px
|
@ -15,6 +15,7 @@
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
368
client/yarn.lock
368
client/yarn.lock
@ -345,6 +345,18 @@
|
||||
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"
|
||||
@ -477,6 +489,28 @@
|
||||
"@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"
|
||||
@ -895,11 +929,43 @@ 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"
|
||||
@ -1046,20 +1112,11 @@ 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.1.2, classnames@^2.3.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"
|
||||
@ -1216,11 +1273,6 @@ 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"
|
||||
@ -1233,6 +1285,13 @@ 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"
|
||||
@ -1308,6 +1367,37 @@ 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"
|
||||
@ -1322,6 +1412,22 @@ 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"
|
||||
@ -1396,6 +1502,32 @@ 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"
|
||||
@ -1507,7 +1639,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.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
|
||||
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
|
||||
@ -1637,6 +1769,16 @@ function-bind@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
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"
|
||||
@ -1666,6 +1808,14 @@ 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"
|
||||
@ -1716,13 +1866,6 @@ 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"
|
||||
@ -1740,7 +1883,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.1, has-bigints@^1.0.2:
|
||||
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==
|
||||
@ -1839,6 +1982,11 @@ 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"
|
||||
@ -1875,6 +2023,15 @@ 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"
|
||||
@ -1905,7 +2062,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.3, is-callable@^1.1.4, is-callable@^1.2.7:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
|
||||
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
|
||||
@ -1924,7 +2081,7 @@ is-core-module@^2.9.0:
|
||||
dependencies:
|
||||
has "^1.0.3"
|
||||
|
||||
is-date-object@^1.0.5:
|
||||
is-date-object@^1.0.1, is-date-object@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
|
||||
integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
|
||||
@ -1948,6 +2105,11 @@ is-map@^2.0.1, is-map@^2.0.2:
|
||||
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
|
||||
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"
|
||||
@ -1988,14 +2150,21 @@ is-set@^2.0.1, is-set@^2.0.2:
|
||||
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec"
|
||||
integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==
|
||||
|
||||
is-string@^1.0.5:
|
||||
is-shared-array-buffer@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
|
||||
integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==
|
||||
dependencies:
|
||||
call-bind "^1.0.2"
|
||||
|
||||
is-string@^1.0.5, is-string@^1.0.7:
|
||||
version "1.0.7"
|
||||
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.3:
|
||||
is-symbol@^1.0.2, 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==
|
||||
@ -2018,6 +2187,13 @@ 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"
|
||||
@ -2255,6 +2431,14 @@ 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"
|
||||
@ -2441,7 +2625,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.9.0:
|
||||
object-inspect@^1.12.2, 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==
|
||||
@ -2459,7 +2643,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.4:
|
||||
object.assign@^4.1.3, 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==
|
||||
@ -2469,6 +2653,41 @@ 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"
|
||||
@ -2634,7 +2853,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.5.8, prop-types@^15.7.2:
|
||||
prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@ -2753,13 +2972,13 @@ react-router@6.4.4, react-router@^6.4.4:
|
||||
dependencies:
|
||||
"@remix-run/router" "1.0.4"
|
||||
|
||||
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==
|
||||
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==
|
||||
dependencies:
|
||||
classnames "^2.1.2"
|
||||
prop-types "^15.5.8"
|
||||
"@floating-ui/dom" "^1.0.4"
|
||||
classnames "^2.3.2"
|
||||
|
||||
react@^18.2.0:
|
||||
version "18.2.0"
|
||||
@ -2819,6 +3038,11 @@ requires-port@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
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"
|
||||
@ -2833,6 +3057,15 @@ 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"
|
||||
@ -2859,6 +3092,15 @@ 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"
|
||||
@ -2899,11 +3141,6 @@ 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"
|
||||
@ -2984,6 +3221,38 @@ 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"
|
||||
@ -3048,11 +3317,6 @@ 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"
|
||||
@ -3158,6 +3422,16 @@ 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"
|
||||
|
@ -1,7 +1,5 @@
|
||||
# Darkwire.io
|
||||
|
||||
[](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.
|
||||
|
||||
[](https://heroku.com/deploy)
|
||||
@ -86,7 +84,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 enviroment variables available in `server/.env.dist` and `client/.env.dist`. The defaults are available in [Dockerfile](Dockerfile)
|
||||
You are able to use any of the environment variables available in `server/.env.dist` and `client/.env.dist`. The defaults are available in [Dockerfile](Dockerfile)
|
||||
|
||||
### Security
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user