mirror of
https://github.com/zkitter/ui.git
synced 2026-01-08 20:57:59 -05:00
6
package-lock.json
generated
6
package-lock.json
generated
@@ -19999,6 +19999,12 @@
|
||||
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
|
||||
"integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="
|
||||
},
|
||||
"prettier": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
|
||||
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-bytes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.0.0.tgz",
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"dev": "NODE_ENV=development concurrently --kill-others-on-fail npm:dev-ui",
|
||||
"build": "NODE_ENV=production npm run build-ui",
|
||||
"test": "NODE_ENV=test jest --coverage=false",
|
||||
"test:coverage": "NODE_ENV=test jest --coverage"
|
||||
"test:coverage": "NODE_ENV=test jest --coverage",
|
||||
"format:check": "prettier --check 'src'",
|
||||
"format:write": "prettier --write 'src'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@draft-js-plugins/editor": "^4.1.0",
|
||||
@@ -87,6 +89,7 @@
|
||||
"jest": "^27.5.1",
|
||||
"node-loader": "^0.6.0",
|
||||
"node-sass": "^4.13.0",
|
||||
"prettier": "^2.7.1",
|
||||
"process": "^0.11.10",
|
||||
"sass-loader": "^8.0.0",
|
||||
"sinon": "^13.0.1",
|
||||
|
||||
18
prettier.config.js
Normal file
18
prettier.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
arrowParens: 'avoid',
|
||||
bracketSameLine: true,
|
||||
bracketSpacing: true,
|
||||
embeddedLanguageFormatting: 'auto',
|
||||
endOfLine: 'lf',
|
||||
htmlWhitespaceSensitivity: 'css',
|
||||
insertPragma: false,
|
||||
jsxSingleQuote: false,
|
||||
printWidth: 100,
|
||||
proseWrap: 'preserve',
|
||||
quoteProps: 'as-needed',
|
||||
requirePragma: false,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
tabWidth: 2,
|
||||
trailingComma: 'es5',
|
||||
};
|
||||
45
src/app.tsx
45
src/app.tsx
@@ -1,34 +1,31 @@
|
||||
import 'isomorphic-fetch';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import {Provider} from 'react-redux';
|
||||
import {BrowserRouter} from 'react-router-dom';
|
||||
import store from "./store/configureAppStore";
|
||||
import App from "./pages/App";
|
||||
import "./util/gun";
|
||||
import {createServiceWorker} from "./util/sw";
|
||||
import {ThemeProvider} from "./components/ThemeContext";
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import store from './store/configureAppStore';
|
||||
import App from './pages/App';
|
||||
import './util/gun';
|
||||
import { createServiceWorker } from './util/sw';
|
||||
import { ThemeProvider } from './components/ThemeContext';
|
||||
|
||||
(async () => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
await createServiceWorker();
|
||||
}
|
||||
if ('serviceWorker' in navigator) {
|
||||
await createServiceWorker();
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
})();
|
||||
|
||||
|
||||
|
||||
if ((module as any).hot) {
|
||||
(module as any).hot.accept();
|
||||
(module as any).hot.accept();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
.avatar {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,206 +1,195 @@
|
||||
import React, {ReactElement, useEffect, useState} from "react";
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import makeBlockie from 'ethereum-blockies-base64';
|
||||
import classNames from "classnames";
|
||||
import Icon from "../Icon";
|
||||
import {fetchAddressByName, getUser, User, useUser} from "../../ducks/users";
|
||||
import {useDispatch} from "react-redux";
|
||||
import Web3 from "web3";
|
||||
import {getTwitterUser} from "../../util/twitter";
|
||||
import {fetchNameByAddress} from "../../util/web3";
|
||||
import {ellipsify} from "../../util/user";
|
||||
import TwitterBronze from "../../../static/icons/twitter_bronze.svg";
|
||||
import TwitterSilver from "../../../static/icons/twitter_silver.svg";
|
||||
import TwitterGold from "../../../static/icons/twitter_gold.svg";
|
||||
import TwitterUnrated from "../../../static/icons/twitter_unrated.svg";
|
||||
import TAZLogo from "../../../static/icons/taz-logo.svg";
|
||||
import "./avatar.scss";
|
||||
import classNames from 'classnames';
|
||||
import Icon from '../Icon';
|
||||
import { fetchAddressByName, getUser, User, useUser } from '../../ducks/users';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Web3 from 'web3';
|
||||
import { getTwitterUser } from '../../util/twitter';
|
||||
import { fetchNameByAddress } from '../../util/web3';
|
||||
import { ellipsify } from '../../util/user';
|
||||
import TwitterBronze from '../../../static/icons/twitter_bronze.svg';
|
||||
import TwitterSilver from '../../../static/icons/twitter_silver.svg';
|
||||
import TwitterGold from '../../../static/icons/twitter_gold.svg';
|
||||
import TwitterUnrated from '../../../static/icons/twitter_unrated.svg';
|
||||
import TAZLogo from '../../../static/icons/taz-logo.svg';
|
||||
import './avatar.scss';
|
||||
|
||||
type Props = {
|
||||
name?: string;
|
||||
address?: string;
|
||||
className?: string;
|
||||
incognito?: boolean;
|
||||
group?: string | null;
|
||||
semaphoreSignals?: any;
|
||||
twitterUsername?: string;
|
||||
}
|
||||
name?: string;
|
||||
address?: string;
|
||||
className?: string;
|
||||
incognito?: boolean;
|
||||
group?: string | null;
|
||||
semaphoreSignals?: any;
|
||||
twitterUsername?: string;
|
||||
};
|
||||
|
||||
const CACHE: {
|
||||
[address: string]: string;
|
||||
[address: string]: string;
|
||||
} = {};
|
||||
|
||||
const GROUP_TO_PFP: {
|
||||
[group: string]: string;
|
||||
[group: string]: string;
|
||||
} = {
|
||||
'interrep_twitter_not_sufficient': TwitterUnrated,
|
||||
'interrep_twitter_unrated': TwitterUnrated,
|
||||
'interrep_twitter_bronze': TwitterBronze,
|
||||
'interrep_twitter_silver': TwitterSilver,
|
||||
'interrep_twitter_gold': TwitterGold,
|
||||
'semaphore_taz_members': TAZLogo,
|
||||
interrep_twitter_not_sufficient: TwitterUnrated,
|
||||
interrep_twitter_unrated: TwitterUnrated,
|
||||
interrep_twitter_bronze: TwitterBronze,
|
||||
interrep_twitter_silver: TwitterSilver,
|
||||
interrep_twitter_gold: TwitterGold,
|
||||
semaphore_taz_members: TAZLogo,
|
||||
};
|
||||
|
||||
export function Username(props: { address?: string }): ReactElement {
|
||||
const [ensName, setEnsName] = useState('');
|
||||
const {address = ''} = props;
|
||||
const [ensName, setEnsName] = useState('');
|
||||
const { address = '' } = props;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setEnsName('');
|
||||
if (!address) return;
|
||||
const ens = await fetchNameByAddress(address);
|
||||
setEnsName(ens);
|
||||
})();
|
||||
}, [address]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setEnsName('');
|
||||
if (!address) return;
|
||||
const ens = await fetchNameByAddress(address);
|
||||
setEnsName(ens);
|
||||
})();
|
||||
}, [address]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ensName ? ensName : ellipsify(address)}
|
||||
</>
|
||||
);
|
||||
return <>{ensName ? ensName : ellipsify(address)}</>;
|
||||
}
|
||||
|
||||
export default function Avatar(props: Props): ReactElement {
|
||||
const {
|
||||
address,
|
||||
name,
|
||||
incognito,
|
||||
group,
|
||||
twitterUsername,
|
||||
className,
|
||||
} = props;
|
||||
const { address, name, incognito, group, twitterUsername, className } = props;
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [twitterProfileUrl, setTwitterProfileUrl] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [twitterProfileUrl, setTwitterProfileUrl] = useState('');
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [protocol, groupName] = group?.split('_') || [];
|
||||
const dispatch = useDispatch();
|
||||
const [protocol, groupName] = group?.split('_') || [];
|
||||
|
||||
const user = useUser(username);
|
||||
const user = useUser(username);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (groupName) {
|
||||
setUsername(groupName);
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (groupName) {
|
||||
setUsername(groupName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name && !Web3.utils.isAddress(name)) {
|
||||
const addr: any = await dispatch(fetchAddressByName(name));
|
||||
setUsername(addr);
|
||||
} else if (address) {
|
||||
setUsername(address);
|
||||
}
|
||||
})();
|
||||
}, [name, address, groupName]);
|
||||
if (name && !Web3.utils.isAddress(name)) {
|
||||
const addr: any = await dispatch(fetchAddressByName(name));
|
||||
setUsername(addr);
|
||||
} else if (address) {
|
||||
setUsername(address);
|
||||
}
|
||||
})();
|
||||
}, [name, address, groupName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (username) {
|
||||
dispatch(getUser(username));
|
||||
}
|
||||
}, [username]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (twitterUsername) {
|
||||
const data = await getTwitterUser(twitterUsername);
|
||||
setTwitterProfileUrl(data?.profile_image_url);
|
||||
}
|
||||
})();
|
||||
}, [twitterUsername])
|
||||
|
||||
if (twitterUsername) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'inline-block',
|
||||
'rounded-full',
|
||||
'flex-shrink-0 flex-grow-0',
|
||||
'bg-cover bg-center bg-no-repeat',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: `url(${twitterProfileUrl})`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
useEffect(() => {
|
||||
if (username) {
|
||||
dispatch(getUser(username));
|
||||
}
|
||||
}, [username]);
|
||||
|
||||
if (incognito && protocol !== 'custom') {
|
||||
const url = group ? GROUP_TO_PFP[group] : undefined;
|
||||
return (
|
||||
<Icon
|
||||
className={classNames(
|
||||
'inline-flex flex-row flex-nowrap items-center justify-center',
|
||||
'rounded-full',
|
||||
'flex-shrink-0 flex-grow-0',
|
||||
'text-gray-100',
|
||||
'avatar',
|
||||
{
|
||||
'bg-gray-800': !url,
|
||||
'bg-transparent': url,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
fa="fas fa-user-secret"
|
||||
url={url}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'inline-block',
|
||||
'rounded-full',
|
||||
'flex-shrink-0 flex-grow-0',
|
||||
'bg-gray-100',
|
||||
'bg-cover bg-center bg-no-repeat',
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const imageUrl = getImageUrl(user);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (twitterUsername) {
|
||||
const data = await getTwitterUser(twitterUsername);
|
||||
setTwitterProfileUrl(data?.profile_image_url);
|
||||
}
|
||||
})();
|
||||
}, [twitterUsername]);
|
||||
|
||||
if (twitterUsername) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'inline-block',
|
||||
'rounded-full',
|
||||
'flex-shrink-0 flex-grow-0',
|
||||
'bg-cover bg-center bg-no-repeat',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: `url(${imageUrl})`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
<div
|
||||
className={classNames(
|
||||
'inline-block',
|
||||
'rounded-full',
|
||||
'flex-shrink-0 flex-grow-0',
|
||||
'bg-cover bg-center bg-no-repeat',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: `url(${twitterProfileUrl})`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (incognito && protocol !== 'custom') {
|
||||
const url = group ? GROUP_TO_PFP[group] : undefined;
|
||||
return (
|
||||
<Icon
|
||||
className={classNames(
|
||||
'inline-flex flex-row flex-nowrap items-center justify-center',
|
||||
'rounded-full',
|
||||
'flex-shrink-0 flex-grow-0',
|
||||
'text-gray-100',
|
||||
'avatar',
|
||||
{
|
||||
'bg-gray-800': !url,
|
||||
'bg-transparent': url,
|
||||
},
|
||||
className
|
||||
)}
|
||||
fa="fas fa-user-secret"
|
||||
url={url}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'inline-block',
|
||||
'rounded-full',
|
||||
'flex-shrink-0 flex-grow-0',
|
||||
'bg-gray-100',
|
||||
'bg-cover bg-center bg-no-repeat',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const imageUrl = getImageUrl(user);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'inline-block',
|
||||
'rounded-full',
|
||||
'flex-shrink-0 flex-grow-0',
|
||||
'bg-cover bg-center bg-no-repeat',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: `url(${imageUrl})`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function getImageUrl (user: User | null): string {
|
||||
if (!user) return '';
|
||||
export function getImageUrl(user: User | null): string {
|
||||
if (!user) return '';
|
||||
|
||||
let imageUrl = user?.profileImage;
|
||||
let imageUrl = user?.profileImage;
|
||||
|
||||
if (!user?.profileImage && user.username) {
|
||||
imageUrl = CACHE[user.username] ? CACHE[user.username] : makeBlockie(user.username);
|
||||
CACHE[user.username] = imageUrl;
|
||||
}
|
||||
if (!user?.profileImage && user.username) {
|
||||
imageUrl = CACHE[user.username] ? CACHE[user.username] : makeBlockie(user.username);
|
||||
CACHE[user.username] = imageUrl;
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
try {
|
||||
const avatar = new URL(imageUrl);
|
||||
if (avatar.protocol === 'ipfs:') {
|
||||
imageUrl = `https://ipfs.io/ipfs/${avatar.pathname.slice(2)}`;
|
||||
} else {
|
||||
imageUrl = avatar.href;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (imageUrl) {
|
||||
try {
|
||||
const avatar = new URL(imageUrl);
|
||||
if (avatar.protocol === 'ipfs:') {
|
||||
imageUrl = `https://ipfs.io/ipfs/${avatar.pathname.slice(2)}`;
|
||||
} else {
|
||||
imageUrl = avatar.href;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
}
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.dark {
|
||||
.bottom-nav {
|
||||
@@ -25,15 +25,14 @@
|
||||
border-top: 1px solid $gray-100;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
padding: .5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
flex: 1 0 auto;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
transition: background-color 100ms ease-in-out,
|
||||
color 100ms ease-in-out;
|
||||
transition: background-color 100ms ease-in-out, color 100ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: $primary-color;
|
||||
@@ -64,7 +63,7 @@
|
||||
&__unlock-menu {
|
||||
bottom: 3.5rem;
|
||||
top: auto !important;
|
||||
right: .25rem;
|
||||
right: 0.25rem;
|
||||
position: fixed;
|
||||
left: auto !important;
|
||||
}
|
||||
@@ -77,12 +76,11 @@
|
||||
.menuable__menu {
|
||||
bottom: 3.5rem;
|
||||
top: auto !important;
|
||||
right: .25rem;
|
||||
right: 0.25rem;
|
||||
position: fixed;
|
||||
left: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +1,73 @@
|
||||
import React, {ReactElement, useEffect, useState} from "react";
|
||||
import "./bottom-nav.scss";
|
||||
import Web3Button from "../Web3Button";
|
||||
import {useHistory, useLocation} from "react-router";
|
||||
import Icon from "../Icon";
|
||||
import classNames from "classnames";
|
||||
import {useAccount, useGunLoggedIn} from "../../ducks/web3";
|
||||
import {useSelectedLocalId} from "../../ducks/worker";
|
||||
import {fetchNameByAddress} from "../../util/web3";
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import './bottom-nav.scss';
|
||||
import Web3Button from '../Web3Button';
|
||||
import { useHistory, useLocation } from 'react-router';
|
||||
import Icon from '../Icon';
|
||||
import classNames from 'classnames';
|
||||
import { useAccount, useGunLoggedIn } from '../../ducks/web3';
|
||||
import { useSelectedLocalId } from '../../ducks/worker';
|
||||
import { fetchNameByAddress } from '../../util/web3';
|
||||
|
||||
export default function BottomNav(): ReactElement {
|
||||
const loggedIn = useGunLoggedIn();
|
||||
const account = useAccount();
|
||||
const selectedLocalId = useSelectedLocalId();
|
||||
const [ensName, setEnsName] = useState('');
|
||||
const loggedIn = useGunLoggedIn();
|
||||
const account = useAccount();
|
||||
const selectedLocalId = useSelectedLocalId();
|
||||
const [ensName, setEnsName] = useState('');
|
||||
|
||||
let address = '';
|
||||
let address = '';
|
||||
|
||||
if (loggedIn) {
|
||||
address = selectedLocalId?.address || account;
|
||||
}
|
||||
if (loggedIn) {
|
||||
address = selectedLocalId?.address || account;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const ens = await fetchNameByAddress(address);
|
||||
setEnsName(ens);
|
||||
})();
|
||||
}, [address]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const ens = await fetchNameByAddress(address);
|
||||
setEnsName(ens);
|
||||
})();
|
||||
}, [address]);
|
||||
|
||||
return (
|
||||
<div className="bottom-nav">
|
||||
<BottomNavIcon fa="fas fa-home" pathname="/home" disabled={!loggedIn} />
|
||||
{/*<BottomNavIcon fa="fas fa-envelope" pathname={`/${ensName || address}/`} disabled={!loggedIn} />*/}
|
||||
<BottomNavIcon fa="fas fa-envelope" pathname={`/chat`} disabled={!selectedLocalId} />
|
||||
<BottomNavIcon fa="fas fa-globe-asia" pathname="/explore" />
|
||||
<Web3Button className="bottom-nav__web3-icon" />
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="bottom-nav">
|
||||
<BottomNavIcon fa="fas fa-home" pathname="/home" disabled={!loggedIn} />
|
||||
{/*<BottomNavIcon fa="fas fa-envelope" pathname={`/${ensName || address}/`} disabled={!loggedIn} />*/}
|
||||
<BottomNavIcon fa="fas fa-envelope" pathname={`/chat`} disabled={!selectedLocalId} />
|
||||
<BottomNavIcon fa="fas fa-globe-asia" pathname="/explore" />
|
||||
<Web3Button className="bottom-nav__web3-icon" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type BottomNavIconProps = {
|
||||
fa: string;
|
||||
pathname: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
fa: string;
|
||||
pathname: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
function BottomNavIcon(props: BottomNavIconProps): ReactElement {
|
||||
const history = useHistory();
|
||||
const {pathname} = useLocation();
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<Icon
|
||||
className={classNames(
|
||||
'flex', 'flex-row', 'items-center', 'justify-center',
|
||||
'bottom-nav__icon',
|
||||
{
|
||||
'bottom-nav__icon--selected': pathname === props.pathname,
|
||||
'bottom-nav__icon--disabled': props.disabled,
|
||||
}
|
||||
)}
|
||||
onClick={(pathname !== props.pathname && !props.disabled) ? () => history.push(props.pathname) : undefined}
|
||||
fa={props.fa}
|
||||
size={1.125}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Icon
|
||||
className={classNames(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'bottom-nav__icon',
|
||||
{
|
||||
'bottom-nav__icon--selected': pathname === props.pathname,
|
||||
'bottom-nav__icon--disabled': props.disabled,
|
||||
}
|
||||
)}
|
||||
onClick={
|
||||
pathname !== props.pathname && !props.disabled
|
||||
? () => history.push(props.pathname)
|
||||
: undefined
|
||||
}
|
||||
fa={props.fa}
|
||||
size={1.125}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.button {
|
||||
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
}
|
||||
@@ -34,7 +33,7 @@
|
||||
|
||||
&:hover {
|
||||
color: lighten($primary-color, 10);
|
||||
border-color: lighten($primary-color, 10);;
|
||||
border-color: lighten($primary-color, 10);
|
||||
}
|
||||
|
||||
&:active {
|
||||
@@ -52,8 +51,8 @@
|
||||
}
|
||||
|
||||
&--small {
|
||||
padding: .25rem .5rem !important;
|
||||
font-size: .75rem !important;
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 0.75rem !important;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,36 @@
|
||||
import React, {ButtonHTMLAttributes, ReactElement} from "react";
|
||||
import classNames from "classnames";
|
||||
import "./button.scss";
|
||||
import Icon from "../Icon";
|
||||
import SpinnerGif from "../../../static/icons/spinner.gif";
|
||||
import React, { ButtonHTMLAttributes, ReactElement } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import './button.scss';
|
||||
import Icon from '../Icon';
|
||||
import SpinnerGif from '../../../static/icons/spinner.gif';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
btnType?: 'primary' | 'secondary' | '';
|
||||
loading?: boolean;
|
||||
small?: boolean;
|
||||
className?: string;
|
||||
btnType?: 'primary' | 'secondary' | '';
|
||||
loading?: boolean;
|
||||
small?: boolean;
|
||||
} & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
export default function Button(props: Props): ReactElement {
|
||||
const {
|
||||
className,
|
||||
btnType = '',
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
loading,
|
||||
...btnProps
|
||||
} = props;
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'rounded-xl',
|
||||
'flex flex-row flex-nowrap items-center',
|
||||
'h-10 px-4 button transition-colors',
|
||||
{
|
||||
'button--primary': btnType === 'primary',
|
||||
'button--secondary': btnType === 'secondary',
|
||||
'cursor-default': disabled || loading,
|
||||
'button--small': !!props.small,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
onClick={!disabled && !loading ? onClick : undefined}
|
||||
disabled={disabled}
|
||||
{...btnProps}
|
||||
>
|
||||
{loading ? <Icon url={SpinnerGif} size={2} />: children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
const { className, btnType = '', children, onClick, disabled, loading, ...btnProps } = props;
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'rounded-xl',
|
||||
'flex flex-row flex-nowrap items-center',
|
||||
'h-10 px-4 button transition-colors',
|
||||
{
|
||||
'button--primary': btnType === 'primary',
|
||||
'button--secondary': btnType === 'secondary',
|
||||
'cursor-default': disabled || loading,
|
||||
'button--small': !!props.small,
|
||||
},
|
||||
className
|
||||
)}
|
||||
onClick={!disabled && !loading ? onClick : undefined}
|
||||
disabled={disabled}
|
||||
{...btnProps}>
|
||||
{loading ? <Icon url={SpinnerGif} size={2} /> : children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable.scss";
|
||||
@import '../../util/variable.scss';
|
||||
|
||||
.dark {
|
||||
.chat-content {
|
||||
@@ -74,8 +74,8 @@
|
||||
|
||||
&__loading-gif {
|
||||
position: absolute;
|
||||
top: .25rem;
|
||||
left: .25rem;
|
||||
top: 0.25rem;
|
||||
left: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,6 @@
|
||||
&__editor {
|
||||
flex: 1 1 auto;
|
||||
textarea {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,7 +117,7 @@
|
||||
border-radius: 1.25rem;
|
||||
background-color: $gray-100;
|
||||
max-width: 50%;
|
||||
margin: .25rem 1rem;
|
||||
margin: 0.25rem 1rem;
|
||||
//border-bottom-left-radius: 0;
|
||||
|
||||
&--self {
|
||||
@@ -147,6 +146,5 @@
|
||||
}
|
||||
|
||||
&__time {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,279 +1,271 @@
|
||||
import "./chat-content.scss";
|
||||
import React, {ReactElement, useState, KeyboardEvent, useCallback, useEffect} from "react";
|
||||
import classNames from "classnames";
|
||||
import {useParams} from "react-router";
|
||||
import InfiniteScrollable from "../InfiniteScrollable";
|
||||
import {useSelectedLocalId, useSelectedZKGroup} from "../../ducks/worker";
|
||||
import Nickname from "../Nickname";
|
||||
import Avatar, {Username} from "../Avatar";
|
||||
import Textarea from "../Textarea";
|
||||
import {generateECDHKeyPairFromhex, generateZkIdentityFromHex, sha256, signWithP256} from "../../util/crypto";
|
||||
import {FromNow} from "../ChatMenu";
|
||||
import chats, {InflatedChat, useChatId, useChatMessage, useMessagesByChatId, zkchat} from "../../ducks/chats";
|
||||
import Icon from "../Icon";
|
||||
import SpinnerGIF from "../../../static/icons/spinner.gif";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {findProof} from "../../util/merkle";
|
||||
import {Strategy, ZkIdentity} from "@zk-kit/identity";
|
||||
import {Chat} from "../../util/zkchat";
|
||||
import {Identity} from "@semaphore-protocol/identity";
|
||||
import './chat-content.scss';
|
||||
import React, { ReactElement, useState, KeyboardEvent, useCallback, useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useParams } from 'react-router';
|
||||
import InfiniteScrollable from '../InfiniteScrollable';
|
||||
import { useSelectedLocalId, useSelectedZKGroup } from '../../ducks/worker';
|
||||
import Nickname from '../Nickname';
|
||||
import Avatar, { Username } from '../Avatar';
|
||||
import Textarea from '../Textarea';
|
||||
import {
|
||||
generateECDHKeyPairFromhex,
|
||||
generateZkIdentityFromHex,
|
||||
sha256,
|
||||
signWithP256,
|
||||
} from '../../util/crypto';
|
||||
import { FromNow } from '../ChatMenu';
|
||||
import chats, {
|
||||
InflatedChat,
|
||||
useChatId,
|
||||
useChatMessage,
|
||||
useMessagesByChatId,
|
||||
zkchat,
|
||||
} from '../../ducks/chats';
|
||||
import Icon from '../Icon';
|
||||
import SpinnerGIF from '../../../static/icons/spinner.gif';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { findProof } from '../../util/merkle';
|
||||
import { Strategy, ZkIdentity } from '@zk-kit/identity';
|
||||
import { Chat } from '../../util/zkchat';
|
||||
import { Identity } from '@semaphore-protocol/identity';
|
||||
|
||||
export default function ChatContent(): ReactElement {
|
||||
const { chatId } = useParams<{chatId: string}>();
|
||||
const messages = useMessagesByChatId(chatId);
|
||||
const chat = useChatId(chatId);
|
||||
const params = useParams<{chatId: string}>();
|
||||
const { chatId } = useParams<{ chatId: string }>();
|
||||
const messages = useMessagesByChatId(chatId);
|
||||
const chat = useChatId(chatId);
|
||||
const params = useParams<{ chatId: string }>();
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (!chat) return;
|
||||
await zkchat.fetchMessagesByChat(chat);
|
||||
}, [chat])
|
||||
const loadMore = useCallback(async () => {
|
||||
if (!chat) return;
|
||||
await zkchat.fetchMessagesByChat(chat);
|
||||
}, [chat]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMore();
|
||||
}, [loadMore]);
|
||||
useEffect(() => {
|
||||
loadMore();
|
||||
}, [loadMore]);
|
||||
|
||||
if (!chat) return <></>;
|
||||
if (!chat) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('chat-content', {
|
||||
'chat-content--anon': chat?.senderHash,
|
||||
'chat-content--chat-selected': params.chatId,
|
||||
})}>
|
||||
<ChatHeader />
|
||||
<InfiniteScrollable
|
||||
className="chat-content__messages"
|
||||
onScrolledToTop={loadMore}
|
||||
topOffset={128}
|
||||
>
|
||||
{messages.map(messageId => {
|
||||
return (
|
||||
<ChatMessageBubble
|
||||
key={messageId}
|
||||
messageId={messageId}
|
||||
chat={chat}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</InfiniteScrollable>
|
||||
<ChatEditor />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={classNames('chat-content', {
|
||||
'chat-content--anon': chat?.senderHash,
|
||||
'chat-content--chat-selected': params.chatId,
|
||||
})}>
|
||||
<ChatHeader />
|
||||
<InfiniteScrollable
|
||||
className="chat-content__messages"
|
||||
onScrolledToTop={loadMore}
|
||||
topOffset={128}>
|
||||
{messages.map(messageId => {
|
||||
return <ChatMessageBubble key={messageId} messageId={messageId} chat={chat} />;
|
||||
})}
|
||||
</InfiniteScrollable>
|
||||
<ChatEditor />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatHeader(): ReactElement {
|
||||
const { chatId } = useParams<{chatId: string}>();
|
||||
const chat = useChatId(chatId);
|
||||
const { chatId } = useParams<{ chatId: string }>();
|
||||
const chat = useChatId(chatId);
|
||||
|
||||
return (
|
||||
<div className="chat-content__header">
|
||||
<Avatar
|
||||
className="w-10 h-10"
|
||||
address={chat?.receiver}
|
||||
incognito={!chat?.receiver}
|
||||
group={chat?.type === 'DIRECT' ? chat.group : undefined}
|
||||
/>
|
||||
<div className="flex flex-col flex-grow flex-shrink ml-2">
|
||||
<Nickname
|
||||
className="font-bold"
|
||||
address={chat?.receiver}
|
||||
group={chat?.type === 'DIRECT' ? chat.group : undefined}
|
||||
/>
|
||||
<div
|
||||
className={classNames("text-xs", {
|
||||
'text-gray-500': true,
|
||||
})}
|
||||
>
|
||||
{chat?.receiver && (
|
||||
<>
|
||||
<span>@</span>
|
||||
<Username address={chat?.receiver || ''} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className="chat-content__header">
|
||||
<Avatar
|
||||
className="w-10 h-10"
|
||||
address={chat?.receiver}
|
||||
incognito={!chat?.receiver}
|
||||
group={chat?.type === 'DIRECT' ? chat.group : undefined}
|
||||
/>
|
||||
<div className="flex flex-col flex-grow flex-shrink ml-2">
|
||||
<Nickname
|
||||
className="font-bold"
|
||||
address={chat?.receiver}
|
||||
group={chat?.type === 'DIRECT' ? chat.group : undefined}
|
||||
/>
|
||||
<div
|
||||
className={classNames('text-xs', {
|
||||
'text-gray-500': true,
|
||||
})}>
|
||||
{chat?.receiver && (
|
||||
<>
|
||||
<span>@</span>
|
||||
<Username address={chat?.receiver || ''} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatEditor(): ReactElement {
|
||||
const { chatId } = useParams<{chatId: string}>();
|
||||
const selected = useSelectedLocalId();
|
||||
const [content, setContent] = useState('');
|
||||
const chat = useChatId(chatId);
|
||||
const [error, setError] = useState('');
|
||||
const [isSending, setSending] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const zkGroup = useSelectedZKGroup();
|
||||
const { chatId } = useParams<{ chatId: string }>();
|
||||
const selected = useSelectedLocalId();
|
||||
const [content, setContent] = useState('');
|
||||
const chat = useChatId(chatId);
|
||||
const [error, setError] = useState('');
|
||||
const [isSending, setSending] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const zkGroup = useSelectedZKGroup();
|
||||
|
||||
useEffect(() => {
|
||||
setContent('');
|
||||
}, [chatId]);
|
||||
useEffect(() => {
|
||||
setContent('');
|
||||
}, [chatId]);
|
||||
|
||||
const submitMessage = useCallback(async () => {
|
||||
if (!chat || !content) return;
|
||||
const submitMessage = useCallback(async () => {
|
||||
if (!chat || !content) return;
|
||||
|
||||
let signature = '';
|
||||
let merkleProof, identitySecretHash;
|
||||
let signature = '';
|
||||
let merkleProof, identitySecretHash;
|
||||
|
||||
if (selected?.type === 'gun') {
|
||||
signature = signWithP256(selected.privateKey, selected.address) + '.' + selected.address;
|
||||
if (chat.senderHash) {
|
||||
const zkseed = await signWithP256(selected.privateKey, 'signing for zk identity - 0');
|
||||
const zkHex = await sha256(zkseed);
|
||||
const zkIdentity = await generateZkIdentityFromHex(zkHex);
|
||||
merkleProof = await findProof(
|
||||
'zksocial_all',
|
||||
zkIdentity.genIdentityCommitment().toString(16),
|
||||
);
|
||||
identitySecretHash = zkIdentity.getSecretHash();
|
||||
}
|
||||
} else if (selected?.type === 'interrep') {
|
||||
const {type, provider, name, identityCommitment, serializedIdentity} = selected;
|
||||
const group = `${type}_${provider.toLowerCase()}_${name}`;
|
||||
const zkIdentity = new ZkIdentity(Strategy.SERIALIZED, serializedIdentity);
|
||||
merkleProof = await findProof(
|
||||
group,
|
||||
BigInt(identityCommitment).toString(16),
|
||||
);
|
||||
identitySecretHash = zkIdentity.getSecretHash();
|
||||
} else if (selected?.type === 'taz') {
|
||||
const {type, identityCommitment, serializedIdentity} = selected;
|
||||
const group = `semaphore_taz_members`;
|
||||
const zkIdentity = new Identity(serializedIdentity);
|
||||
merkleProof = await findProof(
|
||||
group,
|
||||
BigInt(identityCommitment).toString(16),
|
||||
);
|
||||
}
|
||||
|
||||
const json = await zkchat.sendDirectMessage(
|
||||
chat,
|
||||
content,
|
||||
{
|
||||
'X-SIGNED-ADDRESS': signature,
|
||||
},
|
||||
merkleProof,
|
||||
identitySecretHash,
|
||||
if (selected?.type === 'gun') {
|
||||
signature = signWithP256(selected.privateKey, selected.address) + '.' + selected.address;
|
||||
if (chat.senderHash) {
|
||||
const zkseed = await signWithP256(selected.privateKey, 'signing for zk identity - 0');
|
||||
const zkHex = await sha256(zkseed);
|
||||
const zkIdentity = await generateZkIdentityFromHex(zkHex);
|
||||
merkleProof = await findProof(
|
||||
'zksocial_all',
|
||||
zkIdentity.genIdentityCommitment().toString(16)
|
||||
);
|
||||
identitySecretHash = zkIdentity.getSecretHash();
|
||||
}
|
||||
} else if (selected?.type === 'interrep') {
|
||||
const { type, provider, name, identityCommitment, serializedIdentity } = selected;
|
||||
const group = `${type}_${provider.toLowerCase()}_${name}`;
|
||||
const zkIdentity = new ZkIdentity(Strategy.SERIALIZED, serializedIdentity);
|
||||
merkleProof = await findProof(group, BigInt(identityCommitment).toString(16));
|
||||
identitySecretHash = zkIdentity.getSecretHash();
|
||||
} else if (selected?.type === 'taz') {
|
||||
const { type, identityCommitment, serializedIdentity } = selected;
|
||||
const group = `semaphore_taz_members`;
|
||||
const zkIdentity = new Identity(serializedIdentity);
|
||||
merkleProof = await findProof(group, BigInt(identityCommitment).toString(16));
|
||||
}
|
||||
|
||||
const json = await zkchat.sendDirectMessage(
|
||||
chat,
|
||||
content,
|
||||
{
|
||||
'X-SIGNED-ADDRESS': signature,
|
||||
},
|
||||
merkleProof,
|
||||
identitySecretHash
|
||||
);
|
||||
|
||||
setContent('');
|
||||
}, [content, selected, chat]);
|
||||
setContent('');
|
||||
}, [content, selected, chat]);
|
||||
|
||||
const onClickSend = useCallback(async () => {
|
||||
const onClickSend = useCallback(async () => {
|
||||
setSending(true);
|
||||
setError('');
|
||||
try {
|
||||
await submitMessage();
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}, [submitMessage]);
|
||||
|
||||
const onEnter = useCallback(
|
||||
async (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (window.innerWidth < 768) return;
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
setSending(true);
|
||||
setError('');
|
||||
try {
|
||||
await submitMessage();
|
||||
await submitMessage();
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setSending(false);
|
||||
setSending(false);
|
||||
e.target.focus();
|
||||
}
|
||||
}, [submitMessage]);
|
||||
}
|
||||
},
|
||||
[submitMessage]
|
||||
);
|
||||
|
||||
const onEnter = useCallback(async (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (window.innerWidth < 768) return;
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
setSending(true);
|
||||
setError('');
|
||||
try {
|
||||
await submitMessage();
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setSending(false);
|
||||
e.target.focus();
|
||||
}
|
||||
}
|
||||
}, [submitMessage]);
|
||||
const onChange = useCallback(async (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
setContent(e.target.value);
|
||||
setError('');
|
||||
}, []);
|
||||
|
||||
const onChange = useCallback(async (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
setContent(e.target.value);
|
||||
setError('');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="chat-content__editor-wrapper">
|
||||
{ !!error && <small className="error-message text-xs text-center text-red-500 mb-1 mt-2">{error}</small> }
|
||||
<div className="flex flex-row w-full">
|
||||
<div className="chat-content__editor ml-2">
|
||||
<Textarea
|
||||
key={chatId}
|
||||
className="text-light border mr-2 my-2"
|
||||
// ref={(el) => el?.focus()}
|
||||
rows={Math.max(0, content.split('\n').length)}
|
||||
value={content}
|
||||
onChange={onChange}
|
||||
onKeyPress={onEnter}
|
||||
disabled={isSending}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="relative flex flex-row items-end">
|
||||
{
|
||||
content
|
||||
? (
|
||||
<Icon
|
||||
className={classNames("m-2 text-white justify-center rounded-full", {
|
||||
'opacity-50': isSending,
|
||||
'w-10 h-10': !isSending,
|
||||
'bg-primary-color': !zkGroup,
|
||||
'bg-gray-800': zkGroup,
|
||||
})}
|
||||
onClick={onClickSend}
|
||||
fa={isSending ? undefined : "fas fa-paper-plane"}
|
||||
url={isSending ? SpinnerGIF : undefined}
|
||||
size={isSending ? 2.5 : undefined}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Avatar
|
||||
className={classNames("w-10 h-10 m-2", {
|
||||
'opacity-50': isSending,
|
||||
})}
|
||||
address={selected?.address}
|
||||
incognito={!!chat?.senderHash}
|
||||
group={zkGroup}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className="chat-content__editor-wrapper">
|
||||
{!!error && (
|
||||
<small className="error-message text-xs text-center text-red-500 mb-1 mt-2">{error}</small>
|
||||
)}
|
||||
<div className="flex flex-row w-full">
|
||||
<div className="chat-content__editor ml-2">
|
||||
<Textarea
|
||||
key={chatId}
|
||||
className="text-light border mr-2 my-2"
|
||||
// ref={(el) => el?.focus()}
|
||||
rows={Math.max(0, content.split('\n').length)}
|
||||
value={content}
|
||||
onChange={onChange}
|
||||
onKeyPress={onEnter}
|
||||
disabled={isSending}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatMessageBubble(props: {
|
||||
messageId: string;
|
||||
chat: Chat;
|
||||
}) {
|
||||
const chatMessage = useChatMessage(props.messageId);
|
||||
|
||||
if (chatMessage?.type !== 'DIRECT') return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={chatMessage.messageId}
|
||||
className={classNames("chat-message", {
|
||||
'chat-message--self': chatMessage.sender.ecdh === props.chat.senderECDH,
|
||||
'chat-message--anon': chatMessage.sender.hash,
|
||||
})}
|
||||
>
|
||||
<div className={classNames("chat-message__content text-light", {
|
||||
'italic opacity-70': chatMessage.encryptionError,
|
||||
})}>
|
||||
{chatMessage.encryptionError ? 'Cannot decrypt message' : chatMessage.content}
|
||||
</div>
|
||||
<FromNow
|
||||
className="chat-message__time text-xs mt-2 text-gray-700"
|
||||
timestamp={chatMessage.timestamp}
|
||||
<div className="relative flex flex-row items-end">
|
||||
{content ? (
|
||||
<Icon
|
||||
className={classNames('m-2 text-white justify-center rounded-full', {
|
||||
'opacity-50': isSending,
|
||||
'w-10 h-10': !isSending,
|
||||
'bg-primary-color': !zkGroup,
|
||||
'bg-gray-800': zkGroup,
|
||||
})}
|
||||
onClick={onClickSend}
|
||||
fa={isSending ? undefined : 'fas fa-paper-plane'}
|
||||
url={isSending ? SpinnerGIF : undefined}
|
||||
size={isSending ? 2.5 : undefined}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
className={classNames('w-10 h-10 m-2', {
|
||||
'opacity-50': isSending,
|
||||
})}
|
||||
address={selected?.address}
|
||||
incognito={!!chat?.senderHash}
|
||||
group={zkGroup}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatMessageBubble(props: { messageId: string; chat: Chat }) {
|
||||
const chatMessage = useChatMessage(props.messageId);
|
||||
|
||||
if (chatMessage?.type !== 'DIRECT') return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={chatMessage.messageId}
|
||||
className={classNames('chat-message', {
|
||||
'chat-message--self': chatMessage.sender.ecdh === props.chat.senderECDH,
|
||||
'chat-message--anon': chatMessage.sender.hash,
|
||||
})}>
|
||||
<div
|
||||
className={classNames('chat-message__content text-light', {
|
||||
'italic opacity-70': chatMessage.encryptionError,
|
||||
})}>
|
||||
{chatMessage.encryptionError ? 'Cannot decrypt message' : chatMessage.content}
|
||||
</div>
|
||||
<FromNow
|
||||
className="chat-message__time text-xs mt-2 text-gray-700"
|
||||
timestamp={chatMessage.timestamp}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@import "../../util/variable.scss";
|
||||
|
||||
@import '../../util/variable.scss';
|
||||
|
||||
.dark {
|
||||
.chat-menu {
|
||||
@@ -49,15 +48,15 @@
|
||||
}
|
||||
|
||||
&__item {
|
||||
margin: .25rem .5rem;
|
||||
padding: .5rem;
|
||||
border-radius: .5rem;
|
||||
margin: 0.25rem 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms ease-in-out;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($gray-100, .7);
|
||||
background-color: rgba($gray-100, 0.7);
|
||||
}
|
||||
|
||||
&:active,
|
||||
@@ -83,7 +82,7 @@
|
||||
&__header {
|
||||
@extend %col-nowrap;
|
||||
align-items: center;
|
||||
margin: .5rem;
|
||||
margin: 0.5rem;
|
||||
|
||||
&__r {
|
||||
@extend %row-nowrap;
|
||||
@@ -119,4 +118,4 @@
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,202 +1,201 @@
|
||||
import "./chat-menu.scss";
|
||||
import React, {ChangeEvent, MouseEvent, ReactElement, useCallback, useEffect, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import Avatar, {Username} from "../Avatar";
|
||||
import {useSelectedLocalId, useSelectedZKGroup} from "../../ducks/worker";
|
||||
import moment from "moment";
|
||||
import config from "../../util/config";
|
||||
import Nickname from "../Nickname";
|
||||
import {useHistory, useParams} from "react-router";
|
||||
import {useDispatch} from "react-redux";
|
||||
import chats, {fetchChats, setChats, useChatId, useChatIds, useLastNMessages, zkchat} from "../../ducks/chats";
|
||||
import Icon from "../Icon";
|
||||
import Input from "../Input";
|
||||
import {Chat} from "../../util/zkchat";
|
||||
import Modal, {ModalContent, ModalFooter, ModalHeader} from "../Modal";
|
||||
import Button from "../Button";
|
||||
import {getName} from "../../util/user";
|
||||
import {useUser} from "../../ducks/users";
|
||||
import sse from "../../util/sse";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import './chat-menu.scss';
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
MouseEvent,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Avatar, { Username } from '../Avatar';
|
||||
import { useSelectedLocalId, useSelectedZKGroup } from '../../ducks/worker';
|
||||
import moment from 'moment';
|
||||
import config from '../../util/config';
|
||||
import Nickname from '../Nickname';
|
||||
import { useHistory, useParams } from 'react-router';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import chats, {
|
||||
fetchChats,
|
||||
setChats,
|
||||
useChatId,
|
||||
useChatIds,
|
||||
useLastNMessages,
|
||||
zkchat,
|
||||
} from '../../ducks/chats';
|
||||
import Icon from '../Icon';
|
||||
import Input from '../Input';
|
||||
import { Chat } from '../../util/zkchat';
|
||||
import Modal, { ModalContent, ModalFooter, ModalHeader } from '../Modal';
|
||||
import Button from '../Button';
|
||||
import { getName } from '../../util/user';
|
||||
import { useUser } from '../../ducks/users';
|
||||
import sse from '../../util/sse';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
export default function ChatMenu(): ReactElement {
|
||||
const selected = useSelectedLocalId();
|
||||
const selecteduser = useUser(selected?.address);
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const chatIds = useChatIds();
|
||||
const [showingCreateChat, setShowingCreateChat] = useState(false);
|
||||
const [selectedNewConvo, selectNewConvo] = useState<Chat | null>(null);
|
||||
const [searchParam, setSearchParam] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<Chat[] | null>(null);
|
||||
const params = useParams<{chatId: string}>();
|
||||
const selected = useSelectedLocalId();
|
||||
const selecteduser = useUser(selected?.address);
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const chatIds = useChatIds();
|
||||
const [showingCreateChat, setShowingCreateChat] = useState(false);
|
||||
const [selectedNewConvo, selectNewConvo] = useState<Chat | null>(null);
|
||||
const [searchParam, setSearchParam] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<Chat[] | null>(null);
|
||||
const params = useParams<{ chatId: string }>();
|
||||
|
||||
useEffect(() => {
|
||||
if (selecteduser?.ecdh && selected?.type === 'gun') {
|
||||
setTimeout(() => {
|
||||
dispatch(fetchChats(selecteduser.ecdh));
|
||||
}, 500);
|
||||
} else if (selected?.type === 'interrep' || selected?.type === 'taz') {
|
||||
setTimeout(() => {
|
||||
dispatch(fetchChats(selected.identityCommitment));
|
||||
}, 500);
|
||||
}
|
||||
}, [selected, selecteduser]);
|
||||
useEffect(() => {
|
||||
if (selecteduser?.ecdh && selected?.type === 'gun') {
|
||||
setTimeout(() => {
|
||||
dispatch(fetchChats(selecteduser.ecdh));
|
||||
}, 500);
|
||||
} else if (selected?.type === 'interrep' || selected?.type === 'taz') {
|
||||
setTimeout(() => {
|
||||
dispatch(fetchChats(selected.identityCommitment));
|
||||
}, 500);
|
||||
}
|
||||
}, [selected, selecteduser]);
|
||||
|
||||
const onSearchNewChatChange = useCallback(async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchParam(e.target.value);
|
||||
const res = await fetch(`${config.indexerAPI}/v1/zkchat/chats/search/${e.target.value}`);
|
||||
const json = await res.json();
|
||||
const onSearchNewChatChange = useCallback(async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchParam(e.target.value);
|
||||
const res = await fetch(`${config.indexerAPI}/v1/zkchat/chats/search/${e.target.value}`);
|
||||
const json = await res.json();
|
||||
|
||||
setSearchResults(json.payload.map((data: any) => ({
|
||||
type: 'DIRECT',
|
||||
receiver: data.receiver_address,
|
||||
ecdh: data.receiver_ecdh,
|
||||
})))
|
||||
}, []);
|
||||
setSearchResults(
|
||||
json.payload.map((data: any) => ({
|
||||
type: 'DIRECT',
|
||||
receiver: data.receiver_address,
|
||||
ecdh: data.receiver_ecdh,
|
||||
}))
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (!selected) return <></>;
|
||||
if (!selected) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames("chat-menu", {
|
||||
"chat-menu--chat-selected": params.chatId,
|
||||
})}
|
||||
>
|
||||
<div className="chat-menu__header">
|
||||
<div className="flex flex-row chat-menu__header__r">
|
||||
{
|
||||
showingCreateChat && (
|
||||
<Icon
|
||||
className="chat-menu__create-icon text-gray-400 hover:text-gray-800 py-2 pl-2"
|
||||
fa="fas fa-arrow-left"
|
||||
size={.75}
|
||||
onClick={() => {
|
||||
setShowingCreateChat(false);
|
||||
setSearchParam('');
|
||||
setSearchResults(null);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div
|
||||
className="text-xs font-bold flex-grow ml-2"
|
||||
>
|
||||
{showingCreateChat ? 'Create New Conversation' : 'Conversations' }
|
||||
</div>
|
||||
{
|
||||
!showingCreateChat && (
|
||||
<Icon
|
||||
className="chat-menu__create-icon text-gray-400 hover:text-gray-800 py-2 px-2"
|
||||
fa="fas fa-plus"
|
||||
size={.75}
|
||||
onClick={() => {
|
||||
setShowingCreateChat(true);
|
||||
// @ts-ignore
|
||||
onSearchNewChatChange({ target: { value: '' }});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<Input
|
||||
className="border text-sm chat-menu__search"
|
||||
onChange={showingCreateChat ? onSearchNewChatChange : () => null}
|
||||
value={searchParam}
|
||||
placeholder={!showingCreateChat ? 'Search' : 'Search by name'}
|
||||
>
|
||||
<Icon className="text-gray-400 mx-2" fa="fas fa-search" size={.75} />
|
||||
</Input>
|
||||
</div>
|
||||
{ !!showingCreateChat && (
|
||||
searchResults?.length
|
||||
? searchResults.map((chat) => (
|
||||
<ChatMenuItem
|
||||
key={chat.type + chat.receiver}
|
||||
chatId=""
|
||||
chat={chat}
|
||||
selectNewConvo={selectNewConvo}
|
||||
setCreating={setShowingCreateChat}
|
||||
isCreating={showingCreateChat}
|
||||
hideLastChat
|
||||
/>
|
||||
))
|
||||
: (
|
||||
<div className="text-center text-light text-gray-400 font-semibold my-2">
|
||||
No conversations found
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{ !showingCreateChat && chatIds.map((chatId) => (
|
||||
<ChatMenuItem
|
||||
key={chatId}
|
||||
chatId={chatId}
|
||||
selectNewConvo={selectNewConvo}
|
||||
setCreating={setShowingCreateChat}
|
||||
isCreating={showingCreateChat}
|
||||
/>
|
||||
))}
|
||||
{
|
||||
selectedNewConvo && (
|
||||
<CreateChatOptionModal
|
||||
onClose={() => {
|
||||
selectNewConvo(null);
|
||||
setShowingCreateChat(false);
|
||||
}}
|
||||
chat={selectedNewConvo}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classNames('chat-menu', {
|
||||
'chat-menu--chat-selected': params.chatId,
|
||||
})}>
|
||||
<div className="chat-menu__header">
|
||||
<div className="flex flex-row chat-menu__header__r">
|
||||
{showingCreateChat && (
|
||||
<Icon
|
||||
className="chat-menu__create-icon text-gray-400 hover:text-gray-800 py-2 pl-2"
|
||||
fa="fas fa-arrow-left"
|
||||
size={0.75}
|
||||
onClick={() => {
|
||||
setShowingCreateChat(false);
|
||||
setSearchParam('');
|
||||
setSearchResults(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="text-xs font-bold flex-grow ml-2">
|
||||
{showingCreateChat ? 'Create New Conversation' : 'Conversations'}
|
||||
</div>
|
||||
{!showingCreateChat && (
|
||||
<Icon
|
||||
className="chat-menu__create-icon text-gray-400 hover:text-gray-800 py-2 px-2"
|
||||
fa="fas fa-plus"
|
||||
size={0.75}
|
||||
onClick={() => {
|
||||
setShowingCreateChat(true);
|
||||
// @ts-ignore
|
||||
onSearchNewChatChange({ target: { value: '' } });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
<Input
|
||||
className="border text-sm chat-menu__search"
|
||||
onChange={showingCreateChat ? onSearchNewChatChange : () => null}
|
||||
value={searchParam}
|
||||
placeholder={!showingCreateChat ? 'Search' : 'Search by name'}>
|
||||
<Icon className="text-gray-400 mx-2" fa="fas fa-search" size={0.75} />
|
||||
</Input>
|
||||
</div>
|
||||
{!!showingCreateChat &&
|
||||
(searchResults?.length ? (
|
||||
searchResults.map(chat => (
|
||||
<ChatMenuItem
|
||||
key={chat.type + chat.receiver}
|
||||
chatId=""
|
||||
chat={chat}
|
||||
selectNewConvo={selectNewConvo}
|
||||
setCreating={setShowingCreateChat}
|
||||
isCreating={showingCreateChat}
|
||||
hideLastChat
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center text-light text-gray-400 font-semibold my-2">
|
||||
No conversations found
|
||||
</div>
|
||||
))}
|
||||
{!showingCreateChat &&
|
||||
chatIds.map(chatId => (
|
||||
<ChatMenuItem
|
||||
key={chatId}
|
||||
chatId={chatId}
|
||||
selectNewConvo={selectNewConvo}
|
||||
setCreating={setShowingCreateChat}
|
||||
isCreating={showingCreateChat}
|
||||
/>
|
||||
))}
|
||||
{selectedNewConvo && (
|
||||
<CreateChatOptionModal
|
||||
onClose={() => {
|
||||
selectNewConvo(null);
|
||||
setShowingCreateChat(false);
|
||||
}}
|
||||
chat={selectedNewConvo}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateChatOptionModal(props: {
|
||||
chat: Chat;
|
||||
onClose: () => void;
|
||||
}): ReactElement {
|
||||
const selected = useSelectedLocalId();
|
||||
const user = useUser(selected?.address);
|
||||
const r_user = useUser(props.chat.receiver);
|
||||
const history = useHistory();
|
||||
function CreateChatOptionModal(props: { chat: Chat; onClose: () => void }): ReactElement {
|
||||
const selected = useSelectedLocalId();
|
||||
const user = useUser(selected?.address);
|
||||
const r_user = useUser(props.chat.receiver);
|
||||
const history = useHistory();
|
||||
|
||||
const startChat = useCallback(async (e: MouseEvent<HTMLButtonElement>, isAnon: boolean) => {
|
||||
e.stopPropagation();
|
||||
if (!r_user) return;
|
||||
const chat = await zkchat.createDM(r_user.address, r_user.ecdh, isAnon);
|
||||
props.onClose();
|
||||
const chatId = zkchat.deriveChatId(chat);
|
||||
history.push(`/chat/${chatId}`);
|
||||
}, [props.chat, r_user]);
|
||||
const startChat = useCallback(
|
||||
async (e: MouseEvent<HTMLButtonElement>, isAnon: boolean) => {
|
||||
e.stopPropagation();
|
||||
if (!r_user) return;
|
||||
const chat = await zkchat.createDM(r_user.address, r_user.ecdh, isAnon);
|
||||
props.onClose();
|
||||
const chatId = zkchat.deriveChatId(chat);
|
||||
history.push(`/chat/${chatId}`);
|
||||
},
|
||||
[props.chat, r_user]
|
||||
);
|
||||
|
||||
if (!user) return <></>;
|
||||
if (!user) return <></>;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="w-96"
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{ props.chat.type === 'DIRECT' ? 'Create Direct Message' : 'Create New Group'}
|
||||
</ModalHeader>
|
||||
<ModalFooter className="create-chat-options__footer">
|
||||
<Button
|
||||
className="mr-1 create-chat-options__create-btn"
|
||||
onClick={(e) => startChat(e, false)}
|
||||
>
|
||||
<Avatar className="w-10 h-10" address={user.address} />
|
||||
<div className="ml-2">{`Chat as ${getName(user)}`}</div>
|
||||
</Button>
|
||||
<Button
|
||||
className="mr-1 create-chat-options__create-btn create-chat-options__create-btn--anon"
|
||||
onClick={(e) => startChat(e, true)}
|
||||
>
|
||||
<Avatar className="w-10 h-10 bg-black" incognito />
|
||||
<div className="ml-2">Chat anonymously</div>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Modal className="w-96" onClose={props.onClose}>
|
||||
<ModalHeader>
|
||||
{props.chat.type === 'DIRECT' ? 'Create Direct Message' : 'Create New Group'}
|
||||
</ModalHeader>
|
||||
<ModalFooter className="create-chat-options__footer">
|
||||
<Button className="mr-1 create-chat-options__create-btn" onClick={e => startChat(e, false)}>
|
||||
<Avatar className="w-10 h-10" address={user.address} />
|
||||
<div className="ml-2">{`Chat as ${getName(user)}`}</div>
|
||||
</Button>
|
||||
<Button
|
||||
className="mr-1 create-chat-options__create-btn create-chat-options__create-btn--anon"
|
||||
onClick={e => startChat(e, true)}>
|
||||
<Avatar className="w-10 h-10 bg-black" incognito />
|
||||
<div className="ml-2">Chat anonymously</div>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const ONE_MIN = 60 * 1000;
|
||||
@@ -205,153 +204,132 @@ const ONE_DAY = 24 * ONE_HOUR;
|
||||
const ONE_WEEK = 7 * ONE_DAY;
|
||||
|
||||
function ChatMenuItem(props: {
|
||||
chatId: string;
|
||||
chat?: Chat;
|
||||
hideLastChat?: boolean;
|
||||
selectNewConvo: (chat: Chat) => void;
|
||||
setCreating: (showing: boolean) => void;
|
||||
isCreating: boolean;
|
||||
chatId: string;
|
||||
chat?: Chat;
|
||||
hideLastChat?: boolean;
|
||||
selectNewConvo: (chat: Chat) => void;
|
||||
setCreating: (showing: boolean) => void;
|
||||
isCreating: boolean;
|
||||
}): ReactElement {
|
||||
const selected = useSelectedLocalId();
|
||||
const zkGroup = useSelectedZKGroup();
|
||||
let chat = useChatId(props.chatId);
|
||||
const params = useParams<{chatId: string}>();
|
||||
const history = useHistory();
|
||||
const [last] = useLastNMessages(props.chatId, 1);
|
||||
const theme = useThemeContext();
|
||||
const selected = useSelectedLocalId();
|
||||
const zkGroup = useSelectedZKGroup();
|
||||
let chat = useChatId(props.chatId);
|
||||
const params = useParams<{ chatId: string }>();
|
||||
const history = useHistory();
|
||||
const [last] = useLastNMessages(props.chatId, 1);
|
||||
const theme = useThemeContext();
|
||||
|
||||
const isSelected = props.chatId === params.chatId;
|
||||
const isSelected = props.chatId === params.chatId;
|
||||
|
||||
if (props.chat) {
|
||||
// @ts-ignore
|
||||
chat = props.chat;
|
||||
if (props.chat) {
|
||||
// @ts-ignore
|
||||
chat = props.chat;
|
||||
}
|
||||
|
||||
const r_user = useUser(chat?.receiver);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
if (!chat) return;
|
||||
|
||||
if (!props.isCreating) {
|
||||
history.push(`/chat/${zkchat.deriveChatId(chat)}`);
|
||||
} else {
|
||||
if (!r_user) return;
|
||||
if (['interrep', 'taz'].includes(selected?.type as string)) {
|
||||
const newChat = await zkchat.createDM(r_user.address, r_user.ecdh, true);
|
||||
const chatId = zkchat.deriveChatId(newChat);
|
||||
props.setCreating(false);
|
||||
history.push(`/chat/${chatId}`);
|
||||
} else if (selected?.type === 'gun') {
|
||||
props.selectNewConvo(chat);
|
||||
}
|
||||
}
|
||||
}, [chat, r_user, selected, props.isCreating]);
|
||||
|
||||
const r_user = useUser(chat?.receiver);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
if (!chat) return;
|
||||
|
||||
if (!props.isCreating) {
|
||||
history.push(`/chat/${zkchat.deriveChatId(chat)}`);
|
||||
} else {
|
||||
if (!r_user) return;
|
||||
if (['interrep', 'taz'].includes(selected?.type as string)) {
|
||||
const newChat = await zkchat.createDM(r_user.address, r_user.ecdh, true);
|
||||
const chatId = zkchat.deriveChatId(newChat);
|
||||
props.setCreating(false);
|
||||
history.push(`/chat/${chatId}`);
|
||||
} else if (selected?.type === 'gun') {
|
||||
props.selectNewConvo(chat);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!props.chatId || !chat) return;
|
||||
(async () => {
|
||||
await zkchat.fetchMessagesByChat(chat, 1);
|
||||
if (chat.type === 'DIRECT') {
|
||||
if (chat.senderHash && chat.senderECDH) {
|
||||
await sse.updateTopics([`ecdh:${chat.senderECDH}`]);
|
||||
}
|
||||
}, [chat, r_user, selected, props.isCreating]);
|
||||
}
|
||||
})();
|
||||
}, [props.chatId, chat]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.chatId || !chat) return;
|
||||
(async () => {
|
||||
await zkchat.fetchMessagesByChat(chat, 1);
|
||||
if (chat.type === 'DIRECT') {
|
||||
if (chat.senderHash && chat.senderECDH) {
|
||||
await sse.updateTopics([`ecdh:${chat.senderECDH}`]);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [props.chatId, chat]);
|
||||
if (!chat) return <></>;
|
||||
|
||||
if (!chat) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames("flex flex-row chat-menu__item", {
|
||||
'chat-menu__item--selected': isSelected,
|
||||
'chat-menu__item--anon': chat.type === 'DIRECT' && chat.senderHash,
|
||||
return (
|
||||
<div
|
||||
className={classNames('flex flex-row chat-menu__item', {
|
||||
'chat-menu__item--selected': isSelected,
|
||||
'chat-menu__item--anon': chat.type === 'DIRECT' && chat.senderHash,
|
||||
})}
|
||||
onClick={onClick}>
|
||||
<div className="relative">
|
||||
<Avatar
|
||||
className="w-12 h-12 flex-grow-0 flex-shrink-0"
|
||||
address={chat.receiver || ''}
|
||||
incognito={!chat.receiver}
|
||||
group={chat.type === 'DIRECT' ? chat.group : undefined}
|
||||
/>
|
||||
{chat.type === 'DIRECT' && chat.senderHash && (
|
||||
<Avatar
|
||||
className={classNames('chat-menu__item__anon-marker', {
|
||||
'bg-gray-800': !zkGroup,
|
||||
'bg-white': zkGroup,
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="relative">
|
||||
<Avatar
|
||||
className="w-12 h-12 flex-grow-0 flex-shrink-0"
|
||||
address={chat.receiver || ''}
|
||||
incognito={!chat.receiver}
|
||||
group={chat.type === 'DIRECT' ? chat.group : undefined}
|
||||
/>
|
||||
{
|
||||
chat.type === 'DIRECT' && chat.senderHash && (
|
||||
<Avatar
|
||||
className={classNames("chat-menu__item__anon-marker", {
|
||||
'bg-gray-800': !zkGroup,
|
||||
'bg-white': zkGroup,
|
||||
})}
|
||||
incognito
|
||||
group={zkGroup}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow flex-shrink mx-4 w-0">
|
||||
<Nickname
|
||||
className="font-bold truncate"
|
||||
address={chat.receiver || ''}
|
||||
group={chat.type === 'DIRECT' ? chat.group : undefined}
|
||||
/>
|
||||
{
|
||||
!props.hideLastChat && (
|
||||
<div
|
||||
className={classNames("text-sm truncate", {
|
||||
'text-gray-800': theme !== 'dark',
|
||||
'text-gray-200': theme === 'dark',
|
||||
})}
|
||||
>
|
||||
{last?.content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!props.hideLastChat && (
|
||||
<div
|
||||
className={classNames("flex-grow-0 flex-shrink-0 mt-1 text-gray-500")}
|
||||
>
|
||||
{
|
||||
last?.timestamp && (
|
||||
<FromNow
|
||||
className="text-xs"
|
||||
timestamp={last.timestamp}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
incognito
|
||||
group={zkGroup}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow flex-shrink mx-4 w-0">
|
||||
<Nickname
|
||||
className="font-bold truncate"
|
||||
address={chat.receiver || ''}
|
||||
group={chat.type === 'DIRECT' ? chat.group : undefined}
|
||||
/>
|
||||
{!props.hideLastChat && (
|
||||
<div
|
||||
className={classNames('text-sm truncate', {
|
||||
'text-gray-800': theme !== 'dark',
|
||||
'text-gray-200': theme === 'dark',
|
||||
})}>
|
||||
{last?.content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!props.hideLastChat && (
|
||||
<div className={classNames('flex-grow-0 flex-shrink-0 mt-1 text-gray-500')}>
|
||||
{last?.timestamp && <FromNow className="text-xs" timestamp={last.timestamp} />}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FromNow(props: { timestamp: Date; className?: string }): ReactElement {
|
||||
const now = new Date();
|
||||
const past = props.timestamp.getTime();
|
||||
const diff = now.getTime() - past;
|
||||
|
||||
export function FromNow(props: {
|
||||
timestamp: Date;
|
||||
className?: string;
|
||||
}): ReactElement {
|
||||
const now = new Date();
|
||||
const past = props.timestamp.getTime();
|
||||
const diff = now.getTime() - past;
|
||||
let fromNow = '';
|
||||
|
||||
let fromNow = '';
|
||||
if (diff < ONE_MIN) {
|
||||
fromNow = 'Now';
|
||||
} else if (diff < ONE_HOUR) {
|
||||
fromNow = Math.floor(diff / ONE_MIN) + 'm';
|
||||
} else if (diff < ONE_DAY) {
|
||||
fromNow = Math.floor(diff / ONE_HOUR) + 'h';
|
||||
} else if (diff < ONE_WEEK) {
|
||||
fromNow = Math.floor(diff / ONE_DAY) + 'd';
|
||||
} else if (props.timestamp.getFullYear() === now.getFullYear()) {
|
||||
fromNow = moment(props.timestamp).format('ll').split(',')[0];
|
||||
} else {
|
||||
fromNow = moment(props.timestamp).format('ll');
|
||||
}
|
||||
|
||||
if (diff < ONE_MIN) {
|
||||
fromNow = 'Now';
|
||||
} else if (diff < ONE_HOUR) {
|
||||
fromNow = Math.floor(diff/ONE_MIN) + 'm';
|
||||
} else if (diff < ONE_DAY) {
|
||||
fromNow = Math.floor(diff/ONE_HOUR) + 'h';
|
||||
} else if (diff < ONE_WEEK) {
|
||||
fromNow = Math.floor(diff/ONE_DAY) + 'd';
|
||||
} else if (props.timestamp.getFullYear() === now.getFullYear()) {
|
||||
fromNow = moment(props.timestamp).format('ll').split(',')[0];
|
||||
} else {
|
||||
fromNow = moment(props.timestamp).format('ll');
|
||||
}
|
||||
|
||||
return <div className={props.className}>{fromNow}</div>;
|
||||
}
|
||||
return <div className={props.className}>{fromNow}</div>;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
@import "../../util/variable";
|
||||
|
||||
@import '../../util/variable';
|
||||
|
||||
.dark {
|
||||
.checkbox {
|
||||
&__wrapper {
|
||||
input[type=checkbox]:checked + div {
|
||||
input[type='checkbox']:checked + div {
|
||||
border: 1px solid $gray-400;
|
||||
}
|
||||
|
||||
input[type=checkbox]:checked + div:before {
|
||||
input[type='checkbox']:checked + div:before {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +19,7 @@
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
|
||||
input[type=checkbox] {
|
||||
input[type='checkbox'] {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
@@ -35,21 +34,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
input[type=checkbox] + div {
|
||||
input[type='checkbox'] + div {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: .25rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid $gray-200;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
input[type=checkbox]:checked + div {
|
||||
input[type='checkbox']:checked + div {
|
||||
border: 1px solid $gray-600;
|
||||
}
|
||||
|
||||
input[type=checkbox]:checked + div:before {
|
||||
input[type='checkbox']:checked + div:before {
|
||||
content: '';
|
||||
transform-origin: bottom left;
|
||||
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
||||
@@ -61,7 +60,7 @@
|
||||
background-color: $gray-800;
|
||||
}
|
||||
|
||||
input[type=checkbox]:disabled {
|
||||
input[type='checkbox']:disabled {
|
||||
cursor: default;
|
||||
|
||||
&:hover + div {
|
||||
@@ -69,13 +68,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
input[type=checkbox]:disabled + div {
|
||||
opacity: .5;
|
||||
input[type='checkbox']:disabled + div {
|
||||
opacity: 0.5;
|
||||
background-color: $gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin-left: .5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,22 @@
|
||||
import React, {InputHTMLAttributes, ReactElement, ReactNode} from "react";
|
||||
import classNames from "classnames";
|
||||
import "./checkbox.scss";
|
||||
import React, { InputHTMLAttributes, ReactElement, ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import './checkbox.scss';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
} & InputHTMLAttributes<HTMLInputElement>
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
} & InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export default function Checkbox(props: Props): ReactElement {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
...inputProps
|
||||
} = props;
|
||||
const { className, children, ...inputProps } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap items-center checkbox',
|
||||
className,
|
||||
)}>
|
||||
<div className="checkbox__wrapper">
|
||||
<input
|
||||
{...inputProps}
|
||||
type="checkbox"
|
||||
/>
|
||||
<div className="checkbox__el" />
|
||||
</div>
|
||||
{ children && <div className="text-sm checkbox__description">{children}</div> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={classNames('flex flex-row flex-nowrap items-center checkbox', className)}>
|
||||
<div className="checkbox__wrapper">
|
||||
<input {...inputProps} type="checkbox" />
|
||||
<div className="checkbox__el" />
|
||||
</div>
|
||||
{children && <div className="text-sm checkbox__description">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,97 +1,85 @@
|
||||
import React, {ReactElement, useCallback, useEffect, useState} from "react";
|
||||
import Button from "../Button";
|
||||
import Icon from "../Icon";
|
||||
import config from "../../util/config";
|
||||
import SpinnerGif from "../../../static/icons/spinner.gif";
|
||||
import {useCanNonPostMessage, useLoggedIn} from "../../ducks/web3";
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import Button from '../Button';
|
||||
import Icon from '../Icon';
|
||||
import config from '../../util/config';
|
||||
import SpinnerGif from '../../../static/icons/spinner.gif';
|
||||
import { useCanNonPostMessage, useLoggedIn } from '../../ducks/web3';
|
||||
|
||||
export default function ConnectTwitterButton(props: {
|
||||
className?: string;
|
||||
redirectUrl: string;
|
||||
className?: string;
|
||||
redirectUrl: string;
|
||||
}): ReactElement {
|
||||
const [fetching, setFetching] = useState(true);
|
||||
const [twitterAuth, setTwitterAuth] = useState<{
|
||||
token: string;
|
||||
tokenSecret: string;
|
||||
username: string;
|
||||
}|null>(null);
|
||||
const loggedIn = useLoggedIn();
|
||||
const canPost = useCanNonPostMessage();
|
||||
const [fetching, setFetching] = useState(true);
|
||||
const [twitterAuth, setTwitterAuth] = useState<{
|
||||
token: string;
|
||||
tokenSecret: string;
|
||||
username: string;
|
||||
} | null>(null);
|
||||
const loggedIn = useLoggedIn();
|
||||
const canPost = useCanNonPostMessage();
|
||||
|
||||
useEffect(() => {
|
||||
(async function() {
|
||||
try {
|
||||
const resp = await fetch(`${config.indexerAPI}/twitter/session`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
const json: any = await resp.json();
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
try {
|
||||
const resp = await fetch(`${config.indexerAPI}/twitter/session`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
const json: any = await resp.json();
|
||||
|
||||
if (json?.error) {
|
||||
throw new Error(json?.payload);
|
||||
}
|
||||
|
||||
if (json?.payload) {
|
||||
setTwitterAuth({
|
||||
token: json?.payload.user_token,
|
||||
tokenSecret: json?.payload.user_token_secret,
|
||||
username: json?.payload.username,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setFetching(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const connectTwitter = useCallback(async () => {
|
||||
const resp = await fetch(
|
||||
`${config.indexerAPI}/twitter?redirectUrl=${encodeURI(props.redirectUrl)}`,
|
||||
{
|
||||
credentials: 'include',
|
||||
},
|
||||
);
|
||||
const json = await resp.json();
|
||||
|
||||
if (!json.error && json.payload) {
|
||||
window.location.href = json.payload;
|
||||
if (json?.error) {
|
||||
throw new Error(json?.payload);
|
||||
}
|
||||
}, [props.redirectUrl]);
|
||||
|
||||
if (fetching) {
|
||||
return (
|
||||
<Button
|
||||
btnType="primary"
|
||||
className={props.className}
|
||||
disabled
|
||||
>
|
||||
<Icon url={SpinnerGif} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
if (twitterAuth) {
|
||||
return (
|
||||
<Button
|
||||
btnType="primary"
|
||||
className={props.className}
|
||||
disabled={!loggedIn || !canPost}
|
||||
>
|
||||
<Icon fa="fab fa-twitter" className="mr-2" />
|
||||
Connect {twitterAuth.username}
|
||||
</Button>
|
||||
)
|
||||
if (json?.payload) {
|
||||
setTwitterAuth({
|
||||
token: json?.payload.user_token,
|
||||
tokenSecret: json?.payload.user_token_secret,
|
||||
username: json?.payload.username,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setFetching(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const connectTwitter = useCallback(async () => {
|
||||
const resp = await fetch(
|
||||
`${config.indexerAPI}/twitter?redirectUrl=${encodeURI(props.redirectUrl)}`,
|
||||
{
|
||||
credentials: 'include',
|
||||
}
|
||||
);
|
||||
const json = await resp.json();
|
||||
|
||||
if (!json.error && json.payload) {
|
||||
window.location.href = json.payload;
|
||||
}
|
||||
}, [props.redirectUrl]);
|
||||
|
||||
if (fetching) {
|
||||
return (
|
||||
<Button
|
||||
btnType="primary"
|
||||
className={props.className}
|
||||
onClick={connectTwitter}
|
||||
>
|
||||
<Icon fa="fab fa-twitter" className="mr-2" />
|
||||
Connect Twitter
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button btnType="primary" className={props.className} disabled>
|
||||
<Icon url={SpinnerGif} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (twitterAuth) {
|
||||
return (
|
||||
<Button btnType="primary" className={props.className} disabled={!loggedIn || !canPost}>
|
||||
<Icon fa="fab fa-twitter" className="mr-2" />
|
||||
Connect {twitterAuth.username}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button btnType="primary" className={props.className} onClick={connectTwitter}>
|
||||
<Icon fa="fab fa-twitter" className="mr-2" />
|
||||
Connect Twitter
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,44 @@
|
||||
import React, {ReactElement, useEffect, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import Modal, {ModalHeader, ModalContent} from "../Modal";
|
||||
import {useSelectedLocalId} from "../../ducks/worker";
|
||||
import config from "../../util/config";
|
||||
import {UserRow} from "../DiscoverUserPanel";
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Modal, { ModalHeader, ModalContent } from '../Modal';
|
||||
import { useSelectedLocalId } from '../../ducks/worker';
|
||||
import config from '../../util/config';
|
||||
import { UserRow } from '../DiscoverUserPanel';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
onChange: (groupId: string) => void;
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
onChange: (groupId: string) => void;
|
||||
};
|
||||
|
||||
export default function CustomGroupSelectModal(props: Props): ReactElement {
|
||||
const [groups, setGroups] = useState<string[]>(['zksocial_all']);
|
||||
const selected = useSelectedLocalId();
|
||||
const [groups, setGroups] = useState<string[]>(['zksocial_all']);
|
||||
const selected = useSelectedLocalId();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (selected?.type !== 'gun') return;
|
||||
const resp = await fetch(`${config.indexerAPI}/v1/${selected.address}/groups`);
|
||||
const json = await resp.json();
|
||||
if (!json.error) {
|
||||
setGroups([
|
||||
'zksocial_all',
|
||||
...json.payload.map((group: any) => 'custom_' + group.address),
|
||||
]);
|
||||
}
|
||||
})();
|
||||
}, [selected]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (selected?.type !== 'gun') return;
|
||||
const resp = await fetch(`${config.indexerAPI}/v1/${selected.address}/groups`);
|
||||
const json = await resp.json();
|
||||
if (!json.error) {
|
||||
setGroups(['zksocial_all', ...json.payload.map((group: any) => 'custom_' + group.address)]);
|
||||
}
|
||||
})();
|
||||
}, [selected]);
|
||||
|
||||
if (selected?.type !== 'gun') return <></>;
|
||||
if (selected?.type !== 'gun') return <></>;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={classNames('w-96 custom-group-modal', props.className)}
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<ModalHeader onClose={props.onClose}>
|
||||
You may make a post as...
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<UserRow name={selected.address} onClick={() => props.onChange('')} />
|
||||
{groups.map(groupId => (
|
||||
<UserRow key={groupId} group={groupId} onClick={() => props.onChange(groupId)} />
|
||||
))}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
className={classNames('w-96 custom-group-modal', props.className)}
|
||||
onClose={props.onClose}>
|
||||
<ModalHeader onClose={props.onClose}>You may make a post as...</ModalHeader>
|
||||
<ModalContent>
|
||||
<UserRow name={selected.address} onClick={() => props.onChange('')} />
|
||||
{groups.map(groupId => (
|
||||
<UserRow key={groupId} group={groupId} onClick={() => props.onChange(groupId)} />
|
||||
))}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,84 +1,73 @@
|
||||
import React, {ReactElement, useEffect, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import "./discover-tag.scss";
|
||||
import Icon from "../Icon";
|
||||
import SpinnerGIF from "../../../static/icons/spinner.gif";
|
||||
import {useHistory} from "react-router";
|
||||
import config from "../../util/config";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import './discover-tag.scss';
|
||||
import Icon from '../Icon';
|
||||
import SpinnerGIF from '../../../static/icons/spinner.gif';
|
||||
import { useHistory } from 'react-router';
|
||||
import config from '../../util/config';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
export default function DiscoverTagPanel(): ReactElement {
|
||||
const [tags, setTags] = useState<{ tagName: string; postCount: number }[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const theme = useThemeContext();
|
||||
const [tags, setTags] = useState<{ tagName: string; postCount: number }[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const theme = useThemeContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async function onTagPanelMount() {
|
||||
setLoading(true);
|
||||
const resp = await fetch(`${config.indexerAPI}/v1/tags?limit=5`);
|
||||
const json = await resp.json();
|
||||
useEffect(() => {
|
||||
(async function onTagPanelMount() {
|
||||
setLoading(true);
|
||||
const resp = await fetch(`${config.indexerAPI}/v1/tags?limit=5`);
|
||||
const json = await resp.json();
|
||||
|
||||
if (!json.error) {
|
||||
setTags(json.payload);
|
||||
}
|
||||
if (!json.error) {
|
||||
setTags(json.payload);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap flex-grow border border-transparent rounded-xl mt-2',
|
||||
'meta-group meta-group--alt discover-user',
|
||||
{
|
||||
"bg-gray-100": theme !== 'dark',
|
||||
"bg-gray-900": theme === 'dark',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames("px-4 py-2 font-bold text-lg border-b", {
|
||||
"border-gray-200": theme !== 'dark',
|
||||
"border-gray-800": theme === 'dark',
|
||||
})}
|
||||
>
|
||||
Discover Tags
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap py-1">
|
||||
{ loading && <Icon className="self-center my-4" url={SpinnerGIF} size={3} /> }
|
||||
{tags.map(({tagName, postCount}) => (
|
||||
<TagRow
|
||||
key={tagName}
|
||||
tagName={tagName}
|
||||
postCount={postCount}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap flex-grow border border-transparent rounded-xl mt-2',
|
||||
'meta-group meta-group--alt discover-user',
|
||||
{
|
||||
'bg-gray-100': theme !== 'dark',
|
||||
'bg-gray-900': theme === 'dark',
|
||||
}
|
||||
)}>
|
||||
<div
|
||||
className={classNames('px-4 py-2 font-bold text-lg border-b', {
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}>
|
||||
Discover Tags
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap py-1">
|
||||
{loading && <Icon className="self-center my-4" url={SpinnerGIF} size={3} />}
|
||||
{tags.map(({ tagName, postCount }) => (
|
||||
<TagRow key={tagName} tagName={tagName} postCount={postCount} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TagRow(props: { tagName: string; postCount: number }): ReactElement {
|
||||
const history = useHistory();
|
||||
const theme = useThemeContext();
|
||||
const history = useHistory();
|
||||
const theme = useThemeContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-nowrap px-4 py-2 cursor-pointer",
|
||||
"items-center",
|
||||
{
|
||||
"hover:bg-gray-200": theme !== 'dark',
|
||||
"hover:bg-gray-800": theme === 'dark',
|
||||
}
|
||||
)}
|
||||
onClick={() => history.push(`/tag/${encodeURIComponent(props.tagName)}/`)}
|
||||
>
|
||||
<div className="flex flex-col flex-nowrap justify-center">
|
||||
<div className="font-bold text-md hover:underline">{props.tagName}</div>
|
||||
<div className="text-xs text-gray-500">{props.postCount} Posts</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classNames('flex flex-row flex-nowrap px-4 py-2 cursor-pointer', 'items-center', {
|
||||
'hover:bg-gray-200': theme !== 'dark',
|
||||
'hover:bg-gray-800': theme === 'dark',
|
||||
})}
|
||||
onClick={() => history.push(`/tag/${encodeURIComponent(props.tagName)}/`)}>
|
||||
<div className="flex flex-col flex-nowrap justify-center">
|
||||
<div className="font-bold text-md hover:underline">{props.tagName}</div>
|
||||
<div className="text-xs text-gray-500">{props.postCount} Posts</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.discover-user {
|
||||
height: fit-content;
|
||||
@@ -7,4 +7,4 @@
|
||||
@media only screen and (max-width: 768px) {
|
||||
width: 16rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,119 +1,118 @@
|
||||
import React, {ReactElement, useCallback, useEffect, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {fetchAddressByName, fetchUsers, useUser} from "../../ducks/users";
|
||||
import Avatar from "../Avatar";
|
||||
import "./discover-user.scss";
|
||||
import Icon from "../Icon";
|
||||
import SpinnerGIF from "../../../static/icons/spinner.gif";
|
||||
import {useHistory} from "react-router";
|
||||
import {getName, getHandle} from "../../util/user";
|
||||
import Web3 from "web3";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import Nickname from "../Nickname";
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { fetchAddressByName, fetchUsers, useUser } from '../../ducks/users';
|
||||
import Avatar from '../Avatar';
|
||||
import './discover-user.scss';
|
||||
import Icon from '../Icon';
|
||||
import SpinnerGIF from '../../../static/icons/spinner.gif';
|
||||
import { useHistory } from 'react-router';
|
||||
import { getName, getHandle } from '../../util/user';
|
||||
import Web3 from 'web3';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
import Nickname from '../Nickname';
|
||||
|
||||
export default function DiscoverUserPanel(): ReactElement {
|
||||
const [users, setUsers] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const theme = useThemeContext();
|
||||
const [users, setUsers] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const theme = useThemeContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async function onUserPanelMount() {
|
||||
setLoading(true);
|
||||
const list = await dispatch(fetchUsers());
|
||||
setUsers(list as any);
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
(async function onUserPanelMount() {
|
||||
setLoading(true);
|
||||
const list = await dispatch(fetchUsers());
|
||||
setUsers(list as any);
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap flex-grow border border-transparent rounded-xl mt-2',
|
||||
'meta-group meta-group--alt discover-user',
|
||||
{
|
||||
"bg-gray-100": theme !== 'dark',
|
||||
"bg-gray-900": theme === 'dark',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames("px-4 py-2 font-bold text-lg border-b", {
|
||||
"border-gray-200": theme !== 'dark',
|
||||
"border-gray-800": theme === 'dark',
|
||||
})}
|
||||
>
|
||||
Discover Users
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap py-1">
|
||||
{ loading && <Icon className="self-center my-4" url={SpinnerGIF} size={3} /> }
|
||||
{users.map(ens => <UserRow key={ens} name={ens} />)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap flex-grow border border-transparent rounded-xl mt-2',
|
||||
'meta-group meta-group--alt discover-user',
|
||||
{
|
||||
'bg-gray-100': theme !== 'dark',
|
||||
'bg-gray-900': theme === 'dark',
|
||||
}
|
||||
)}>
|
||||
<div
|
||||
className={classNames('px-4 py-2 font-bold text-lg border-b', {
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}>
|
||||
Discover Users
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap py-1">
|
||||
{loading && <Icon className="self-center my-4" url={SpinnerGIF} size={3} />}
|
||||
{users.map(ens => (
|
||||
<UserRow key={ens} name={ens} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function UserRow(props: {name?: string; group?: string; onClick?: () => void}): ReactElement {
|
||||
const history = useHistory();
|
||||
const [username, setUsername] = useState('');
|
||||
export function UserRow(props: {
|
||||
name?: string;
|
||||
group?: string;
|
||||
onClick?: () => void;
|
||||
}): ReactElement {
|
||||
const history = useHistory();
|
||||
const [username, setUsername] = useState('');
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const user = useUser(username);
|
||||
const theme = useThemeContext();
|
||||
const dispatch = useDispatch();
|
||||
const user = useUser(username);
|
||||
const theme = useThemeContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!props.name) return;
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!props.name) return;
|
||||
|
||||
if (!Web3.utils.isAddress(props.name)) {
|
||||
const address: any = await dispatch(fetchAddressByName(props.name));
|
||||
setUsername(address);
|
||||
} else {
|
||||
setUsername(props.name);
|
||||
}
|
||||
})();
|
||||
}, [props.name]);
|
||||
if (!Web3.utils.isAddress(props.name)) {
|
||||
const address: any = await dispatch(fetchAddressByName(props.name));
|
||||
setUsername(address);
|
||||
} else {
|
||||
setUsername(props.name);
|
||||
}
|
||||
})();
|
||||
}, [props.name]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (user) {
|
||||
history.push(`/${user.ens || user.address}/`)
|
||||
const onClick = useCallback(() => {
|
||||
if (user) {
|
||||
history.push(`/${user.ens || user.address}/`);
|
||||
}
|
||||
}, [user, props.group]);
|
||||
|
||||
if (!user && !props.group) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap px-4 py-2 cursor-pointer',
|
||||
'items-center transition',
|
||||
{
|
||||
'hover:bg-gray-200': theme !== 'dark',
|
||||
'hover:bg-gray-800': theme === 'dark',
|
||||
}
|
||||
}, [user, props.group]);
|
||||
|
||||
if (!user && !props.group) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-nowrap px-4 py-2 cursor-pointer",
|
||||
"items-center transition",
|
||||
{
|
||||
"hover:bg-gray-200": theme !== 'dark',
|
||||
"hover:bg-gray-800": theme === 'dark',
|
||||
}
|
||||
)}
|
||||
onClick={props.onClick || onClick}
|
||||
>
|
||||
<Avatar
|
||||
address={user?.address}
|
||||
group={props.group}
|
||||
className="w-10 h-10 mr-3"
|
||||
incognito={!!props.group}
|
||||
/>
|
||||
<div className="flex flex-col flex-nowrap justify-center">
|
||||
{ props.group && <Nickname group={props.group} className="font-semibold text-sm" /> }
|
||||
{ user && (
|
||||
<>
|
||||
<div className="font-bold text-md hover:underline">
|
||||
{getName(user, 8, 6)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
@{getHandle(user, 8, 6)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)}
|
||||
onClick={props.onClick || onClick}>
|
||||
<Avatar
|
||||
address={user?.address}
|
||||
group={props.group}
|
||||
className="w-10 h-10 mr-3"
|
||||
incognito={!!props.group}
|
||||
/>
|
||||
<div className="flex flex-col flex-nowrap justify-center">
|
||||
{props.group && <Nickname group={props.group} className="font-semibold text-sm" />}
|
||||
{user && (
|
||||
<>
|
||||
<div className="font-bold text-md hover:underline">{getName(user, 8, 6)}</div>
|
||||
<div className="text-sm text-gray-500">@{getHandle(user, 8, 6)}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,78 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
//@import '/node_modules/@draft-js-plugins/mention/lib/plugin.css';
|
||||
|
||||
.m6zwb4v, .m6zwb4v:visited, .hngfxw3 {
|
||||
.m6zwb4v,
|
||||
.m6zwb4v:visited,
|
||||
.hngfxw3 {
|
||||
color: $primary-color;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.mnw6qvm{border:1px solid #eee;position:absolute;min-width:220px;max-width:440px;background:#fff;border-radius:2px;box-shadow:0px 4px 30px 0px rgba(220,220,220,1);cursor:pointer;padding-top:8px;padding-bottom:8px;z-index:2;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;box-sizing:border-box;-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);}
|
||||
.m1ymsnxd{opacity:0;-webkit-transition:opacity 0.25s cubic-bezier(0.3,1.2,0.2,1);transition:opacity 0.25s cubic-bezier(0.3,1.2,0.2,1);}
|
||||
.m126ak5t{opacity:1;}
|
||||
.mtiwdxc{padding:7px 10px 3px 10px;-webkit-transition:background-color 0.4s cubic-bezier(.27,1.27,.48,.56);transition:background-color 0.4s cubic-bezier(.27,1.27,.48,.56);}.mtiwdxc:active{background-color:#cce7ff;}
|
||||
.myz2dw1{padding:7px 10px 3px 10px;-webkit-transition:background-color 0.4s cubic-bezier(.27,1.27,.48,.56);transition:background-color 0.4s cubic-bezier(.27,1.27,.48,.56);background-color:#e6f3ff;}.myz2dw1:active{background-color:#cce7ff;}
|
||||
.mpqdcgq{display:inline-block;margin-left:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:368px;font-size:0.9em;margin-bottom:0.2em;}
|
||||
.m1mfvffo{display:inline-block;width:24px;height:24px;border-radius:12px;}
|
||||
.mnw6qvm {
|
||||
border: 1px solid #eee;
|
||||
position: absolute;
|
||||
min-width: 220px;
|
||||
max-width: 440px;
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0px 4px 30px 0px rgba(220, 220, 220, 1);
|
||||
cursor: pointer;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
z-index: 2;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
-webkit-transform: scale(0);
|
||||
-ms-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
.m1ymsnxd {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.25s cubic-bezier(0.3, 1.2, 0.2, 1);
|
||||
transition: opacity 0.25s cubic-bezier(0.3, 1.2, 0.2, 1);
|
||||
}
|
||||
.m126ak5t {
|
||||
opacity: 1;
|
||||
}
|
||||
.mtiwdxc {
|
||||
padding: 7px 10px 3px 10px;
|
||||
-webkit-transition: background-color 0.4s cubic-bezier(0.27, 1.27, 0.48, 0.56);
|
||||
transition: background-color 0.4s cubic-bezier(0.27, 1.27, 0.48, 0.56);
|
||||
}
|
||||
.mtiwdxc:active {
|
||||
background-color: #cce7ff;
|
||||
}
|
||||
.myz2dw1 {
|
||||
padding: 7px 10px 3px 10px;
|
||||
-webkit-transition: background-color 0.4s cubic-bezier(0.27, 1.27, 0.48, 0.56);
|
||||
transition: background-color 0.4s cubic-bezier(0.27, 1.27, 0.48, 0.56);
|
||||
background-color: #e6f3ff;
|
||||
}
|
||||
.myz2dw1:active {
|
||||
background-color: #cce7ff;
|
||||
}
|
||||
.mpqdcgq {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 368px;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
.m1mfvffo {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.mnw6qvm {
|
||||
max-height: 12rem;
|
||||
|
||||
@@ -1,373 +1,369 @@
|
||||
import React, {ReactElement, useCallback, useEffect, useMemo, useState} from "react";
|
||||
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
CompositeDecorator, ContentBlock, ContentState, convertFromRaw, convertToRaw,
|
||||
DefaultDraftBlockRenderMap, EditorState,
|
||||
} from "draft-js";
|
||||
import DraftJSPluginEditor, {PluginEditorProps} from "@draft-js-plugins/editor";
|
||||
CompositeDecorator,
|
||||
ContentBlock,
|
||||
ContentState,
|
||||
convertFromRaw,
|
||||
convertToRaw,
|
||||
DefaultDraftBlockRenderMap,
|
||||
EditorState,
|
||||
} from 'draft-js';
|
||||
import DraftJSPluginEditor, { PluginEditorProps } from '@draft-js-plugins/editor';
|
||||
import createLinkifyPlugin from '@draft-js-plugins/linkify';
|
||||
import createHashtagPlugin from '@draft-js-plugins/hashtag';
|
||||
import createMentionPlugin, {
|
||||
defaultSuggestionsFilter, MentionData,
|
||||
defaultSuggestionsFilter,
|
||||
MentionData,
|
||||
} from '@draft-js-plugins/mention';
|
||||
const TableUtils = require('draft-js-table');
|
||||
const { linkify } = require("remarkable/linkify");
|
||||
const { linkify } = require('remarkable/linkify');
|
||||
const { markdownToDraft } = require('markdown-draft-js');
|
||||
import "./draft-js-editor.scss";
|
||||
import {useHistory} from "react-router";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {fetchAddressByName, getUser, searchUsers, useUser} from "../../ducks/users";
|
||||
import Avatar from "../Avatar";
|
||||
import classNames from "classnames";
|
||||
import debounce from "lodash.debounce";
|
||||
import {getHandle, getName, getUsername} from "../../util/user";
|
||||
import Web3 from "web3";
|
||||
import './draft-js-editor.scss';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { fetchAddressByName, getUser, searchUsers, useUser } from '../../ducks/users';
|
||||
import Avatar from '../Avatar';
|
||||
import classNames from 'classnames';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { getHandle, getName, getUsername } from '../../util/user';
|
||||
import Web3 from 'web3';
|
||||
|
||||
let searchNonce = 0;
|
||||
let searchTimeout: any = null;
|
||||
|
||||
export function DraftEditor(props: PluginEditorProps): ReactElement {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { MentionSuggestions, plugins, blockRenderMap } = useMemo(() => {
|
||||
const blockRenderMap = DefaultDraftBlockRenderMap.merge(TableUtils.DraftBlockRenderMap);
|
||||
const linkifyPlugin = createLinkifyPlugin();
|
||||
const hashtagPlugin = createHashtagPlugin();
|
||||
const mentionPlugin = createMentionPlugin({
|
||||
mentionRegExp,
|
||||
const { MentionSuggestions, plugins, blockRenderMap } = useMemo(() => {
|
||||
const blockRenderMap = DefaultDraftBlockRenderMap.merge(TableUtils.DraftBlockRenderMap);
|
||||
const linkifyPlugin = createLinkifyPlugin();
|
||||
const hashtagPlugin = createHashtagPlugin();
|
||||
const mentionPlugin = createMentionPlugin({
|
||||
mentionRegExp,
|
||||
});
|
||||
const { MentionSuggestions } = mentionPlugin;
|
||||
return {
|
||||
plugins: [linkifyPlugin, hashtagPlugin, mentionPlugin],
|
||||
MentionSuggestions,
|
||||
blockRenderMap,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [suggestions, setSuggestions] = useState<MentionData[]>([]);
|
||||
|
||||
const onOpenChange = useCallback((_open: boolean) => {
|
||||
setOpen(_open);
|
||||
}, []);
|
||||
|
||||
const onSearchChange = useCallback(async ({ value }: { value: string }) => {
|
||||
searchNonce++;
|
||||
const oldNonce = searchNonce;
|
||||
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = null;
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(async () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = null;
|
||||
const result: any = await dispatch(searchUsers(value));
|
||||
if (oldNonce !== searchNonce) return;
|
||||
const suggestions = [];
|
||||
|
||||
for (let r of result) {
|
||||
suggestions.push({
|
||||
name: '@' + (r.ens || r.address),
|
||||
address: r.address,
|
||||
profileImage: r.profileImage,
|
||||
nickname: r.name,
|
||||
});
|
||||
const { MentionSuggestions } = mentionPlugin;
|
||||
return {
|
||||
plugins: [linkifyPlugin, hashtagPlugin, mentionPlugin],
|
||||
MentionSuggestions,
|
||||
blockRenderMap,
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [suggestions, setSuggestions] = useState<MentionData[]>([]);
|
||||
setSuggestions(suggestions);
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
const onOpenChange = useCallback((_open: boolean) => {
|
||||
setOpen(_open);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
return function () {
|
||||
if (props.onChange) {
|
||||
props.onChange(EditorState.createEmpty());
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onSearchChange = useCallback(async ({ value }: { value: string }) => {
|
||||
searchNonce++;
|
||||
const oldNonce = searchNonce;
|
||||
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = null;
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(async () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = null;
|
||||
const result: any = await dispatch(searchUsers(value));
|
||||
if (oldNonce !== searchNonce) return;
|
||||
const suggestions = [];
|
||||
|
||||
for (let r of result) {
|
||||
suggestions.push({
|
||||
name: '@' + (r.ens || r.address),
|
||||
address: r.address,
|
||||
profileImage: r.profileImage,
|
||||
nickname: r.name,
|
||||
});
|
||||
}
|
||||
|
||||
setSuggestions(suggestions);
|
||||
}, 500);
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return function() {
|
||||
if (props.onChange) {
|
||||
props.onChange(EditorState.createEmpty());
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DraftJSPluginEditor
|
||||
blockRenderMap={blockRenderMap}
|
||||
customStyleMap={{
|
||||
CODE: {
|
||||
backgroundColor: '#f6f6f6',
|
||||
color: '#1c1e21',
|
||||
padding: '2px 4px',
|
||||
margin: '0 2px',
|
||||
borderRadius: '2px',
|
||||
fontFamily: 'Roboto Mono, monospace',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
plugins={plugins}
|
||||
onChange={editorState => {
|
||||
if (props.onChange && !props.readOnly) {
|
||||
props.onChange(editorState);
|
||||
return editorState;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<MentionSuggestions
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
suggestions={suggestions}
|
||||
onSearchChange={onSearchChange}
|
||||
// @ts-ignore
|
||||
entryComponent={Entry}
|
||||
onAddMention={() => {
|
||||
// get the mention object selected
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<DraftJSPluginEditor
|
||||
blockRenderMap={blockRenderMap}
|
||||
customStyleMap={{
|
||||
CODE: {
|
||||
backgroundColor: '#f6f6f6',
|
||||
color: '#1c1e21',
|
||||
padding: '2px 4px',
|
||||
margin: '0 2px',
|
||||
borderRadius: '2px',
|
||||
fontFamily: 'Roboto Mono, monospace',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
plugins={plugins}
|
||||
onChange={editorState => {
|
||||
if (props.onChange && !props.readOnly) {
|
||||
props.onChange(editorState);
|
||||
return editorState;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<MentionSuggestions
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
suggestions={suggestions}
|
||||
onSearchChange={onSearchChange}
|
||||
// @ts-ignore
|
||||
entryComponent={Entry}
|
||||
onAddMention={() => {
|
||||
// get the mention object selected
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const decorator = new CompositeDecorator([
|
||||
{
|
||||
strategy: findLinkEntities,
|
||||
component: (props: any) => {
|
||||
const {url} = props.contentState.getEntity(props.entityKey).getData();
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{props.children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
{
|
||||
strategy: findLinkEntities,
|
||||
component: (props: any) => {
|
||||
const { url } = props.contentState.getEntity(props.entityKey).getData();
|
||||
return (
|
||||
<a href={url} target="_blank" onClick={e => e.stopPropagation()}>
|
||||
{props.children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
{
|
||||
strategy: findHashtagEntities,
|
||||
component: (props: any) => {
|
||||
const history = useHistory();
|
||||
return (
|
||||
<a
|
||||
className="hashtag"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
history.push(`/tag/${encodeURIComponent(props.decoratedText)}`)
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
strategy: findHashtagEntities,
|
||||
component: (props: any) => {
|
||||
const history = useHistory();
|
||||
return (
|
||||
<a
|
||||
className="hashtag"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
history.push(`/tag/${encodeURIComponent(props.decoratedText)}`);
|
||||
}}>
|
||||
{props.children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
{
|
||||
strategy: findMentionEntities,
|
||||
component: (props: any) => {
|
||||
const history = useHistory();
|
||||
const nameOrAddress = props.decoratedText.slice(1);
|
||||
const [username, setUsername] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
},
|
||||
{
|
||||
strategy: findMentionEntities,
|
||||
component: (props: any) => {
|
||||
const history = useHistory();
|
||||
const nameOrAddress = props.decoratedText.slice(1);
|
||||
const [username, setUsername] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!Web3.utils.isAddress(nameOrAddress)) {
|
||||
const address: any = await dispatch(fetchAddressByName(nameOrAddress));
|
||||
setUsername(address);
|
||||
dispatch(getUser(address));
|
||||
} else {
|
||||
setUsername(nameOrAddress);
|
||||
dispatch(getUser(nameOrAddress));
|
||||
}
|
||||
})();
|
||||
}, [nameOrAddress]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!Web3.utils.isAddress(nameOrAddress)) {
|
||||
const address: any = await dispatch(fetchAddressByName(nameOrAddress));
|
||||
setUsername(address);
|
||||
dispatch(getUser(address));
|
||||
} else {
|
||||
setUsername(nameOrAddress);
|
||||
dispatch(getUser(nameOrAddress));
|
||||
}
|
||||
})();
|
||||
}, [nameOrAddress]);
|
||||
|
||||
const user = useUser(username);
|
||||
const user = useUser(username);
|
||||
|
||||
if (!user?.address || user?.address === "0x0000000000000000000000000000000000000000") {
|
||||
return <div className="hashtag">@{nameOrAddress}</div>;
|
||||
}
|
||||
if (!user?.address || user?.address === '0x0000000000000000000000000000000000000000') {
|
||||
return <div className="hashtag">@{nameOrAddress}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
className="hashtag"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
history.push(`/${encodeURIComponent(getUsername(user))}/`)
|
||||
}}
|
||||
>
|
||||
@{getHandle(user)}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
return (
|
||||
<a
|
||||
className="hashtag"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
history.push(`/${encodeURIComponent(getUsername(user))}/`);
|
||||
}}>
|
||||
@{getHandle(user)}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
function findLinkEntities(contentBlock: any, callback: any, contentState: any) {
|
||||
contentBlock.findEntityRanges(
|
||||
(character: any) => {
|
||||
const entityKey = character.getEntity();
|
||||
return (
|
||||
entityKey !== null &&
|
||||
contentState.getEntity(entityKey).getType() === 'LINK'
|
||||
);
|
||||
},
|
||||
callback
|
||||
);
|
||||
contentBlock.findEntityRanges((character: any) => {
|
||||
const entityKey = character.getEntity();
|
||||
return entityKey !== null && contentState.getEntity(entityKey).getType() === 'LINK';
|
||||
}, callback);
|
||||
}
|
||||
|
||||
const mentionRegExp = '[' +
|
||||
'\\w-' +
|
||||
// Latin-1 Supplement (letters only) - https://en.wikipedia.org/wiki/List_of_Unicode_characters#Latin-1_Supplement
|
||||
'\u00C0-\u00D6' +
|
||||
'\u00D8-\u00F6' +
|
||||
'\u00F8-\u00FF' +
|
||||
// Latin Extended-A (without deprecated character) - https://en.wikipedia.org/wiki/List_of_Unicode_characters#Latin_Extended-A
|
||||
'\u0100-\u0148' +
|
||||
'\u014A-\u017F' +
|
||||
// Cyrillic symbols: \u0410-\u044F - https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode
|
||||
'\u0410-\u044F' +
|
||||
// hiragana (japanese): \u3040-\u309F - https://gist.github.com/ryanmcgrath/982242#file-japaneseregex-js
|
||||
'\u3040-\u309F' +
|
||||
// katakana (japanese): \u30A0-\u30FF - https://gist.github.com/ryanmcgrath/982242#file-japaneseregex-js
|
||||
'\u30A0-\u30FF' +
|
||||
// For an advanced explaination about Hangul see https://github.com/draft-js-plugins/draft-js-plugins/pull/480#issuecomment-254055437
|
||||
// Hangul Jamo (korean): \u3130-\u318F - https://en.wikipedia.org/wiki/Korean_language_and_computers#Hangul_in_Unicode
|
||||
// Hangul Syllables (korean): \uAC00-\uD7A3 - https://en.wikipedia.org/wiki/Korean_language_and_computers#Hangul_in_Unicode
|
||||
'\u3130-\u318F' +
|
||||
'\uAC00-\uD7A3' +
|
||||
// common chinese symbols: \u4e00-\u9eff - http://stackoverflow.com/a/1366113/837709
|
||||
// extended to \u9fa5 https://github.com/draft-js-plugins/draft-js-plugins/issues/1888
|
||||
'\u4e00-\u9fa5' +
|
||||
// Arabic https://en.wikipedia.org/wiki/Arabic_(Unicode_block)
|
||||
'\u0600-\u06ff' +
|
||||
// Vietnamese http://vietunicode.sourceforge.net/charset/
|
||||
'\u00C0-\u1EF9' +
|
||||
'\u002E'+
|
||||
']';
|
||||
const mentionRegExp =
|
||||
'[' +
|
||||
'\\w-' +
|
||||
// Latin-1 Supplement (letters only) - https://en.wikipedia.org/wiki/List_of_Unicode_characters#Latin-1_Supplement
|
||||
'\u00C0-\u00D6' +
|
||||
'\u00D8-\u00F6' +
|
||||
'\u00F8-\u00FF' +
|
||||
// Latin Extended-A (without deprecated character) - https://en.wikipedia.org/wiki/List_of_Unicode_characters#Latin_Extended-A
|
||||
'\u0100-\u0148' +
|
||||
'\u014A-\u017F' +
|
||||
// Cyrillic symbols: \u0410-\u044F - https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode
|
||||
'\u0410-\u044F' +
|
||||
// hiragana (japanese): \u3040-\u309F - https://gist.github.com/ryanmcgrath/982242#file-japaneseregex-js
|
||||
'\u3040-\u309F' +
|
||||
// katakana (japanese): \u30A0-\u30FF - https://gist.github.com/ryanmcgrath/982242#file-japaneseregex-js
|
||||
'\u30A0-\u30FF' +
|
||||
// For an advanced explaination about Hangul see https://github.com/draft-js-plugins/draft-js-plugins/pull/480#issuecomment-254055437
|
||||
// Hangul Jamo (korean): \u3130-\u318F - https://en.wikipedia.org/wiki/Korean_language_and_computers#Hangul_in_Unicode
|
||||
// Hangul Syllables (korean): \uAC00-\uD7A3 - https://en.wikipedia.org/wiki/Korean_language_and_computers#Hangul_in_Unicode
|
||||
'\u3130-\u318F' +
|
||||
'\uAC00-\uD7A3' +
|
||||
// common chinese symbols: \u4e00-\u9eff - http://stackoverflow.com/a/1366113/837709
|
||||
// extended to \u9fa5 https://github.com/draft-js-plugins/draft-js-plugins/issues/1888
|
||||
'\u4e00-\u9fa5' +
|
||||
// Arabic https://en.wikipedia.org/wiki/Arabic_(Unicode_block)
|
||||
'\u0600-\u06ff' +
|
||||
// Vietnamese http://vietunicode.sourceforge.net/charset/
|
||||
'\u00C0-\u1EF9' +
|
||||
'\u002E' +
|
||||
']';
|
||||
const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g;
|
||||
const MENTION_REGEX = new RegExp(`\@${mentionRegExp}+`, 'g');
|
||||
function findHashtagEntities(contentBlock: ContentBlock, callback: any, contentState: ContentState) {
|
||||
findWithRegex(HASHTAG_REGEX, contentBlock, callback);
|
||||
function findHashtagEntities(
|
||||
contentBlock: ContentBlock,
|
||||
callback: any,
|
||||
contentState: ContentState
|
||||
) {
|
||||
findWithRegex(HASHTAG_REGEX, contentBlock, callback);
|
||||
}
|
||||
|
||||
function findMentionEntities(contentBlock: ContentBlock, callback: any, contentState: ContentState) {
|
||||
findWithRegex(MENTION_REGEX, contentBlock, callback);
|
||||
function findMentionEntities(
|
||||
contentBlock: ContentBlock,
|
||||
callback: any,
|
||||
contentState: ContentState
|
||||
) {
|
||||
findWithRegex(MENTION_REGEX, contentBlock, callback);
|
||||
}
|
||||
|
||||
function findWithRegex(regex: any, contentBlock: ContentBlock, callback: any) {
|
||||
const text = contentBlock.getText();
|
||||
let matchArr, start;
|
||||
while ((matchArr = regex.exec(text)) !== null) {
|
||||
start = matchArr.index;
|
||||
callback(start, start + matchArr[0].length);
|
||||
}
|
||||
const text = contentBlock.getText();
|
||||
let matchArr, start;
|
||||
while ((matchArr = regex.exec(text)) !== null) {
|
||||
start = matchArr.index;
|
||||
callback(start, start + matchArr[0].length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const markdownConvertOptions = {
|
||||
preserveNewlines: true,
|
||||
blockStyles: {
|
||||
'ins_open': 'UNDERLINE',
|
||||
'del_open': 'STRIKETHROUGH',
|
||||
},
|
||||
styleItems: {
|
||||
'UNDERLINE': {
|
||||
open: function () {
|
||||
return '++';
|
||||
},
|
||||
preserveNewlines: true,
|
||||
blockStyles: {
|
||||
ins_open: 'UNDERLINE',
|
||||
del_open: 'STRIKETHROUGH',
|
||||
},
|
||||
styleItems: {
|
||||
UNDERLINE: {
|
||||
open: function () {
|
||||
return '++';
|
||||
},
|
||||
|
||||
close: function () {
|
||||
return '++';
|
||||
}
|
||||
},
|
||||
'STRIKETHROUGH': {
|
||||
open: function () {
|
||||
return '~~';
|
||||
},
|
||||
|
||||
close: function () {
|
||||
return '~~';
|
||||
}
|
||||
},
|
||||
close: function () {
|
||||
return '++';
|
||||
},
|
||||
},
|
||||
remarkableOptions: {
|
||||
html: false,
|
||||
xhtmlOut: false,
|
||||
breaks: true,
|
||||
enable: {
|
||||
inline: ["ins", 'del', 'links', 'autolink'],
|
||||
core: ['abbr'],
|
||||
block: ['list', 'table']
|
||||
},
|
||||
STRIKETHROUGH: {
|
||||
open: function () {
|
||||
return '~~';
|
||||
},
|
||||
|
||||
// highlight: function (str: string, lang: string) {
|
||||
// if (lang && hljs.getLanguage(lang)) {
|
||||
// try {
|
||||
// return hljs.highlight(lang, str).value;
|
||||
// } catch (err) {
|
||||
// //
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// return hljs.highlightAuto(str).value;
|
||||
// } catch (err) {
|
||||
// //
|
||||
// }
|
||||
//
|
||||
// return ''; // use external default escaping
|
||||
// }
|
||||
close: function () {
|
||||
return '~~';
|
||||
},
|
||||
},
|
||||
remarkablePlugins: [linkify],
|
||||
},
|
||||
remarkableOptions: {
|
||||
html: false,
|
||||
xhtmlOut: false,
|
||||
breaks: true,
|
||||
enable: {
|
||||
inline: ['ins', 'del', 'links', 'autolink'],
|
||||
core: ['abbr'],
|
||||
block: ['list', 'table'],
|
||||
},
|
||||
|
||||
// highlight: function (str: string, lang: string) {
|
||||
// if (lang && hljs.getLanguage(lang)) {
|
||||
// try {
|
||||
// return hljs.highlight(lang, str).value;
|
||||
// } catch (err) {
|
||||
// //
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// return hljs.highlightAuto(str).value;
|
||||
// } catch (err) {
|
||||
// //
|
||||
// }
|
||||
//
|
||||
// return ''; // use external default escaping
|
||||
// }
|
||||
},
|
||||
remarkablePlugins: [linkify],
|
||||
};
|
||||
|
||||
export const convertMarkdownToDraft = (md: string) => {
|
||||
return EditorState.createWithContent(
|
||||
convertFromRaw(markdownToDraft(md, markdownConvertOptions)),
|
||||
decorator,
|
||||
);
|
||||
}
|
||||
return EditorState.createWithContent(
|
||||
convertFromRaw(markdownToDraft(md, markdownConvertOptions)),
|
||||
decorator
|
||||
);
|
||||
};
|
||||
|
||||
interface EntryComponentProps {
|
||||
className?: string;
|
||||
onMouseDown(event: MouseEvent): void;
|
||||
onMouseUp(event: MouseEvent): void;
|
||||
onMouseEnter(event: MouseEvent): void;
|
||||
role: string;
|
||||
id: string;
|
||||
'aria-selected'?: boolean | 'false' | 'true';
|
||||
mention: MentionData;
|
||||
isFocused: boolean;
|
||||
searchValue?: string;
|
||||
className?: string;
|
||||
onMouseDown(event: MouseEvent): void;
|
||||
onMouseUp(event: MouseEvent): void;
|
||||
onMouseEnter(event: MouseEvent): void;
|
||||
role: string;
|
||||
id: string;
|
||||
'aria-selected'?: boolean | 'false' | 'true';
|
||||
mention: MentionData;
|
||||
isFocused: boolean;
|
||||
searchValue?: string;
|
||||
}
|
||||
|
||||
function Entry(props: EntryComponentProps): ReactElement {
|
||||
const {
|
||||
mention,
|
||||
searchValue, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
isFocused, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
className,
|
||||
...parentProps
|
||||
} = props;
|
||||
const {
|
||||
mention,
|
||||
searchValue, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
isFocused, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
className,
|
||||
...parentProps
|
||||
} = props;
|
||||
|
||||
const address = mention.address;
|
||||
const address = mention.address;
|
||||
|
||||
const user = useUser(address);
|
||||
const user = useUser(address);
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<div
|
||||
className={classNames('mention-entry', className)}
|
||||
{...parentProps}
|
||||
>
|
||||
<div
|
||||
className="flex flex-row flex-nowrap p-1 cursor-pointer items-center"
|
||||
>
|
||||
<Avatar address={address} className="w-8 h-8 mr-2" />
|
||||
<div className="flex flex-col flex-nowrap justify-center">
|
||||
<div className="font-bold text-sm hover:underline">{getName(user)}</div>
|
||||
<div className="text-xs text-gray-500">@{getHandle(user)}</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
// @ts-ignore
|
||||
<div className={classNames('mention-entry', className)} {...parentProps}>
|
||||
<div className="flex flex-row flex-nowrap p-1 cursor-pointer items-center">
|
||||
<Avatar address={address} className="w-8 h-8 mr-2" />
|
||||
<div className="flex flex-col flex-nowrap justify-center">
|
||||
<div className="font-bold text-sm hover:underline">{getName(user)}</div>
|
||||
<div className="text-xs text-gray-500">@{getHandle(user)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.dark {
|
||||
.public-DraftEditorPlaceholder-root {
|
||||
@@ -52,7 +52,7 @@ div.public-DraftEditor-content {
|
||||
|
||||
.editor {
|
||||
div.DraftEditor-root {
|
||||
padding: .75rem .5rem;
|
||||
padding: 0.75rem 0.5rem;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ div.public-DraftEditor-content {
|
||||
|
||||
.moderation-btn {
|
||||
//display: none;
|
||||
margin-bottom: .5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
//&:focus-within {
|
||||
@@ -77,7 +77,7 @@ div.public-DraftEditor-content {
|
||||
//color: $gray-200;
|
||||
height: 2rem;
|
||||
cursor: pointer;
|
||||
font-size: .9375rem;
|
||||
font-size: 0.9375rem;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
@@ -97,7 +97,7 @@ div.public-DraftEditor-content {
|
||||
&__submit-btn {
|
||||
@extend %row-nowrap;
|
||||
padding: 0;
|
||||
font-size: .875rem;
|
||||
font-size: 0.875rem;
|
||||
height: 2rem;
|
||||
min-width: 2rem;
|
||||
|
||||
@@ -134,7 +134,7 @@ div.public-DraftEditor-content {
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 0.5rem;
|
||||
border-left: 1px solid rgba($white, .5);
|
||||
border-left: 1px solid rgba($white, 0.5);
|
||||
border-top-right-radius: 0.75rem;
|
||||
border-bottom-right-radius: 0.75rem;
|
||||
|
||||
@@ -169,6 +169,5 @@ div.public-DraftEditor-content {
|
||||
|
||||
.dark {
|
||||
.editor {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,461 +1,453 @@
|
||||
import React, {ReactElement, useCallback, useEffect, useState} from "react";
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { convertFromRaw, DraftHandleValue, EditorState, RichUtils } from 'draft-js';
|
||||
import classNames from 'classnames';
|
||||
import './editor.scss';
|
||||
import {
|
||||
convertFromRaw,
|
||||
DraftHandleValue,
|
||||
EditorState,
|
||||
RichUtils,
|
||||
} from "draft-js";
|
||||
import classNames from "classnames";
|
||||
import "./editor.scss";
|
||||
import {useAccount, useCanNonPostMessage, useENSName, useGunKey, useLoggedIn, useSemaphoreID} from "../../ducks/web3";
|
||||
import Avatar from "../Avatar";
|
||||
import Web3Button from "../Web3Button";
|
||||
import Button from "../Button";
|
||||
import {DraftEditor} from "../DraftEditor";
|
||||
import Icon from "../Icon";
|
||||
import drafts, {setDraft, setGloabl, setMirror, setModeration, useDraft, useMirror} from "../../ducks/drafts";
|
||||
import {useDispatch} from "react-redux";
|
||||
import URLPreview from "../URLPreview";
|
||||
import SpinnerGif from "../../../static/icons/spinner.gif";
|
||||
import {useUser} from "../../ducks/users";
|
||||
import {setPostingGroup, usePostingGroup, useSelectedLocalId, useSelectedZKGroup} from "../../ducks/worker";
|
||||
import {useHistory} from "react-router";
|
||||
import Checkbox from "../Checkbox";
|
||||
import {getSession, verifyTweet} from "../../util/twitter";
|
||||
import ModerationButton from "../ModerationButton";
|
||||
import {ModerationMessageSubType} from "../../util/message";
|
||||
import {usePostModeration} from "../../ducks/mods";
|
||||
import {useCommentDisabled, useMeta} from "../../ducks/posts";
|
||||
import Menuable from "../Menuable";
|
||||
import FileUploadModal from "../FileUploadModal";
|
||||
import LinkInputModal from "../LinkInputModal";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import CustomGroupSelectModal from "../CustomGroupSelectModal";
|
||||
useAccount,
|
||||
useCanNonPostMessage,
|
||||
useENSName,
|
||||
useGunKey,
|
||||
useLoggedIn,
|
||||
useSemaphoreID,
|
||||
} from '../../ducks/web3';
|
||||
import Avatar from '../Avatar';
|
||||
import Web3Button from '../Web3Button';
|
||||
import Button from '../Button';
|
||||
import { DraftEditor } from '../DraftEditor';
|
||||
import Icon from '../Icon';
|
||||
import drafts, {
|
||||
setDraft,
|
||||
setGloabl,
|
||||
setMirror,
|
||||
setModeration,
|
||||
useDraft,
|
||||
useMirror,
|
||||
} from '../../ducks/drafts';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import URLPreview from '../URLPreview';
|
||||
import SpinnerGif from '../../../static/icons/spinner.gif';
|
||||
import { useUser } from '../../ducks/users';
|
||||
import {
|
||||
setPostingGroup,
|
||||
usePostingGroup,
|
||||
useSelectedLocalId,
|
||||
useSelectedZKGroup,
|
||||
} from '../../ducks/worker';
|
||||
import { useHistory } from 'react-router';
|
||||
import Checkbox from '../Checkbox';
|
||||
import { getSession, verifyTweet } from '../../util/twitter';
|
||||
import ModerationButton from '../ModerationButton';
|
||||
import { ModerationMessageSubType } from '../../util/message';
|
||||
import { usePostModeration } from '../../ducks/mods';
|
||||
import { useCommentDisabled, useMeta } from '../../ducks/posts';
|
||||
import Menuable from '../Menuable';
|
||||
import FileUploadModal from '../FileUploadModal';
|
||||
import LinkInputModal from '../LinkInputModal';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
import CustomGroupSelectModal from '../CustomGroupSelectModal';
|
||||
|
||||
type Props = {
|
||||
messageId: string;
|
||||
editorState: EditorState;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
loading?: boolean;
|
||||
onPost?: () => Promise<void>;
|
||||
}
|
||||
messageId: string;
|
||||
editorState: EditorState;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
loading?: boolean;
|
||||
onPost?: () => Promise<void>;
|
||||
};
|
||||
|
||||
export default function Editor(props: Props): ReactElement {
|
||||
const {
|
||||
messageId,
|
||||
editorState,
|
||||
disabled,
|
||||
readOnly,
|
||||
loading,
|
||||
} = props;
|
||||
const { messageId, editorState, disabled, readOnly, loading } = props;
|
||||
|
||||
const address = useAccount();
|
||||
const user = useUser(address);
|
||||
const incognitoGroup = usePostingGroup();
|
||||
const draft = useDraft(messageId);
|
||||
const dispatch = useDispatch();
|
||||
const gun = useGunKey();
|
||||
const history = useHistory();
|
||||
const selectedId = useSelectedLocalId();
|
||||
const isEmpty = !editorState.getCurrentContent().hasText() && !draft.attachment;
|
||||
const mirror = useMirror();
|
||||
const selectedZKGroup = useSelectedZKGroup();
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [verifying, setVerifying] = useState(true);
|
||||
const [showingImageUploadModal, setShowingImageUploadModal] = useState(false);
|
||||
const [showingLinkModal, setShowingLinkModal] = useState(false);
|
||||
const [showingCustomGroupsModal, setShowingCustomGroupsModal] = useState(false);
|
||||
const [verified, setVerified] = useState(false);
|
||||
const [verifiedSession, setVerifiedSession] = useState('');
|
||||
const meta = useMeta(messageId);
|
||||
const modOverride = usePostModeration(meta?.rootId);
|
||||
const commentDisabled = useCommentDisabled(meta?.rootId);
|
||||
const shouldDisplayWarning = !!modOverride?.unmoderated && commentDisabled;
|
||||
const canNonPostMessage = useCanNonPostMessage();
|
||||
const theme = useThemeContext();
|
||||
const bgColor = classNames({
|
||||
'bg-white': theme !== 'dark',
|
||||
'bg-dark': theme === 'dark'
|
||||
});
|
||||
const address = useAccount();
|
||||
const user = useUser(address);
|
||||
const incognitoGroup = usePostingGroup();
|
||||
const draft = useDraft(messageId);
|
||||
const dispatch = useDispatch();
|
||||
const gun = useGunKey();
|
||||
const history = useHistory();
|
||||
const selectedId = useSelectedLocalId();
|
||||
const isEmpty = !editorState.getCurrentContent().hasText() && !draft.attachment;
|
||||
const mirror = useMirror();
|
||||
const selectedZKGroup = useSelectedZKGroup();
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [verifying, setVerifying] = useState(true);
|
||||
const [showingImageUploadModal, setShowingImageUploadModal] = useState(false);
|
||||
const [showingLinkModal, setShowingLinkModal] = useState(false);
|
||||
const [showingCustomGroupsModal, setShowingCustomGroupsModal] = useState(false);
|
||||
const [verified, setVerified] = useState(false);
|
||||
const [verifiedSession, setVerifiedSession] = useState('');
|
||||
const meta = useMeta(messageId);
|
||||
const modOverride = usePostModeration(meta?.rootId);
|
||||
const commentDisabled = useCommentDisabled(meta?.rootId);
|
||||
const shouldDisplayWarning = !!modOverride?.unmoderated && commentDisabled;
|
||||
const canNonPostMessage = useCanNonPostMessage();
|
||||
const theme = useThemeContext();
|
||||
const bgColor = classNames({
|
||||
'bg-white': theme !== 'dark',
|
||||
'bg-dark': theme === 'dark',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async function() {
|
||||
setVerified(false);
|
||||
setVerifiedSession('');
|
||||
setVerifying(true);
|
||||
dispatch(setMirror(false));
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
setVerified(false);
|
||||
setVerifiedSession('');
|
||||
setVerifying(true);
|
||||
dispatch(setMirror(false));
|
||||
|
||||
if (!selectedId) return;
|
||||
if (!selectedId) return;
|
||||
|
||||
let session;
|
||||
let session;
|
||||
|
||||
try {
|
||||
session = await getSession(selectedId);
|
||||
try {
|
||||
session = await getSession(selectedId);
|
||||
|
||||
if (session) {
|
||||
setVerifiedSession(session.username);
|
||||
dispatch(setMirror(!!localStorage.getItem(`${session.username}_should_mirror`)));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if (await verifyTweet(user?.address, user?.twitterVerification, session?.username)) {
|
||||
setVerified(true);
|
||||
}
|
||||
|
||||
setVerifying(false);
|
||||
})();
|
||||
}, [selectedId, user]);
|
||||
|
||||
const onChange = useCallback((newEditorState: EditorState) => {
|
||||
if (readOnly) return;
|
||||
setErrorMessage('');
|
||||
dispatch(setDraft({
|
||||
editorState: newEditorState,
|
||||
reference: messageId,
|
||||
attachment: draft.attachment,
|
||||
moderation: draft.moderation,
|
||||
global: draft.global,
|
||||
}));
|
||||
}, [messageId, readOnly, draft]);
|
||||
|
||||
const onModerationChange = useCallback((type: ModerationMessageSubType | null) => {
|
||||
if (readOnly) return;
|
||||
dispatch(setModeration(messageId || '', type));
|
||||
}, [messageId]);
|
||||
|
||||
const onSetMirror = useCallback(async (e: any) => {
|
||||
const checked = e.target.checked;
|
||||
|
||||
if (!verifiedSession || !verified) {
|
||||
history.push('/connect/twitter');
|
||||
return;
|
||||
if (session) {
|
||||
setVerifiedSession(session.username);
|
||||
dispatch(setMirror(!!localStorage.getItem(`${session.username}_should_mirror`)));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if (verified) {
|
||||
localStorage.setItem(`${verifiedSession}_should_mirror`, checked ? '1': '');
|
||||
dispatch(setMirror(checked));
|
||||
}
|
||||
}, [verified, verifiedSession]);
|
||||
if (await verifyTweet(user?.address, user?.twitterVerification, session?.username)) {
|
||||
setVerified(true);
|
||||
}
|
||||
|
||||
const onChangeGroup = useCallback((groupId: string) => {
|
||||
dispatch(setPostingGroup(groupId));
|
||||
setShowingCustomGroupsModal(false);
|
||||
}, [incognitoGroup]);
|
||||
setVerifying(false);
|
||||
})();
|
||||
}, [selectedId, user]);
|
||||
|
||||
const onAddLink = useCallback((url: string) => {
|
||||
if (readOnly) return;
|
||||
const onChange = useCallback(
|
||||
(newEditorState: EditorState) => {
|
||||
if (readOnly) return;
|
||||
setErrorMessage('');
|
||||
dispatch(
|
||||
setDraft({
|
||||
editorState: newEditorState,
|
||||
reference: messageId,
|
||||
attachment: draft.attachment,
|
||||
moderation: draft.moderation,
|
||||
global: draft.global,
|
||||
})
|
||||
);
|
||||
},
|
||||
[messageId, readOnly, draft]
|
||||
);
|
||||
|
||||
dispatch(setDraft({
|
||||
editorState: draft.editorState,
|
||||
reference: draft.reference,
|
||||
attachment: url,
|
||||
}));
|
||||
const onModerationChange = useCallback(
|
||||
(type: ModerationMessageSubType | null) => {
|
||||
if (readOnly) return;
|
||||
dispatch(setModeration(messageId || '', type));
|
||||
},
|
||||
[messageId]
|
||||
);
|
||||
|
||||
setShowingImageUploadModal(false);
|
||||
setShowingLinkModal(false);
|
||||
}, [messageId, readOnly, draft]);
|
||||
const onSetMirror = useCallback(
|
||||
async (e: any) => {
|
||||
const checked = e.target.checked;
|
||||
|
||||
const handleKeyCommand: (command: string) => DraftHandleValue = useCallback((command: string): DraftHandleValue => {
|
||||
const newState = RichUtils.handleKeyCommand(editorState, command);
|
||||
if (newState) {
|
||||
onChange && onChange(newState);
|
||||
return 'handled';
|
||||
}
|
||||
return 'not-handled';
|
||||
}, [editorState]);
|
||||
if (!verifiedSession || !verified) {
|
||||
history.push('/connect/twitter');
|
||||
return;
|
||||
}
|
||||
|
||||
const onPost = useCallback(async () => {
|
||||
if (mirror && (!verifiedSession || !verified)) {
|
||||
history.push('/connect/twitter');
|
||||
return;
|
||||
}
|
||||
setErrorMessage('');
|
||||
try {
|
||||
if (props.onPost) {
|
||||
await props.onPost();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setErrorMessage(e.message);
|
||||
}
|
||||
}, [props.onPost, verified, verifiedSession, mirror]);
|
||||
if (verified) {
|
||||
localStorage.setItem(`${verifiedSession}_should_mirror`, checked ? '1' : '');
|
||||
dispatch(setMirror(checked));
|
||||
}
|
||||
},
|
||||
[verified, verifiedSession]
|
||||
);
|
||||
|
||||
const onGlobalChange = useCallback((e: any) => {
|
||||
dispatch(setGloabl(messageId, e.target.checked));
|
||||
}, [messageId]);
|
||||
const onChangeGroup = useCallback(
|
||||
(groupId: string) => {
|
||||
dispatch(setPostingGroup(groupId));
|
||||
setShowingCustomGroupsModal(false);
|
||||
},
|
||||
[incognitoGroup]
|
||||
);
|
||||
|
||||
const onAddLink = useCallback(
|
||||
(url: string) => {
|
||||
if (readOnly) return;
|
||||
|
||||
if (!disabled && gun.priv && gun.pub && !gun.joinedTx) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap items-center',
|
||||
'p-4',
|
||||
bgColor,
|
||||
'rounded-xl',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<Icon className="opacity-50" url={SpinnerGif} size={4} />
|
||||
</div>
|
||||
)
|
||||
dispatch(
|
||||
setDraft({
|
||||
editorState: draft.editorState,
|
||||
reference: draft.reference,
|
||||
attachment: url,
|
||||
})
|
||||
);
|
||||
|
||||
setShowingImageUploadModal(false);
|
||||
setShowingLinkModal(false);
|
||||
},
|
||||
[messageId, readOnly, draft]
|
||||
);
|
||||
|
||||
const handleKeyCommand: (command: string) => DraftHandleValue = useCallback(
|
||||
(command: string): DraftHandleValue => {
|
||||
const newState = RichUtils.handleKeyCommand(editorState, command);
|
||||
if (newState) {
|
||||
onChange && onChange(newState);
|
||||
return 'handled';
|
||||
}
|
||||
return 'not-handled';
|
||||
},
|
||||
[editorState]
|
||||
);
|
||||
|
||||
const onPost = useCallback(async () => {
|
||||
if (mirror && (!verifiedSession || !verified)) {
|
||||
history.push('/connect/twitter');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedId?.type === 'interrep' && !selectedId.identityPath) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap items-center',
|
||||
'p-4',
|
||||
bgColor,
|
||||
'rounded-xl',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames("mt-2 mb-4", {
|
||||
'text-gray-800': theme !== 'dark',
|
||||
'text-gray-200': theme === 'dark'
|
||||
})}
|
||||
>
|
||||
Join Interrep to make a post
|
||||
</div>
|
||||
<Button btnType="primary" onClick={() => history.push('/onboarding/interrep')}>
|
||||
Join Interrep
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
setErrorMessage('');
|
||||
try {
|
||||
if (props.onPost) {
|
||||
await props.onPost();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setErrorMessage(e.message);
|
||||
}
|
||||
}, [props.onPost, verified, verifiedSession, mirror]);
|
||||
|
||||
if (!disabled && !selectedId) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap items-center',
|
||||
'p-4',
|
||||
bgColor,
|
||||
'rounded-xl',
|
||||
props.className,
|
||||
{
|
||||
"border-gray-100": theme !== 'dark',
|
||||
"border-gray-800": theme === 'dark',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames("mt-2 mb-4", {
|
||||
'text-gray-800': theme !== 'dark',
|
||||
'text-gray-200': theme === 'dark'
|
||||
})}
|
||||
>
|
||||
Connect to a wallet to make a post
|
||||
</div>
|
||||
<Web3Button
|
||||
className={classNames("rounded-xl border", {
|
||||
"border-gray-100": theme !== 'dark',
|
||||
"border-gray-800": theme === 'dark',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const onGlobalChange = useCallback(
|
||||
(e: any) => {
|
||||
dispatch(setGloabl(messageId, e.target.checked));
|
||||
},
|
||||
[messageId]
|
||||
);
|
||||
|
||||
if (!disabled && gun.priv && gun.pub && !gun.joinedTx) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap',
|
||||
'pt-3 pb-2 px-4',
|
||||
bgColor,
|
||||
'rounded-xl',
|
||||
'text-lg',
|
||||
'editor',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row flex-nowrap w-full h-full">
|
||||
<Avatar
|
||||
className="w-12 h-12 mr-3"
|
||||
address={address}
|
||||
group={selectedZKGroup || incognitoGroup}
|
||||
incognito={
|
||||
['interrep', 'zkpr_interrep', 'taz'].includes(selectedId?.type as string)
|
||||
|| !!incognitoGroup
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-row flex-nowrap w-full h-full">
|
||||
<div className="flex flex-col flex-nowrap w-full h-full editor__wrapper">
|
||||
{
|
||||
shouldDisplayWarning && (
|
||||
<div className="rounded p-2 text-sm bg-yellow-100 text-yellow-500">
|
||||
You can still submit a reply, but it will be hidden by default due to the OP's reply polilcy.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<DraftEditor
|
||||
editorState={editorState}
|
||||
onChange={onChange}
|
||||
handleKeyCommand={handleKeyCommand}
|
||||
placeholder={readOnly ? '' : "Write here..."}
|
||||
readOnly={readOnly || disabled}
|
||||
/>
|
||||
{
|
||||
selectedId?.type === 'gun' && !messageId && !incognitoGroup && (
|
||||
<ModerationButton
|
||||
onChange={onModerationChange}
|
||||
currentType={draft.moderation || null}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
draft.attachment && (
|
||||
<div className="editor__attachment py-2">
|
||||
<URLPreview
|
||||
url={draft.attachment}
|
||||
onRemove={() => onAddLink('')}
|
||||
editable
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{ errorMessage && <div className="error-message text-xs text-center text-red-500 m-2">{errorMessage}</div> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames("flex flex-row flex-nowrap border-t pt-2 ml-15", {
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}
|
||||
>
|
||||
<div className="flex-grow pr-4 mr-4 flex flex-row flex-nowrap items-center">
|
||||
{
|
||||
selectedId?.type === 'gun' && (
|
||||
<Icon
|
||||
className={classNames(
|
||||
"editor__button text-blue-300 w-8 h-8 relative",
|
||||
{
|
||||
'hover:bg-blue-50 hover:text-blue-400': theme !== 'dark',
|
||||
'hover:bg-blue-900 hover:text-blue-600': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
fa="fas fa-user-secret"
|
||||
onClick={() => setShowingCustomGroupsModal(true)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<Icon
|
||||
className={classNames(
|
||||
"editor__button text-blue-300 w-8 h-8 relative",
|
||||
{
|
||||
'hover:bg-blue-50 hover:text-blue-400': theme !== 'dark',
|
||||
'hover:bg-blue-900 hover:text-blue-600': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
fa="fas fa-image"
|
||||
onClick={() => setShowingImageUploadModal(true)}
|
||||
/>
|
||||
<Icon
|
||||
className={classNames(
|
||||
"editor__button text-blue-300 w-8 h-8 relative",
|
||||
{
|
||||
'hover:bg-blue-50 hover:text-blue-400': theme !== 'dark',
|
||||
'hover:bg-blue-900 hover:text-blue-600': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
fa="fas fa-link"
|
||||
onClick={() => setShowingLinkModal(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow flex flex-row flex-nowrap items-center justify-end">
|
||||
<Button
|
||||
className="editor__submit-btn"
|
||||
btnType="primary"
|
||||
onClick={onPost}
|
||||
disabled={isEmpty}
|
||||
loading={loading}
|
||||
>
|
||||
<div className="editor__submit-btn__wrapper">
|
||||
<div className="editor__submit-btn__wrapper__text">
|
||||
{
|
||||
!draft?.global && selectedId?.type === 'gun' && !incognitoGroup
|
||||
? 'Post to my own feed'
|
||||
: 'Post to global feed'
|
||||
}
|
||||
</div>
|
||||
{
|
||||
(canNonPostMessage && !incognitoGroup) && (
|
||||
<Menuable
|
||||
items={[
|
||||
{
|
||||
label: 'Make a public post',
|
||||
className: 'editor__post-menu',
|
||||
component: (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row mb-2">
|
||||
<Checkbox
|
||||
className="mr-4 text-gray-500"
|
||||
onChange={onGlobalChange}
|
||||
checked={!!draft?.global}
|
||||
>
|
||||
Post to global timeline
|
||||
</Checkbox>
|
||||
</div>
|
||||
{
|
||||
!['interrep', 'zkpr_interrep', 'taz'].includes(selectedId?.type as string) && (
|
||||
<div className="flex flex-row">
|
||||
<Checkbox
|
||||
className="mr-4 text-gray-500"
|
||||
onChange={onSetMirror}
|
||||
checked={mirror}
|
||||
disabled={verifying}
|
||||
>
|
||||
Mirror to Twitter
|
||||
</Checkbox>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Icon fa="fas fa-caret-down" />
|
||||
</Menuable>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
showingCustomGroupsModal && (
|
||||
<CustomGroupSelectModal
|
||||
onChange={onChangeGroup}
|
||||
onClose={() => setShowingCustomGroupsModal(false)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showingImageUploadModal && (
|
||||
<FileUploadModal
|
||||
onClose={() => setShowingImageUploadModal(false)}
|
||||
onAccept={onAddLink}
|
||||
mustLinkBeImage
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showingLinkModal && (
|
||||
<LinkInputModal
|
||||
onClose={() => setShowingLinkModal(false)}
|
||||
onAccept={onAddLink}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap items-center',
|
||||
'p-4',
|
||||
bgColor,
|
||||
'rounded-xl',
|
||||
props.className
|
||||
)}>
|
||||
<Icon className="opacity-50" url={SpinnerGif} size={4} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedId?.type === 'interrep' && !selectedId.identityPath) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap items-center',
|
||||
'p-4',
|
||||
bgColor,
|
||||
'rounded-xl',
|
||||
props.className
|
||||
)}>
|
||||
<div
|
||||
className={classNames('mt-2 mb-4', {
|
||||
'text-gray-800': theme !== 'dark',
|
||||
'text-gray-200': theme === 'dark',
|
||||
})}>
|
||||
Join Interrep to make a post
|
||||
</div>
|
||||
<Button btnType="primary" onClick={() => history.push('/onboarding/interrep')}>
|
||||
Join Interrep
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!disabled && !selectedId) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap items-center',
|
||||
'p-4',
|
||||
bgColor,
|
||||
'rounded-xl',
|
||||
props.className,
|
||||
{
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
}
|
||||
)}>
|
||||
<div
|
||||
className={classNames('mt-2 mb-4', {
|
||||
'text-gray-800': theme !== 'dark',
|
||||
'text-gray-200': theme === 'dark',
|
||||
})}>
|
||||
Connect to a wallet to make a post
|
||||
</div>
|
||||
<Web3Button
|
||||
className={classNames('rounded-xl border', {
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap',
|
||||
'pt-3 pb-2 px-4',
|
||||
bgColor,
|
||||
'rounded-xl',
|
||||
'text-lg',
|
||||
'editor',
|
||||
props.className
|
||||
)}>
|
||||
<div className="flex flex-row flex-nowrap w-full h-full">
|
||||
<Avatar
|
||||
className="w-12 h-12 mr-3"
|
||||
address={address}
|
||||
group={selectedZKGroup || incognitoGroup}
|
||||
incognito={
|
||||
['interrep', 'zkpr_interrep', 'taz'].includes(selectedId?.type as string) ||
|
||||
!!incognitoGroup
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-row flex-nowrap w-full h-full">
|
||||
<div className="flex flex-col flex-nowrap w-full h-full editor__wrapper">
|
||||
{shouldDisplayWarning && (
|
||||
<div className="rounded p-2 text-sm bg-yellow-100 text-yellow-500">
|
||||
You can still submit a reply, but it will be hidden by default due to the OP's reply
|
||||
polilcy.
|
||||
</div>
|
||||
)}
|
||||
<DraftEditor
|
||||
editorState={editorState}
|
||||
onChange={onChange}
|
||||
handleKeyCommand={handleKeyCommand}
|
||||
placeholder={readOnly ? '' : 'Write here...'}
|
||||
readOnly={readOnly || disabled}
|
||||
/>
|
||||
{selectedId?.type === 'gun' && !messageId && !incognitoGroup && (
|
||||
<ModerationButton
|
||||
onChange={onModerationChange}
|
||||
currentType={draft.moderation || null}
|
||||
/>
|
||||
)}
|
||||
{draft.attachment && (
|
||||
<div className="editor__attachment py-2">
|
||||
<URLPreview url={draft.attachment} onRemove={() => onAddLink('')} editable />
|
||||
</div>
|
||||
)}
|
||||
{errorMessage && (
|
||||
<div className="error-message text-xs text-center text-red-500 m-2">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames('flex flex-row flex-nowrap border-t pt-2 ml-15', {
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}>
|
||||
<div className="flex-grow pr-4 mr-4 flex flex-row flex-nowrap items-center">
|
||||
{selectedId?.type === 'gun' && (
|
||||
<Icon
|
||||
className={classNames('editor__button text-blue-300 w-8 h-8 relative', {
|
||||
'hover:bg-blue-50 hover:text-blue-400': theme !== 'dark',
|
||||
'hover:bg-blue-900 hover:text-blue-600': theme === 'dark',
|
||||
})}
|
||||
fa="fas fa-user-secret"
|
||||
onClick={() => setShowingCustomGroupsModal(true)}
|
||||
/>
|
||||
)}
|
||||
<Icon
|
||||
className={classNames('editor__button text-blue-300 w-8 h-8 relative', {
|
||||
'hover:bg-blue-50 hover:text-blue-400': theme !== 'dark',
|
||||
'hover:bg-blue-900 hover:text-blue-600': theme === 'dark',
|
||||
})}
|
||||
fa="fas fa-image"
|
||||
onClick={() => setShowingImageUploadModal(true)}
|
||||
/>
|
||||
<Icon
|
||||
className={classNames('editor__button text-blue-300 w-8 h-8 relative', {
|
||||
'hover:bg-blue-50 hover:text-blue-400': theme !== 'dark',
|
||||
'hover:bg-blue-900 hover:text-blue-600': theme === 'dark',
|
||||
})}
|
||||
fa="fas fa-link"
|
||||
onClick={() => setShowingLinkModal(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow flex flex-row flex-nowrap items-center justify-end">
|
||||
<Button
|
||||
className="editor__submit-btn"
|
||||
btnType="primary"
|
||||
onClick={onPost}
|
||||
disabled={isEmpty}
|
||||
loading={loading}>
|
||||
<div className="editor__submit-btn__wrapper">
|
||||
<div className="editor__submit-btn__wrapper__text">
|
||||
{!draft?.global && selectedId?.type === 'gun' && !incognitoGroup
|
||||
? 'Post to my own feed'
|
||||
: 'Post to global feed'}
|
||||
</div>
|
||||
{canNonPostMessage && !incognitoGroup && (
|
||||
<Menuable
|
||||
items={[
|
||||
{
|
||||
label: 'Make a public post',
|
||||
className: 'editor__post-menu',
|
||||
component: (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row mb-2">
|
||||
<Checkbox
|
||||
className="mr-4 text-gray-500"
|
||||
onChange={onGlobalChange}
|
||||
checked={!!draft?.global}>
|
||||
Post to global timeline
|
||||
</Checkbox>
|
||||
</div>
|
||||
{!['interrep', 'zkpr_interrep', 'taz'].includes(
|
||||
selectedId?.type as string
|
||||
) && (
|
||||
<div className="flex flex-row">
|
||||
<Checkbox
|
||||
className="mr-4 text-gray-500"
|
||||
onChange={onSetMirror}
|
||||
checked={mirror}
|
||||
disabled={verifying}>
|
||||
Mirror to Twitter
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}>
|
||||
<Icon fa="fas fa-caret-down" />
|
||||
</Menuable>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{showingCustomGroupsModal && (
|
||||
<CustomGroupSelectModal
|
||||
onChange={onChangeGroup}
|
||||
onClose={() => setShowingCustomGroupsModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showingImageUploadModal && (
|
||||
<FileUploadModal
|
||||
onClose={() => setShowingImageUploadModal(false)}
|
||||
onAccept={onAddLink}
|
||||
mustLinkBeImage
|
||||
/>
|
||||
)}
|
||||
{showingLinkModal && (
|
||||
<LinkInputModal onClose={() => setShowingLinkModal(false)} onAccept={onAddLink} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,51 +1,48 @@
|
||||
import React, {ReactElement} from "react";
|
||||
import Modal, {ModalContent, ModalHeader} from "../Modal";
|
||||
import {useSelectedLocalId} from "../../ducks/worker";
|
||||
import React, { ReactElement } from 'react';
|
||||
import Modal, { ModalContent, ModalHeader } from '../Modal';
|
||||
import { useSelectedLocalId } from '../../ducks/worker';
|
||||
import QRCode from 'react-qr-code';
|
||||
import {Identity} from "../../serviceWorkers/identity";
|
||||
import { Identity } from '../../serviceWorkers/identity';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
}
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function ExportPrivateKeyModal(props: Props): ReactElement {
|
||||
const selected = useSelectedLocalId();
|
||||
const selected = useSelectedLocalId();
|
||||
|
||||
let data;
|
||||
let data;
|
||||
|
||||
if (selected?.type === 'gun') {
|
||||
data = {
|
||||
type: selected.type,
|
||||
address: selected.address,
|
||||
nonce: selected.nonce,
|
||||
publicKey: selected.publicKey,
|
||||
privateKey: selected.privateKey,
|
||||
};
|
||||
}
|
||||
if (selected?.type === 'gun') {
|
||||
data = {
|
||||
type: selected.type,
|
||||
address: selected.address,
|
||||
nonce: selected.nonce,
|
||||
publicKey: selected.publicKey,
|
||||
privateKey: selected.privateKey,
|
||||
};
|
||||
}
|
||||
|
||||
if (selected?.type === 'interrep') {
|
||||
data = {
|
||||
type: selected.type,
|
||||
address: selected.address,
|
||||
serializedIdentity: selected.serializedIdentity,
|
||||
identityCommitment: selected.identityCommitment,
|
||||
nonce: selected.nonce,
|
||||
provider: selected.provider,
|
||||
name: selected.name,
|
||||
};
|
||||
}
|
||||
if (selected?.type === 'interrep') {
|
||||
data = {
|
||||
type: selected.type,
|
||||
address: selected.address,
|
||||
serializedIdentity: selected.serializedIdentity,
|
||||
identityCommitment: selected.identityCommitment,
|
||||
nonce: selected.nonce,
|
||||
provider: selected.provider,
|
||||
name: selected.name,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="w-96"
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<ModalHeader onClose={props.onClose}>
|
||||
<b>{`Export Private Key`}</b>
|
||||
</ModalHeader>
|
||||
<ModalContent className="p-4 my-4">
|
||||
<QRCode className="my-0 mx-auto" value={JSON.stringify(data)} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Modal className="w-96" onClose={props.onClose}>
|
||||
<ModalHeader onClose={props.onClose}>
|
||||
<b>{`Export Private Key`}</b>
|
||||
</ModalHeader>
|
||||
<ModalContent className="p-4 my-4">
|
||||
<QRCode className="my-0 mx-auto" value={JSON.stringify(data)} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable.scss";
|
||||
@import '../../util/variable.scss';
|
||||
|
||||
.file-select-btn {
|
||||
@extend %col-nowrap;
|
||||
@@ -6,7 +6,7 @@
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
input[type=file] {
|
||||
input[type='file'] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -15,4 +15,4 @@
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
import React, {ChangeEvent, EventHandler, ReactElement} from "react";
|
||||
import "./file-select-btn.scss";
|
||||
import Button from "../Button";
|
||||
import Icon from "../Icon";
|
||||
import classNames from "classnames";
|
||||
import React, { ChangeEvent, EventHandler, ReactElement } from 'react';
|
||||
import './file-select-btn.scss';
|
||||
import Button from '../Button';
|
||||
import Icon from '../Icon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
accept?: string;
|
||||
onChange?: EventHandler<ChangeEvent<HTMLInputElement>>;
|
||||
}
|
||||
className?: string;
|
||||
accept?: string;
|
||||
onChange?: EventHandler<ChangeEvent<HTMLInputElement>>;
|
||||
};
|
||||
|
||||
export default function FileSelectButton(props: Props): ReactElement {
|
||||
return (
|
||||
<div className={classNames("file-select-btn", props.className)}>
|
||||
<Button
|
||||
btnType="primary"
|
||||
>
|
||||
<Icon fa="fas fa-file-upload" className="mr-2" />
|
||||
<span>Select File</span>
|
||||
</Button>
|
||||
<input
|
||||
type="file"
|
||||
accept={props.accept}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={classNames('file-select-btn', props.className)}>
|
||||
<Button btnType="primary">
|
||||
<Icon fa="fas fa-file-upload" className="mr-2" />
|
||||
<span>Select File</span>
|
||||
</Button>
|
||||
<input type="file" accept={props.accept} onChange={props.onChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable.scss";
|
||||
@import '../../util/variable.scss';
|
||||
|
||||
.dark {
|
||||
.file-upload-modal {
|
||||
@@ -14,7 +14,7 @@
|
||||
@extend %col-nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: .5rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 2px dashed $gray-200;
|
||||
background-color: $gray-100;
|
||||
padding: 4rem;
|
||||
@@ -33,4 +33,4 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,252 +1,232 @@
|
||||
import React, {
|
||||
KeyboardEvent, ChangeEvent, ReactElement,
|
||||
useCallback, useEffect, useRef, useState,
|
||||
} from "react";
|
||||
import Modal, {ModalContent, ModalFooter, ModalHeader} from "../Modal";
|
||||
import Button from "../Button";
|
||||
import Icon from "../Icon";
|
||||
import classNames from "classnames";
|
||||
import "./file-upload-modal.scss";
|
||||
import FileSelectButton from "../FileSelectButton";
|
||||
import Input from "../Input";
|
||||
import config from "../../util/config";
|
||||
import URLPreview from "../URLPreview";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {ipfsUploadOne} from "../../util/upload";
|
||||
KeyboardEvent,
|
||||
ChangeEvent,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Modal, { ModalContent, ModalFooter, ModalHeader } from '../Modal';
|
||||
import Button from '../Button';
|
||||
import Icon from '../Icon';
|
||||
import classNames from 'classnames';
|
||||
import './file-upload-modal.scss';
|
||||
import FileSelectButton from '../FileSelectButton';
|
||||
import Input from '../Input';
|
||||
import config from '../../util/config';
|
||||
import URLPreview from '../URLPreview';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { ipfsUploadOne } from '../../util/upload';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
onAccept: (url: string) => void;
|
||||
mustLinkBeImage?: boolean;
|
||||
skipLinkPreview?: boolean;
|
||||
}
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
onAccept: (url: string) => void;
|
||||
mustLinkBeImage?: boolean;
|
||||
skipLinkPreview?: boolean;
|
||||
};
|
||||
|
||||
const ONE_MB = 1048576;
|
||||
const maxFileSize = ONE_MB * 5;
|
||||
|
||||
export default function FileUploadModal(props: Props): ReactElement {
|
||||
const dispatch = useDispatch();
|
||||
const drop = useRef<HTMLDivElement>(null);
|
||||
const [err, setError] = useState('');
|
||||
const [previewUrl, setPreviewUrl] = useState('');
|
||||
const [link, setLink] = useState('');
|
||||
const [file, setFile] = useState<File|null>(null);
|
||||
const [isUploading, setUploading] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const drop = useRef<HTMLDivElement>(null);
|
||||
const [err, setError] = useState('');
|
||||
const [previewUrl, setPreviewUrl] = useState('');
|
||||
const [link, setLink] = useState('');
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [isUploading, setUploading] = useState(false);
|
||||
|
||||
const handleDrag = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const handleDrag = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!e.dataTransfer) return;
|
||||
if (!e.dataTransfer) return;
|
||||
|
||||
const {files} = e.dataTransfer;
|
||||
const file = files[0];
|
||||
const { files } = e.dataTransfer;
|
||||
const file = files[0];
|
||||
|
||||
onFileChange(file);
|
||||
onFileChange(file);
|
||||
};
|
||||
|
||||
const onFileChange = async (file: File) => {
|
||||
setError('');
|
||||
|
||||
if (!/(^image)(\/)[a-zA-Z0-9_]*/.test(file.type)) {
|
||||
setError('invalid file type');
|
||||
return;
|
||||
}
|
||||
|
||||
const onFileChange = async (file: File) => {
|
||||
setError('');
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
if (!(/(^image)(\/)[a-zA-Z0-9_]*/).test(file.type)) {
|
||||
setError('invalid file type');
|
||||
return;
|
||||
}
|
||||
const isImageValid = await validateImage(url);
|
||||
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
const isImageValid = await validateImage(url);
|
||||
|
||||
if (!isImageValid) {
|
||||
setError('image cannot be previewed');
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(file);
|
||||
setLink('');
|
||||
setPreviewUrl(url);
|
||||
if (!isImageValid) {
|
||||
setError('image cannot be previewed');
|
||||
return;
|
||||
}
|
||||
|
||||
const onLinkEntered = useCallback(async () => {
|
||||
setError('');
|
||||
setFile(file);
|
||||
setLink('');
|
||||
setPreviewUrl(url);
|
||||
};
|
||||
|
||||
if (props.mustLinkBeImage) {
|
||||
const isImageValid = await validateImage(link);
|
||||
const onLinkEntered = useCallback(async () => {
|
||||
setError('');
|
||||
|
||||
if (!isImageValid) {
|
||||
setError('image cannot be previewed');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (props.mustLinkBeImage) {
|
||||
const isImageValid = await validateImage(link);
|
||||
|
||||
if (props.skipLinkPreview) {
|
||||
props.onAccept(link);
|
||||
if (!isImageValid) {
|
||||
setError('image cannot be previewed');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (props.skipLinkPreview) {
|
||||
props.onAccept(link);
|
||||
} else {
|
||||
setFile(null);
|
||||
setPreviewUrl(link);
|
||||
}
|
||||
}, [link]);
|
||||
|
||||
const onLinkChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
setLink(val);
|
||||
};
|
||||
|
||||
const onKeyPress = useCallback(
|
||||
async (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onLinkEntered();
|
||||
}
|
||||
},
|
||||
[onLinkEntered]
|
||||
);
|
||||
|
||||
const reset = () => {
|
||||
setPreviewUrl('');
|
||||
setLink('');
|
||||
setError('');
|
||||
};
|
||||
|
||||
const upload = useCallback(async () => {
|
||||
setUploading(true);
|
||||
|
||||
try {
|
||||
if (file) {
|
||||
if (file.size > maxFileSize) throw new Error('cannot exceed 5MB');
|
||||
const json: any = await dispatch(ipfsUploadOne(file));
|
||||
|
||||
if (!json.error) {
|
||||
const { url } = json.payload;
|
||||
props.onAccept(url);
|
||||
} else {
|
||||
setFile(null);
|
||||
setPreviewUrl(link);
|
||||
setError(json.payload);
|
||||
}
|
||||
}, [link]);
|
||||
} else if (link) {
|
||||
props.onAccept(link);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}, [file, link]);
|
||||
|
||||
const onLinkChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
setLink(val);
|
||||
useEffect(() => {
|
||||
if (!drop.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onKeyPress = useCallback(async (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onLinkEntered();
|
||||
}
|
||||
}, [onLinkEntered]);
|
||||
drop.current.addEventListener('dragover', handleDrag);
|
||||
drop.current.addEventListener('drop', handleDrop);
|
||||
|
||||
const reset = () => {
|
||||
setPreviewUrl('');
|
||||
setLink('');
|
||||
setError('');
|
||||
}
|
||||
return () => {
|
||||
if (drop.current) {
|
||||
drop.current.removeEventListener('dragover', handleDrag);
|
||||
drop.current.removeEventListener('drop', handleDrop);
|
||||
}
|
||||
};
|
||||
}, [drop.current]);
|
||||
|
||||
const upload = useCallback(async () => {
|
||||
setUploading(true);
|
||||
|
||||
try {
|
||||
if (file) {
|
||||
if (file.size > maxFileSize) throw new Error('cannot exceed 5MB');
|
||||
const json: any = await dispatch(ipfsUploadOne(file));
|
||||
|
||||
if (!json.error) {
|
||||
const {url} = json.payload;
|
||||
props.onAccept(url);
|
||||
} else {
|
||||
setError(json.payload);
|
||||
}
|
||||
} else if (link) {
|
||||
props.onAccept(link);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
|
||||
}, [file, link]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drop.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
drop.current.addEventListener('dragover', handleDrag);
|
||||
drop.current.addEventListener('drop', handleDrop);
|
||||
|
||||
return () => {
|
||||
if (drop.current) {
|
||||
drop.current.removeEventListener('dragover', handleDrag);
|
||||
drop.current.removeEventListener('drop', handleDrop);
|
||||
}
|
||||
}
|
||||
}, [drop.current]);
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={classNames('w-148 file-upload-modal', props.className)}
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<ModalHeader onClose={!previewUrl ? props.onClose : undefined}>
|
||||
Add a File
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
{
|
||||
previewUrl
|
||||
? (
|
||||
<div className="file-upload-modal__img-preview">
|
||||
<img
|
||||
src={previewUrl}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex flex-col items-center justify-center p-4">
|
||||
<div
|
||||
ref={drop}
|
||||
className="file-upload-modal__dd mt-4"
|
||||
>
|
||||
<div>Drag & Drop</div>
|
||||
<FileSelectButton
|
||||
className="my-2"
|
||||
accept="image/*"
|
||||
onChange={e => e.target.files && onFileChange(e.target.files[0])}
|
||||
/>
|
||||
<small>Maximum image size: 5MB</small>
|
||||
</div>
|
||||
<div className="m-4 font-semibold">or</div>
|
||||
<div className="relative w-full mb-4">
|
||||
<Input
|
||||
className="border"
|
||||
label="Paste a Link"
|
||||
onChange={onLinkChange}
|
||||
onKeyPress={onKeyPress}
|
||||
>
|
||||
{ validateLink(link) && (
|
||||
<Icon
|
||||
className="mx-4 text-blue-300 hover:text-blue-400"
|
||||
fa="fas fa-arrow-right"
|
||||
onClick={onLinkEntered}
|
||||
/>
|
||||
)}
|
||||
</Input>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{ err ? <div className="text-xs text-center text-red-500 mb-4">{err}</div> : null }
|
||||
</ModalContent>
|
||||
{
|
||||
previewUrl && (
|
||||
<ModalFooter>
|
||||
<Button
|
||||
btnType="secondary"
|
||||
onClick={reset}
|
||||
disabled={isUploading}
|
||||
>
|
||||
<Icon fa="fas fa-times" />
|
||||
</Button>
|
||||
<Button
|
||||
btnType="primary"
|
||||
className="ml-2"
|
||||
loading={isUploading}
|
||||
onClick={upload}
|
||||
>
|
||||
<Icon
|
||||
fa="fas fa-check"
|
||||
/>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
)
|
||||
}
|
||||
</Modal>
|
||||
)
|
||||
return (
|
||||
<Modal
|
||||
className={classNames('w-148 file-upload-modal', props.className)}
|
||||
onClose={props.onClose}>
|
||||
<ModalHeader onClose={!previewUrl ? props.onClose : undefined}>Add a File</ModalHeader>
|
||||
<ModalContent>
|
||||
{previewUrl ? (
|
||||
<div className="file-upload-modal__img-preview">
|
||||
<img src={previewUrl} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center p-4">
|
||||
<div ref={drop} className="file-upload-modal__dd mt-4">
|
||||
<div>Drag & Drop</div>
|
||||
<FileSelectButton
|
||||
className="my-2"
|
||||
accept="image/*"
|
||||
onChange={e => e.target.files && onFileChange(e.target.files[0])}
|
||||
/>
|
||||
<small>Maximum image size: 5MB</small>
|
||||
</div>
|
||||
<div className="m-4 font-semibold">or</div>
|
||||
<div className="relative w-full mb-4">
|
||||
<Input
|
||||
className="border"
|
||||
label="Paste a Link"
|
||||
onChange={onLinkChange}
|
||||
onKeyPress={onKeyPress}>
|
||||
{validateLink(link) && (
|
||||
<Icon
|
||||
className="mx-4 text-blue-300 hover:text-blue-400"
|
||||
fa="fas fa-arrow-right"
|
||||
onClick={onLinkEntered}
|
||||
/>
|
||||
)}
|
||||
</Input>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{err ? <div className="text-xs text-center text-red-500 mb-4">{err}</div> : null}
|
||||
</ModalContent>
|
||||
{previewUrl && (
|
||||
<ModalFooter>
|
||||
<Button btnType="secondary" onClick={reset} disabled={isUploading}>
|
||||
<Icon fa="fas fa-times" />
|
||||
</Button>
|
||||
<Button btnType="primary" className="ml-2" loading={isUploading} onClick={upload}>
|
||||
<Icon fa="fas fa-check" />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const validateLink = (link: string): boolean => {
|
||||
try {
|
||||
new URL(link);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
new URL(link);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const validateImage = async (link: string): Promise<boolean> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(true);
|
||||
img.onerror = () => resolve(false);
|
||||
img.src = link;
|
||||
});
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(true);
|
||||
img.onerror = () => resolve(false);
|
||||
img.src = link;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,104 +1,96 @@
|
||||
import React, {ReactElement, useEffect, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import Icon from "../Icon";
|
||||
import SpinnerGIF from "../../../static/icons/spinner.gif";
|
||||
import SwitchButton from "../SwitchButton";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import {UserRow} from "../DiscoverUserPanel";
|
||||
import Button from "../Button";
|
||||
import {useParams} from "react-router";
|
||||
import Web3 from "web3";
|
||||
import {fetchAddressByName, useUser} from "../../ducks/users";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {useSelectedLocalId} from "../../ducks/worker";
|
||||
import MemberInviteModal from "../MemberInviteModal";
|
||||
import config from "../../util/config";
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '../Icon';
|
||||
import SpinnerGIF from '../../../static/icons/spinner.gif';
|
||||
import SwitchButton from '../SwitchButton';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
import { UserRow } from '../DiscoverUserPanel';
|
||||
import Button from '../Button';
|
||||
import { useParams } from 'react-router';
|
||||
import Web3 from 'web3';
|
||||
import { fetchAddressByName, useUser } from '../../ducks/users';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useSelectedLocalId } from '../../ducks/worker';
|
||||
import MemberInviteModal from '../MemberInviteModal';
|
||||
import config from '../../util/config';
|
||||
|
||||
type Props = {
|
||||
|
||||
};
|
||||
type Props = {};
|
||||
|
||||
export default function GroupMembersPanel(props: Props): ReactElement {
|
||||
const theme = useThemeContext();
|
||||
const dispatch = useDispatch();
|
||||
const selected = useSelectedLocalId();
|
||||
const [username, setUsername] = useState('');
|
||||
const {name} = useParams<{name: string}>();
|
||||
const [users, setUsers] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showingInviteModal, showInviteModal] = useState(false);
|
||||
const isCurrentUser = selected?.address === username;
|
||||
const user = useUser(username);
|
||||
const theme = useThemeContext();
|
||||
const dispatch = useDispatch();
|
||||
const selected = useSelectedLocalId();
|
||||
const [username, setUsername] = useState('');
|
||||
const { name } = useParams<{ name: string }>();
|
||||
const [users, setUsers] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showingInviteModal, showInviteModal] = useState(false);
|
||||
const isCurrentUser = selected?.address === username;
|
||||
const user = useUser(username);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!Web3.utils.isAddress(name)) {
|
||||
const address: any = await dispatch(fetchAddressByName(name));
|
||||
setUsername(address);
|
||||
} else {
|
||||
setUsername(name);
|
||||
}
|
||||
})();
|
||||
}, [name]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!Web3.utils.isAddress(name)) {
|
||||
const address: any = await dispatch(fetchAddressByName(name));
|
||||
setUsername(address);
|
||||
} else {
|
||||
setUsername(name);
|
||||
}
|
||||
})();
|
||||
}, [name]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!username || !user?.group) return;
|
||||
const resp = await fetch(`${config.indexerAPI}/v1/group_members/custom_${username}`);
|
||||
const json = await resp.json();
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!username || !user?.group) return;
|
||||
const resp = await fetch(`${config.indexerAPI}/v1/group_members/custom_${username}`);
|
||||
const json = await resp.json();
|
||||
|
||||
if (!json.error) {
|
||||
setUsers(json.payload.map((member: any) => member.address));
|
||||
}
|
||||
})();
|
||||
}, [username, user]);
|
||||
if (!json.error) {
|
||||
setUsers(json.payload.map((member: any) => member.address));
|
||||
}
|
||||
})();
|
||||
}, [username, user]);
|
||||
|
||||
if (!user?.group) return <></>;
|
||||
if (!user?.group) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap flex-grow border rounded-xl mt-2',
|
||||
'meta-group post-mod-panel',
|
||||
{
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{ showingInviteModal && <MemberInviteModal onClose={() => showInviteModal(false)} /> }
|
||||
<div
|
||||
className={classNames(
|
||||
"px-4 py-2 font-bold text-lg border-b",
|
||||
'flex flew-row items-center',
|
||||
{
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<p>Members</p>
|
||||
{
|
||||
isCurrentUser && (
|
||||
<a
|
||||
className="flex flex-row flex-grow justify-end text-xs font-normal cursor-pointer"
|
||||
onClick={() => showInviteModal(true)}
|
||||
>
|
||||
Invite
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap py-1 max-h-80 overflow-y-auto">
|
||||
{ loading && <Icon className="self-center my-4" url={SpinnerGIF} size={3} /> }
|
||||
{users.map(address => <UserRow key={address} name={address} />)}
|
||||
{!users.length && (
|
||||
<div className="text-light flex flex-row justify-center py-2">
|
||||
No members yet :(
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap flex-grow border rounded-xl mt-2',
|
||||
'meta-group post-mod-panel',
|
||||
{
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
}
|
||||
)}>
|
||||
{showingInviteModal && <MemberInviteModal onClose={() => showInviteModal(false)} />}
|
||||
<div
|
||||
className={classNames(
|
||||
'px-4 py-2 font-bold text-lg border-b',
|
||||
'flex flew-row items-center',
|
||||
{
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
}
|
||||
)}>
|
||||
<p>Members</p>
|
||||
{isCurrentUser && (
|
||||
<a
|
||||
className="flex flex-row flex-grow justify-end text-xs font-normal cursor-pointer"
|
||||
onClick={() => showInviteModal(true)}>
|
||||
Invite
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap py-1 max-h-80 overflow-y-auto">
|
||||
{loading && <Icon className="self-center my-4" url={SpinnerGIF} size={3} />}
|
||||
{users.map(address => (
|
||||
<UserRow key={address} name={address} />
|
||||
))}
|
||||
{!users.length && (
|
||||
<div className="text-light flex flex-row justify-center py-2">No members yet :(</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.icon {
|
||||
@extend %row-nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,36 @@
|
||||
import React, {MouseEventHandler, ReactElement, ReactNode} from "react";
|
||||
import classNames from "classnames";
|
||||
import "./icon.scss";
|
||||
import React, { MouseEventHandler, ReactElement, ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import './icon.scss';
|
||||
|
||||
type Props = {
|
||||
url?: string;
|
||||
fa?: string;
|
||||
className?: string;
|
||||
size?: number;
|
||||
onClick?: MouseEventHandler;
|
||||
children?: ReactNode;
|
||||
}
|
||||
url?: string;
|
||||
fa?: string;
|
||||
className?: string;
|
||||
size?: number;
|
||||
onClick?: MouseEventHandler;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export default function Icon(props: Props): ReactElement {
|
||||
const {
|
||||
url,
|
||||
size = 1,
|
||||
className = '',
|
||||
fa,
|
||||
onClick,
|
||||
children,
|
||||
} = props;
|
||||
const { url, size = 1, className = '', fa, onClick, children } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-contain bg-center bg-no-repeat icon',
|
||||
{
|
||||
'cursor-pointer': onClick,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: url ? `url(${url})` : undefined,
|
||||
width: !fa ? `${size}rem` : undefined,
|
||||
height: !fa ? `${size}rem` : undefined,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{(!url && !!fa) && <i className={fa} style={{ fontSize: `${size}rem`}}/>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-contain bg-center bg-no-repeat icon',
|
||||
{
|
||||
'cursor-pointer': onClick,
|
||||
},
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: url ? `url(${url})` : undefined,
|
||||
width: !fa ? `${size}rem` : undefined,
|
||||
height: !fa ? `${size}rem` : undefined,
|
||||
}}
|
||||
onClick={onClick}>
|
||||
{!url && !!fa && <i className={fa} style={{ fontSize: `${size}rem` }} />}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,63 +1,60 @@
|
||||
import React, {BaseHTMLAttributes, ReactElement, ReactNode, useCallback, useRef, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import debounce from "lodash.debounce";
|
||||
import React, {
|
||||
BaseHTMLAttributes,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
onScrolledToBottom?: () => void;
|
||||
onScrolledToTop?: () => void;
|
||||
children?: ReactNode;
|
||||
bottomOffset?: number;
|
||||
topOffset?: number;
|
||||
className?: string;
|
||||
onScrolledToBottom?: () => void;
|
||||
onScrolledToTop?: () => void;
|
||||
children?: ReactNode;
|
||||
bottomOffset?: number;
|
||||
topOffset?: number;
|
||||
};
|
||||
|
||||
export default function InfiniteScrollable(props: Props): ReactElement {
|
||||
const {
|
||||
className,
|
||||
onScrolledToBottom,
|
||||
onScrolledToTop,
|
||||
children,
|
||||
bottomOffset = 0,
|
||||
topOffset = 0,
|
||||
} = props;
|
||||
const {
|
||||
className,
|
||||
onScrolledToBottom,
|
||||
onScrolledToTop,
|
||||
children,
|
||||
bottomOffset = 0,
|
||||
topOffset = 0,
|
||||
} = props;
|
||||
|
||||
const el = useRef<HTMLDivElement>(null);
|
||||
const el = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onScroll = useCallback(() => {
|
||||
const current = el.current;
|
||||
const onScroll = useCallback(() => {
|
||||
const current = el.current;
|
||||
|
||||
if (!current) return;
|
||||
if (!current) return;
|
||||
|
||||
const {
|
||||
scrollTop,
|
||||
offsetHeight,
|
||||
scrollHeight,
|
||||
} = current;
|
||||
const { scrollTop, offsetHeight, scrollHeight } = current;
|
||||
|
||||
if (onScrolledToBottom) {
|
||||
if ((scrollTop + offsetHeight) >= scrollHeight - bottomOffset) {
|
||||
onScrolledToBottom();
|
||||
}
|
||||
}
|
||||
if (onScrolledToBottom) {
|
||||
if (scrollTop + offsetHeight >= scrollHeight - bottomOffset) {
|
||||
onScrolledToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
if (onScrolledToTop) {
|
||||
if ((scrollTop + scrollHeight) <= offsetHeight + topOffset) {
|
||||
onScrolledToTop();
|
||||
}
|
||||
}
|
||||
if (onScrolledToTop) {
|
||||
if (scrollTop + scrollHeight <= offsetHeight + topOffset) {
|
||||
onScrolledToTop();
|
||||
}
|
||||
}
|
||||
}, [el, onScrolledToBottom, onScrolledToTop]);
|
||||
|
||||
const debouncedOnScroll = debounce(onScroll, 100, { leading: true });
|
||||
|
||||
}, [el, onScrolledToBottom, onScrolledToTop]);
|
||||
|
||||
const debouncedOnScroll = debounce(onScroll, 100, { leading: true })
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={el}
|
||||
className={classNames(className)}
|
||||
onScroll={debouncedOnScroll}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div ref={el} className={classNames(className)} onScroll={debouncedOnScroll}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,52 +1,39 @@
|
||||
import React, {InputHTMLAttributes, ReactElement, ReactNode} from "react";
|
||||
import classNames from "classnames";
|
||||
import "./input.scss";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import React, { InputHTMLAttributes, ReactElement, ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import './input.scss';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
errorMessage?: string;
|
||||
children?: ReactNode;
|
||||
label?: string;
|
||||
errorMessage?: string;
|
||||
children?: ReactNode;
|
||||
} & InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export default function Input(props: Props): ReactElement {
|
||||
const {
|
||||
label,
|
||||
errorMessage,
|
||||
className,
|
||||
children,
|
||||
...inputProps
|
||||
} = props;
|
||||
const { label, errorMessage, className, children, ...inputProps } = props;
|
||||
|
||||
const theme = useThemeContext();
|
||||
const theme = useThemeContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"rounded-lg input-group",
|
||||
className,
|
||||
{
|
||||
'input-group--readOnly': inputProps.readOnly,
|
||||
'focus-within:border-gray-400 ': !inputProps.readOnly && theme !== 'dark',
|
||||
'focus-within:border-gray-600': !inputProps.readOnly && theme === 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{ label && <div className="input-group__label">{label}</div> }
|
||||
<div
|
||||
className="w-full flex flex-row flex-nowrap items-center"
|
||||
>
|
||||
<input
|
||||
className={classNames("bg-transparent rounded-xl flex-grow flex-shrink", {
|
||||
'cursor-default': inputProps.readOnly,
|
||||
})}
|
||||
{...inputProps}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
className={classNames('rounded-lg input-group', className, {
|
||||
'input-group--readOnly': inputProps.readOnly,
|
||||
'focus-within:border-gray-400 ': !inputProps.readOnly && theme !== 'dark',
|
||||
'focus-within:border-gray-600': !inputProps.readOnly && theme === 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}>
|
||||
{label && <div className="input-group__label">{label}</div>}
|
||||
<div className="w-full flex flex-row flex-nowrap items-center">
|
||||
<input
|
||||
className={classNames('bg-transparent rounded-xl flex-grow flex-shrink', {
|
||||
'cursor-default': inputProps.readOnly,
|
||||
})}
|
||||
{...inputProps}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{ errorMessage && <small className="error-message text-red-500">{errorMessage}</small> }
|
||||
</div>
|
||||
)
|
||||
{errorMessage && <small className="error-message text-red-500">{errorMessage}</small>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.dark {
|
||||
.input-group {
|
||||
@@ -24,16 +24,16 @@
|
||||
background: white;
|
||||
color: $gray-800;
|
||||
display: inline-block;
|
||||
top: -.625rem;
|
||||
top: -0.625rem;
|
||||
width: fit-content;
|
||||
font-size: 12px;
|
||||
padding: 0 .25rem 0 .1875rem;
|
||||
margin-left: .625rem;
|
||||
padding: 0 0.25rem 0 0.1875rem;
|
||||
margin-left: 0.625rem;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: .5rem .75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
@@ -43,6 +43,6 @@
|
||||
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding-top: .25rem;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +1,93 @@
|
||||
import React, {ChangeEvent, KeyboardEvent, ReactElement, useCallback, useState} from "react";
|
||||
import Modal, {ModalContent, ModalFooter, ModalHeader} from "../Modal";
|
||||
import classNames from "classnames";
|
||||
import Input from "../Input";
|
||||
import Icon from "../Icon";
|
||||
import Button from "../Button";
|
||||
import URLPreview from "../URLPreview";
|
||||
import React, { ChangeEvent, KeyboardEvent, ReactElement, useCallback, useState } from 'react';
|
||||
import Modal, { ModalContent, ModalFooter, ModalHeader } from '../Modal';
|
||||
import classNames from 'classnames';
|
||||
import Input from '../Input';
|
||||
import Icon from '../Icon';
|
||||
import Button from '../Button';
|
||||
import URLPreview from '../URLPreview';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
onAccept: (url: string) => void;
|
||||
}
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
onAccept: (url: string) => void;
|
||||
};
|
||||
|
||||
export default function LinkInputModal(props: Props): ReactElement {
|
||||
const [err, setError] = useState('');
|
||||
const [previewUrl, setPreviewUrl] = useState('');
|
||||
const [link, setLink] = useState('');
|
||||
const [err, setError] = useState('');
|
||||
const [previewUrl, setPreviewUrl] = useState('');
|
||||
const [link, setLink] = useState('');
|
||||
|
||||
const onLinkEntered = useCallback(async () => {
|
||||
setError('');
|
||||
setPreviewUrl(link);
|
||||
}, [link]);
|
||||
|
||||
const onLinkEntered = useCallback(async () => {
|
||||
setError('');
|
||||
setPreviewUrl(link);
|
||||
}, [link]);
|
||||
const onLinkChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
setLink(val);
|
||||
};
|
||||
|
||||
const onLinkChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
setLink(val);
|
||||
}
|
||||
const onKeyPress = useCallback(
|
||||
async (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (validateLink(link) && e.key === 'Enter') {
|
||||
onLinkEntered();
|
||||
}
|
||||
},
|
||||
[onLinkEntered, link]
|
||||
);
|
||||
|
||||
const onKeyPress = useCallback(async (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (validateLink(link) && e.key === 'Enter') {
|
||||
onLinkEntered();
|
||||
}
|
||||
}, [onLinkEntered, link]);
|
||||
|
||||
const reset = () => {
|
||||
setPreviewUrl('');
|
||||
setLink('');
|
||||
setError('');
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
className={classNames('w-148 file-upload-modal', props.className)}
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<ModalHeader onClose={!previewUrl ? props.onClose : undefined}>
|
||||
Add a Link
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<div className="flex flex-col items-center justify-center p-4">
|
||||
<div className="relative w-full mb-4">
|
||||
<Input
|
||||
className="border"
|
||||
label="Enter link"
|
||||
onChange={onLinkChange}
|
||||
value={link}
|
||||
onKeyPress={onKeyPress}
|
||||
placeholder="http(s):// or magnet://..."
|
||||
>
|
||||
{ validateLink(link) && (
|
||||
<Icon
|
||||
className="mx-4 text-blue-300 hover:text-blue-400"
|
||||
fa="fas fa-binoculars"
|
||||
onClick={onLinkEntered}
|
||||
/>
|
||||
)}
|
||||
</Input>
|
||||
</div>
|
||||
{
|
||||
previewUrl && (
|
||||
<URLPreview className="w-full" url={previewUrl} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{ err ? <div className="text-xs text-center text-red-500 mb-4">{err}</div> : null }
|
||||
</ModalContent>
|
||||
{
|
||||
previewUrl && (
|
||||
<ModalFooter>
|
||||
<Button
|
||||
btnType="secondary"
|
||||
onClick={reset}
|
||||
>
|
||||
<Icon fa="fas fa-times" />
|
||||
</Button>
|
||||
<Button
|
||||
btnType="primary"
|
||||
className="ml-2"
|
||||
onClick={() => props.onAccept(link)}
|
||||
>
|
||||
<Icon
|
||||
fa="fas fa-check"
|
||||
/>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
)
|
||||
}
|
||||
</Modal>
|
||||
);
|
||||
const reset = () => {
|
||||
setPreviewUrl('');
|
||||
setLink('');
|
||||
setError('');
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
className={classNames('w-148 file-upload-modal', props.className)}
|
||||
onClose={props.onClose}>
|
||||
<ModalHeader onClose={!previewUrl ? props.onClose : undefined}>Add a Link</ModalHeader>
|
||||
<ModalContent>
|
||||
<div className="flex flex-col items-center justify-center p-4">
|
||||
<div className="relative w-full mb-4">
|
||||
<Input
|
||||
className="border"
|
||||
label="Enter link"
|
||||
onChange={onLinkChange}
|
||||
value={link}
|
||||
onKeyPress={onKeyPress}
|
||||
placeholder="http(s):// or magnet://...">
|
||||
{validateLink(link) && (
|
||||
<Icon
|
||||
className="mx-4 text-blue-300 hover:text-blue-400"
|
||||
fa="fas fa-binoculars"
|
||||
onClick={onLinkEntered}
|
||||
/>
|
||||
)}
|
||||
</Input>
|
||||
</div>
|
||||
{previewUrl && <URLPreview className="w-full" url={previewUrl} />}
|
||||
</div>
|
||||
{err ? <div className="text-xs text-center text-red-500 mb-4">{err}</div> : null}
|
||||
</ModalContent>
|
||||
{previewUrl && (
|
||||
<ModalFooter>
|
||||
<Button btnType="secondary" onClick={reset}>
|
||||
<Icon fa="fas fa-times" />
|
||||
</Button>
|
||||
<Button btnType="primary" className="ml-2" onClick={() => props.onAccept(link)}>
|
||||
<Icon fa="fas fa-check" />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const validateLink = (link: string): boolean => {
|
||||
try {
|
||||
new URL(link);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
new URL(link);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
import React, {ReactElement, useCallback, useState} from "react";
|
||||
import NotificationBox from "../NotificationBox";
|
||||
import Button from "../Button";
|
||||
import {addIdentity, getIdentities, getIdentityStatus, selectIdentity} from "../../serviceWorkers/util";
|
||||
import {useHasLocal, useLoggedIn} from "../../ducks/web3";
|
||||
import {useHistory} from "react-router";
|
||||
import {postWorkerMessage} from "../../util/sw";
|
||||
import {Identity} from "../../serviceWorkers/identity";
|
||||
import LoginModal from "../LoginModal";
|
||||
import {useSelectedLocalId} from "../../ducks/worker";
|
||||
import React, { ReactElement, useCallback, useState } from 'react';
|
||||
import NotificationBox from '../NotificationBox';
|
||||
import Button from '../Button';
|
||||
import {
|
||||
addIdentity,
|
||||
getIdentities,
|
||||
getIdentityStatus,
|
||||
selectIdentity,
|
||||
} from '../../serviceWorkers/util';
|
||||
import { useHasLocal, useLoggedIn } from '../../ducks/web3';
|
||||
import { useHistory } from 'react-router';
|
||||
import { postWorkerMessage } from '../../util/sw';
|
||||
import { Identity } from '../../serviceWorkers/identity';
|
||||
import LoginModal from '../LoginModal';
|
||||
import { useSelectedLocalId } from '../../ducks/worker';
|
||||
|
||||
export default function LocalBackupNotification(): ReactElement {
|
||||
const loggedIn = useLoggedIn();
|
||||
const hasLocalBackup = useHasLocal();
|
||||
const history = useHistory();
|
||||
const selected = useSelectedLocalId();
|
||||
const [showingLogin, setShowingLogin] = useState(false);
|
||||
const loggedIn = useLoggedIn();
|
||||
const hasLocalBackup = useHasLocal();
|
||||
const history = useHistory();
|
||||
const selected = useSelectedLocalId();
|
||||
const [showingLogin, setShowingLogin] = useState(false);
|
||||
|
||||
const onAddIdentity = useCallback(async () => {
|
||||
if (!selected) return;
|
||||
await postWorkerMessage(addIdentity(selected));
|
||||
await postWorkerMessage(selectIdentity(
|
||||
selected.type === 'gun'
|
||||
? selected.publicKey
|
||||
: selected.identityCommitment
|
||||
));
|
||||
}, [selected]);
|
||||
|
||||
const onCreateLocalBackup = useCallback(async () => {
|
||||
const identities = await postWorkerMessage<Identity[]>(getIdentities());
|
||||
const {unlocked} = await postWorkerMessage<{unlocked: boolean}>(getIdentityStatus());
|
||||
|
||||
if (!identities.length && !unlocked) {
|
||||
history.push('/create-local-backup');
|
||||
} else if (unlocked) {
|
||||
await onAddIdentity();
|
||||
} else {
|
||||
setShowingLogin(true);
|
||||
}
|
||||
}, [onAddIdentity]);
|
||||
|
||||
const onClose = useCallback(async () => {
|
||||
setShowingLogin(false);
|
||||
await onAddIdentity();
|
||||
}, [onAddIdentity])
|
||||
|
||||
if (!loggedIn || hasLocalBackup || selected?.type === 'zkpr_interrep') return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ showingLogin && <LoginModal onClose={onClose} /> }
|
||||
<NotificationBox className="text-center mb-2">
|
||||
You can store a secure backup of your identity locally - next time you login, you won't have to connect to your wallet and sign a message.
|
||||
<Button
|
||||
btnType="primary"
|
||||
className="p-2 mt-4 mx-auto"
|
||||
onClick={onCreateLocalBackup}
|
||||
>
|
||||
Make a local backup
|
||||
</Button>
|
||||
</NotificationBox>
|
||||
</>
|
||||
const onAddIdentity = useCallback(async () => {
|
||||
if (!selected) return;
|
||||
await postWorkerMessage(addIdentity(selected));
|
||||
await postWorkerMessage(
|
||||
selectIdentity(selected.type === 'gun' ? selected.publicKey : selected.identityCommitment)
|
||||
);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
const onCreateLocalBackup = useCallback(async () => {
|
||||
const identities = await postWorkerMessage<Identity[]>(getIdentities());
|
||||
const { unlocked } = await postWorkerMessage<{ unlocked: boolean }>(getIdentityStatus());
|
||||
|
||||
if (!identities.length && !unlocked) {
|
||||
history.push('/create-local-backup');
|
||||
} else if (unlocked) {
|
||||
await onAddIdentity();
|
||||
} else {
|
||||
setShowingLogin(true);
|
||||
}
|
||||
}, [onAddIdentity]);
|
||||
|
||||
const onClose = useCallback(async () => {
|
||||
setShowingLogin(false);
|
||||
await onAddIdentity();
|
||||
}, [onAddIdentity]);
|
||||
|
||||
if (!loggedIn || hasLocalBackup || selected?.type === 'zkpr_interrep') return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showingLogin && <LoginModal onClose={onClose} />}
|
||||
<NotificationBox className="text-center mb-2">
|
||||
You can store a secure backup of your identity locally - next time you login, you won't have
|
||||
to connect to your wallet and sign a message.
|
||||
<Button btnType="primary" className="p-2 mt-4 mx-auto" onClick={onCreateLocalBackup}>
|
||||
Make a local backup
|
||||
</Button>
|
||||
</NotificationBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,61 +1,57 @@
|
||||
import React, {ReactElement, useCallback, useState} from "react";
|
||||
import Modal, {ModalContent, ModalFooter, ModalHeader} from "../Modal";
|
||||
import Input from "../Input";
|
||||
import Button from "../Button";
|
||||
import {postWorkerMessage} from "../../util/sw";
|
||||
import {setPassphrase} from "../../serviceWorkers/util";
|
||||
import "./login-modal.scss";
|
||||
import React, { ReactElement, useCallback, useState } from 'react';
|
||||
import Modal, { ModalContent, ModalFooter, ModalHeader } from '../Modal';
|
||||
import Input from '../Input';
|
||||
import Button from '../Button';
|
||||
import { postWorkerMessage } from '../../util/sw';
|
||||
import { setPassphrase } from '../../serviceWorkers/util';
|
||||
import './login-modal.scss';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
onClose: () => void;
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
export default function LoginModal(props: Props): ReactElement {
|
||||
const [password, setPassword] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const onUnlock = useCallback(async () => {
|
||||
try {
|
||||
setErrorMessage('');
|
||||
await postWorkerMessage(setPassphrase(password));
|
||||
props.onSuccess && props.onSuccess();
|
||||
props.onClose();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setErrorMessage(e.message);
|
||||
}
|
||||
}, [password]);
|
||||
const onUnlock = useCallback(async () => {
|
||||
try {
|
||||
setErrorMessage('');
|
||||
await postWorkerMessage(setPassphrase(password));
|
||||
props.onSuccess && props.onSuccess();
|
||||
props.onClose();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setErrorMessage(e.message);
|
||||
}
|
||||
}, [password]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="w-96 login-modal"
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<ModalHeader onClose={props.onClose}>
|
||||
<b>{`Unlock with password`}</b>
|
||||
</ModalHeader>
|
||||
<ModalContent className="p-4 my-4">
|
||||
<Input
|
||||
className="border relative"
|
||||
label="Enter password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={e => {
|
||||
setPassword(e.target.value);
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</ModalContent>
|
||||
{ errorMessage && <div className="error-message text-xs text-center text-red-500 m-2">{errorMessage}</div> }
|
||||
<ModalFooter>
|
||||
<Button
|
||||
btnType="primary"
|
||||
onClick={onUnlock}
|
||||
>
|
||||
Unlock
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Modal className="w-96 login-modal" onClose={props.onClose}>
|
||||
<ModalHeader onClose={props.onClose}>
|
||||
<b>{`Unlock with password`}</b>
|
||||
</ModalHeader>
|
||||
<ModalContent className="p-4 my-4">
|
||||
<Input
|
||||
className="border relative"
|
||||
label="Enter password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={e => {
|
||||
setPassword(e.target.value);
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</ModalContent>
|
||||
{errorMessage && (
|
||||
<div className="error-message text-xs text-center text-red-500 m-2">{errorMessage}</div>
|
||||
)}
|
||||
<ModalFooter>
|
||||
<Button btnType="primary" onClick={onUnlock}>
|
||||
Unlock
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.login-modal {
|
||||
@media only screen and (max-width: 768px) {
|
||||
@@ -17,4 +17,4 @@
|
||||
max-height: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,168 +1,166 @@
|
||||
import React, {ChangeEvent, ReactElement, useCallback, useEffect, useState} from "react";
|
||||
import Modal, {ModalContent, ModalFooter, ModalHeader} from "../Modal";
|
||||
import Input from "../Input";
|
||||
import Button from "../Button";
|
||||
import "./member-invite-modal.scss";
|
||||
import {fetchAddressByName, searchUsers, useUser} from "../../ducks/users";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {useHistory} from "react-router";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import Web3 from "web3";
|
||||
import classNames from "classnames";
|
||||
import Avatar from "../Avatar";
|
||||
import {getHandle, getName} from "../../util/user";
|
||||
import Icon from "../Icon";
|
||||
import SpinnerGIF from "../../../static/icons/spinner.gif";
|
||||
import {submitConnection} from "../../ducks/drafts";
|
||||
import {ConnectionMessageSubType} from "../../util/message";
|
||||
import React, { ChangeEvent, ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import Modal, { ModalContent, ModalFooter, ModalHeader } from '../Modal';
|
||||
import Input from '../Input';
|
||||
import Button from '../Button';
|
||||
import './member-invite-modal.scss';
|
||||
import { fetchAddressByName, searchUsers, useUser } from '../../ducks/users';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
import Web3 from 'web3';
|
||||
import classNames from 'classnames';
|
||||
import Avatar from '../Avatar';
|
||||
import { getHandle, getName } from '../../util/user';
|
||||
import Icon from '../Icon';
|
||||
import SpinnerGIF from '../../../static/icons/spinner.gif';
|
||||
import { submitConnection } from '../../ducks/drafts';
|
||||
import { ConnectionMessageSubType } from '../../util/message';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function MemberInviteModal(props: Props): ReactElement {
|
||||
const [value, setValue] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [results, setResults] = useState<string[]>([]);
|
||||
const [users, setUsers] = useState<{[address: string]: string}>({});
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
const [value, setValue] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [results, setResults] = useState<string[]>([]);
|
||||
const [users, setUsers] = useState<{ [address: string]: string }>({});
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onInputChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(e.target.value);
|
||||
}, []);
|
||||
const onInputChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(e.target.value);
|
||||
}, []);
|
||||
|
||||
const toggleInvite = useCallback((address: string) => {
|
||||
const ret = {
|
||||
...users,
|
||||
};
|
||||
const toggleInvite = useCallback(
|
||||
(address: string) => {
|
||||
const ret = {
|
||||
...users,
|
||||
};
|
||||
|
||||
if (ret[address]) {
|
||||
delete ret[address];
|
||||
} else {
|
||||
ret[address] = address;
|
||||
}
|
||||
if (ret[address]) {
|
||||
delete ret[address];
|
||||
} else {
|
||||
ret[address] = address;
|
||||
}
|
||||
|
||||
setUsers(ret);
|
||||
}, [users]);
|
||||
setUsers(ret);
|
||||
},
|
||||
[users]
|
||||
);
|
||||
|
||||
const onSendInvite = useCallback(async () => {
|
||||
setSubmitting(true);
|
||||
const onSendInvite = useCallback(async () => {
|
||||
setSubmitting(true);
|
||||
|
||||
const addresses = Object.keys(users);
|
||||
const addresses = Object.keys(users);
|
||||
|
||||
for (let i = 0; i < addresses.length; i++) {
|
||||
await dispatch(submitConnection(addresses[i], ConnectionMessageSubType.MemberInvite));
|
||||
}
|
||||
for (let i = 0; i < addresses.length; i++) {
|
||||
await dispatch(submitConnection(addresses[i], ConnectionMessageSubType.MemberInvite));
|
||||
}
|
||||
|
||||
setSubmitting(false);
|
||||
props.onClose();
|
||||
}, [users]);
|
||||
setSubmitting(false);
|
||||
props.onClose();
|
||||
}, [users]);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
setLoading(true);
|
||||
const list: any = await dispatch(searchUsers(value));
|
||||
setResults(list.map(({ address }: any) => address));
|
||||
setLoading(false);
|
||||
})();
|
||||
}, [value]);
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
setLoading(true);
|
||||
const list: any = await dispatch(searchUsers(value));
|
||||
setResults(list.map(({ address }: any) => address));
|
||||
setLoading(false);
|
||||
})();
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="w-96"
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<ModalHeader onClose={props.onClose}>
|
||||
<b>Invite Members</b>
|
||||
</ModalHeader>
|
||||
<ModalContent className="p-2 my-2">
|
||||
<div className="flex flex-col items-center w-full">
|
||||
<Input
|
||||
className="border relative mb-4 w-full"
|
||||
type="text"
|
||||
label="Search by name or address"
|
||||
onChange={onInputChange}
|
||||
value={value}
|
||||
/>
|
||||
{ loading && <Icon className="self-center my-4 w-full" url={SpinnerGIF} size={3} /> }
|
||||
{results.map(address => (
|
||||
<UserRow
|
||||
key={address}
|
||||
name={address}
|
||||
toggleInvite={() => toggleInvite(address)}
|
||||
selected={!!users[address]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ModalContent>
|
||||
{ errorMessage && <div className="error-message text-xs text-center text-red-500 m-2">{errorMessage}</div> }
|
||||
<ModalFooter>
|
||||
<Button
|
||||
btnType="primary"
|
||||
onClick={onSendInvite}
|
||||
disabled={!Object.keys(users).length}
|
||||
loading={submitting}
|
||||
>
|
||||
{`Send ${Object.keys(users).length} Invitations`}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Modal className="w-96" onClose={props.onClose}>
|
||||
<ModalHeader onClose={props.onClose}>
|
||||
<b>Invite Members</b>
|
||||
</ModalHeader>
|
||||
<ModalContent className="p-2 my-2">
|
||||
<div className="flex flex-col items-center w-full">
|
||||
<Input
|
||||
className="border relative mb-4 w-full"
|
||||
type="text"
|
||||
label="Search by name or address"
|
||||
onChange={onInputChange}
|
||||
value={value}
|
||||
/>
|
||||
{loading && <Icon className="self-center my-4 w-full" url={SpinnerGIF} size={3} />}
|
||||
{results.map(address => (
|
||||
<UserRow
|
||||
key={address}
|
||||
name={address}
|
||||
toggleInvite={() => toggleInvite(address)}
|
||||
selected={!!users[address]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ModalContent>
|
||||
{errorMessage && (
|
||||
<div className="error-message text-xs text-center text-red-500 m-2">{errorMessage}</div>
|
||||
)}
|
||||
<ModalFooter>
|
||||
<Button
|
||||
btnType="primary"
|
||||
onClick={onSendInvite}
|
||||
disabled={!Object.keys(users).length}
|
||||
loading={submitting}>
|
||||
{`Send ${Object.keys(users).length} Invitations`}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function UserRow(props: {name: string; toggleInvite: () => void; selected: boolean}): ReactElement {
|
||||
const history = useHistory();
|
||||
const [username, setUsername] = useState('');
|
||||
function UserRow(props: {
|
||||
name: string;
|
||||
toggleInvite: () => void;
|
||||
selected: boolean;
|
||||
}): ReactElement {
|
||||
const history = useHistory();
|
||||
const [username, setUsername] = useState('');
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const user = useUser(username);
|
||||
const theme = useThemeContext();
|
||||
const dispatch = useDispatch();
|
||||
const user = useUser(username);
|
||||
const theme = useThemeContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!Web3.utils.isAddress(props.name)) {
|
||||
const address: any = await dispatch(fetchAddressByName(props.name));
|
||||
setUsername(address);
|
||||
} else {
|
||||
setUsername(props.name);
|
||||
}
|
||||
})();
|
||||
}, [props.name]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!Web3.utils.isAddress(props.name)) {
|
||||
const address: any = await dispatch(fetchAddressByName(props.name));
|
||||
setUsername(address);
|
||||
} else {
|
||||
setUsername(props.name);
|
||||
}
|
||||
})();
|
||||
}, [props.name]);
|
||||
|
||||
if (!user) return <></>;
|
||||
if (!user) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-nowrap px-4 py-2 cursor-pointer",
|
||||
"items-center transition w-full",
|
||||
{
|
||||
"hover:bg-gray-200": theme !== 'dark',
|
||||
"hover:bg-gray-800": theme === 'dark',
|
||||
}
|
||||
)}
|
||||
onClick={props.toggleInvite}
|
||||
>
|
||||
<Avatar address={user.address} className="w-10 h-10 mr-3" />
|
||||
<div className="flex flex-col flex-nowrap flex-grow justify-center">
|
||||
<div className="font-bold text-md">
|
||||
{getName(user, 8, 6)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
@{getHandle(user, 8, 6)}
|
||||
</div>
|
||||
</div>
|
||||
<Icon
|
||||
className={classNames(
|
||||
{
|
||||
'opacity-10': !user.meta?.inviteSent && !props.selected,
|
||||
'opacity-100 text-primary-color': user.meta?.inviteSent || props.selected,
|
||||
},
|
||||
)}
|
||||
fa="fas fa-check"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap px-4 py-2 cursor-pointer',
|
||||
'items-center transition w-full',
|
||||
{
|
||||
'hover:bg-gray-200': theme !== 'dark',
|
||||
'hover:bg-gray-800': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
onClick={props.toggleInvite}>
|
||||
<Avatar address={user.address} className="w-10 h-10 mr-3" />
|
||||
<div className="flex flex-col flex-nowrap flex-grow justify-center">
|
||||
<div className="font-bold text-md">{getName(user, 8, 6)}</div>
|
||||
<div className="text-sm text-gray-500">@{getHandle(user, 8, 6)}</div>
|
||||
</div>
|
||||
<Icon
|
||||
className={classNames({
|
||||
'opacity-10': !user.meta?.inviteSent && !props.selected,
|
||||
'opacity-100 text-primary-color': user.meta?.inviteSent || props.selected,
|
||||
})}
|
||||
fa="fas fa-check"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.member-invite-modal {
|
||||
@media only screen and (max-width: 768px) {
|
||||
height: 24rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,201 +1,199 @@
|
||||
import React, {
|
||||
MouseEvent,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
ReactNodeArray,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState
|
||||
} from "react";
|
||||
import classNames from "classnames";
|
||||
import "./menuable.scss";
|
||||
import Icon from "../Icon";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
MouseEvent,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
ReactNodeArray,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import './menuable.scss';
|
||||
import Icon from '../Icon';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
type MenuableProps = {
|
||||
items: ItemProps[];
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
menuClassName?: string;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
opened?: boolean;
|
||||
}
|
||||
items: ItemProps[];
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
menuClassName?: string;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
opened?: boolean;
|
||||
};
|
||||
|
||||
export type ItemProps = {
|
||||
label: string;
|
||||
iconUrl?: string;
|
||||
iconFA?: string;
|
||||
iconClassName?: string;
|
||||
className?: string;
|
||||
onClick?: (e: MouseEvent, reset: () => void) => void;
|
||||
disabled?: boolean;
|
||||
children?: ItemProps[];
|
||||
component?: ReactNode;
|
||||
}
|
||||
label: string;
|
||||
iconUrl?: string;
|
||||
iconFA?: string;
|
||||
iconClassName?: string;
|
||||
className?: string;
|
||||
onClick?: (e: MouseEvent, reset: () => void) => void;
|
||||
disabled?: boolean;
|
||||
children?: ItemProps[];
|
||||
component?: ReactNode;
|
||||
};
|
||||
|
||||
export default function Menuable(props: MenuableProps): ReactElement {
|
||||
const {
|
||||
opened,
|
||||
} = props;
|
||||
const { opened } = props;
|
||||
|
||||
const [isShowing, setShowing] = useState(!!props.opened);
|
||||
const [path, setPath] = useState<number[]>([]);
|
||||
const theme = useThemeContext();
|
||||
const [isShowing, setShowing] = useState(!!props.opened);
|
||||
const [path, setPath] = useState<number[]>([]);
|
||||
const theme = useThemeContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof opened !== 'undefined') {
|
||||
setShowing(opened);
|
||||
if (!opened) {
|
||||
setPath([]);
|
||||
}
|
||||
}
|
||||
}, [opened]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
props.onClose && props.onClose();
|
||||
setShowing(false);
|
||||
}, []);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
props.onOpen && props.onOpen();
|
||||
setShowing(true);
|
||||
|
||||
const cb = () => {
|
||||
onClose();
|
||||
window.removeEventListener('click', cb);
|
||||
};
|
||||
|
||||
window.addEventListener('click', cb);
|
||||
}, [onClose]);
|
||||
|
||||
const goBack = useCallback((e: any) => {
|
||||
e.stopPropagation();
|
||||
const newPath = [...path];
|
||||
newPath.pop();
|
||||
setPath(newPath);
|
||||
}, [path]);
|
||||
|
||||
const onItemClick = useCallback((e: any, item: ItemProps, i: number) => {
|
||||
e.stopPropagation();
|
||||
if (item.disabled) return;
|
||||
if (item.children) {
|
||||
setPath([...path, i]);
|
||||
} else if (item.onClick) {
|
||||
item.onClick(e, () => setPath([]));
|
||||
}
|
||||
}, [path]);
|
||||
|
||||
let items: ItemProps[] = props.items;
|
||||
|
||||
if (path) {
|
||||
for (const pathIndex of path) {
|
||||
if (items[pathIndex].children) {
|
||||
items = items[pathIndex].children as ItemProps[];
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (typeof opened !== 'undefined') {
|
||||
setShowing(opened);
|
||||
if (!opened) {
|
||||
setPath([]);
|
||||
}
|
||||
}
|
||||
}, [opened]);
|
||||
|
||||
return (
|
||||
const onClose = useCallback(() => {
|
||||
props.onClose && props.onClose();
|
||||
setShowing(false);
|
||||
}, []);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
props.onOpen && props.onOpen();
|
||||
setShowing(true);
|
||||
|
||||
const cb = () => {
|
||||
onClose();
|
||||
window.removeEventListener('click', cb);
|
||||
};
|
||||
|
||||
window.addEventListener('click', cb);
|
||||
}, [onClose]);
|
||||
|
||||
const goBack = useCallback(
|
||||
(e: any) => {
|
||||
e.stopPropagation();
|
||||
const newPath = [...path];
|
||||
newPath.pop();
|
||||
setPath(newPath);
|
||||
},
|
||||
[path]
|
||||
);
|
||||
|
||||
const onItemClick = useCallback(
|
||||
(e: any, item: ItemProps, i: number) => {
|
||||
e.stopPropagation();
|
||||
if (item.disabled) return;
|
||||
if (item.children) {
|
||||
setPath([...path, i]);
|
||||
} else if (item.onClick) {
|
||||
item.onClick(e, () => setPath([]));
|
||||
}
|
||||
},
|
||||
[path]
|
||||
);
|
||||
|
||||
let items: ItemProps[] = props.items;
|
||||
|
||||
if (path) {
|
||||
for (const pathIndex of path) {
|
||||
if (items[pathIndex].children) {
|
||||
items = items[pathIndex].children as ItemProps[];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'menuable',
|
||||
{
|
||||
'menuable--active': isShowing,
|
||||
},
|
||||
props.className
|
||||
)}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (isShowing) return onClose();
|
||||
onOpen();
|
||||
}}>
|
||||
{props.children}
|
||||
{isShowing && (
|
||||
<div
|
||||
className={classNames('menuable', {
|
||||
'menuable--active': isShowing,
|
||||
}, props.className)}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (isShowing) return onClose();
|
||||
onOpen();
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
className={classNames(
|
||||
'rounded-xl border menuable__menu',
|
||||
{
|
||||
isShowing && (
|
||||
<div className={classNames(
|
||||
"rounded-xl border menuable__menu",
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
},
|
||||
props.menuClassName
|
||||
)}>
|
||||
{!!path.length && (
|
||||
<div
|
||||
className={classNames(
|
||||
'text-sm whitespace-nowrap cursor-pointer',
|
||||
'flex flex-row flex-nowrap items-center',
|
||||
'menuable__menu__item',
|
||||
{
|
||||
'text-gray-500 hover:text-gray-800 hover:bg-gray-50 ': theme !== 'dark',
|
||||
'text-gray-500 hover:text-gray-200 hover:bg-gray-900 ': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
onClick={goBack}>
|
||||
<Icon fa="fas fa-caret-left" />
|
||||
<span className="ml-2">Go back</span>
|
||||
</div>
|
||||
)}
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={classNames(
|
||||
'text-sm whitespace-nowrap',
|
||||
'flex flex-row flex-nowrap items-center',
|
||||
'menuable__menu__item',
|
||||
{
|
||||
'hover:bg-gray-50 ': !item.disabled && theme !== 'dark',
|
||||
'hover:bg-gray-900 ': !item.disabled && theme === 'dark',
|
||||
'text-gray-500 hover:text-gray-800':
|
||||
!item.component && theme !== 'dark' && !item.disabled,
|
||||
'text-gray-500 hover:text-gray-200':
|
||||
!item.component && theme === 'dark' && !item.disabled,
|
||||
},
|
||||
{
|
||||
'cursor-pointer': !item.disabled,
|
||||
'cursor-default': item.disabled,
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
onClick={e => onItemClick(e, item, i)}>
|
||||
{item.component ? (
|
||||
item.component
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={classNames('menuable__menu__item__label flex-grow', {
|
||||
'hover:font-semibold': !item.disabled,
|
||||
'opacity-50': item.disabled,
|
||||
})}>
|
||||
{item.label}
|
||||
</div>
|
||||
{(item.iconUrl || item.iconFA) && (
|
||||
<Icon
|
||||
fa={item.iconFA}
|
||||
url={item.iconUrl}
|
||||
className={classNames(
|
||||
'ml-4',
|
||||
{
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
'opacity-50': item.disabled,
|
||||
},
|
||||
props.menuClassName
|
||||
)}>
|
||||
{!!path.length && (
|
||||
<div
|
||||
className={classNames(
|
||||
"text-sm whitespace-nowrap cursor-pointer",
|
||||
'flex flex-row flex-nowrap items-center',
|
||||
"menuable__menu__item",
|
||||
{
|
||||
'text-gray-500 hover:text-gray-800 hover:bg-gray-50 ': theme !== 'dark',
|
||||
'text-gray-500 hover:text-gray-200 hover:bg-gray-900 ': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
onClick={goBack}
|
||||
>
|
||||
<Icon fa="fas fa-caret-left" />
|
||||
<span className="ml-2">Go back</span>
|
||||
</div>
|
||||
)}
|
||||
{items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={classNames(
|
||||
"text-sm whitespace-nowrap",
|
||||
'flex flex-row flex-nowrap items-center',
|
||||
"menuable__menu__item",
|
||||
{
|
||||
'hover:bg-gray-50 ': !item.disabled && theme !== 'dark',
|
||||
'hover:bg-gray-900 ': !item.disabled && theme === 'dark',
|
||||
'text-gray-500 hover:text-gray-800': !item.component && (theme !== 'dark' && !item.disabled),
|
||||
'text-gray-500 hover:text-gray-200': !item.component && (theme === 'dark' && !item.disabled),
|
||||
},
|
||||
{
|
||||
'cursor-pointer': !item.disabled,
|
||||
'cursor-default': item.disabled,
|
||||
},
|
||||
item.className,
|
||||
)}
|
||||
onClick={e => onItemClick(e, item, i)}
|
||||
>
|
||||
{
|
||||
item.component
|
||||
? item.component
|
||||
: (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
"menuable__menu__item__label flex-grow",
|
||||
{
|
||||
'hover:font-semibold': !item.disabled,
|
||||
'opacity-50': item.disabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{ item.label }
|
||||
</div>
|
||||
{
|
||||
(item.iconUrl || item.iconFA) && (
|
||||
<Icon
|
||||
fa={item.iconFA}
|
||||
url={item.iconUrl}
|
||||
className={classNames(
|
||||
'ml-4',
|
||||
{
|
||||
'opacity-50': item.disabled,
|
||||
},
|
||||
item.iconClassName,
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
item.iconClassName
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.dark {
|
||||
.menuable {
|
||||
@@ -13,18 +13,18 @@
|
||||
|
||||
&__menu {
|
||||
position: absolute;
|
||||
margin-top: .5rem;
|
||||
margin-top: 0.5rem;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: $white;
|
||||
border-radius: .75rem;
|
||||
padding: .5rem 0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.5rem 0;
|
||||
z-index: 400;
|
||||
|
||||
&__item {
|
||||
@extend %row-nowrap;
|
||||
align-content: center;
|
||||
padding: .5rem 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,4 +49,4 @@
|
||||
color: $red-800;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,128 +1,103 @@
|
||||
import React, {ReactElement} from "react";
|
||||
import {Route, Switch} from "react-router";
|
||||
import DiscoverUserPanel from "../DiscoverUserPanel";
|
||||
import DiscoverTagPanel from "../DiscoverTagPanel";
|
||||
import PostModerationPanel from "../PostModerationPanel";
|
||||
import Icon from "../Icon";
|
||||
import classNames from "classnames";
|
||||
import GroupMembersPanel from "../GroupMembersPanel";
|
||||
import React, { ReactElement } from 'react';
|
||||
import { Route, Switch } from 'react-router';
|
||||
import DiscoverUserPanel from '../DiscoverUserPanel';
|
||||
import DiscoverTagPanel from '../DiscoverTagPanel';
|
||||
import PostModerationPanel from '../PostModerationPanel';
|
||||
import Icon from '../Icon';
|
||||
import classNames from 'classnames';
|
||||
import GroupMembersPanel from '../GroupMembersPanel';
|
||||
|
||||
export default function MetaPanel(props: {
|
||||
className?: string;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/explore">
|
||||
<DefaultMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/:name/status/:hash">
|
||||
<PostMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/post/:hash">
|
||||
<PostMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/tag/:tagName">
|
||||
<DefaultMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/home">
|
||||
<DefaultMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/notifications" />
|
||||
<Route path="/settings" />
|
||||
<Route path="/create-local-backup" />
|
||||
<Route path="/onboarding/interrep" />
|
||||
<Route path="/connect/twitter" />
|
||||
<Route path="/signup" />
|
||||
<Route path="/chat" />
|
||||
<Route path="/:name">
|
||||
<ProfileMetaPanels className={props.className} />
|
||||
</Route>
|
||||
</Switch>
|
||||
)
|
||||
export default function MetaPanel(props: { className?: string }): ReactElement {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/explore">
|
||||
<DefaultMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/:name/status/:hash">
|
||||
<PostMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/post/:hash">
|
||||
<PostMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/tag/:tagName">
|
||||
<DefaultMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/home">
|
||||
<DefaultMetaPanels className={props.className} />
|
||||
</Route>
|
||||
<Route path="/notifications" />
|
||||
<Route path="/settings" />
|
||||
<Route path="/create-local-backup" />
|
||||
<Route path="/onboarding/interrep" />
|
||||
<Route path="/connect/twitter" />
|
||||
<Route path="/signup" />
|
||||
<Route path="/chat" />
|
||||
<Route path="/:name">
|
||||
<ProfileMetaPanels className={props.className} />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function DefaultMetaPanels(props: {
|
||||
className?: string;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className={classNames("app__meta-content", props.className)}>
|
||||
<DiscoverUserPanel key="discover-user" />
|
||||
<DiscoverTagPanel key="discover-tag" />
|
||||
<AppFooter />
|
||||
</div>
|
||||
);
|
||||
function DefaultMetaPanels(props: { className?: string }): ReactElement {
|
||||
return (
|
||||
<div className={classNames('app__meta-content', props.className)}>
|
||||
<DiscoverUserPanel key="discover-user" />
|
||||
<DiscoverTagPanel key="discover-tag" />
|
||||
<AppFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileMetaPanels(props: {
|
||||
className?: string;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className={classNames("app__meta-content", props.className)}>
|
||||
<GroupMembersPanel />
|
||||
<DiscoverUserPanel key="discover-user" />
|
||||
<DiscoverTagPanel key="discover-tag" />
|
||||
<AppFooter />
|
||||
</div>
|
||||
);
|
||||
function ProfileMetaPanels(props: { className?: string }): ReactElement {
|
||||
return (
|
||||
<div className={classNames('app__meta-content', props.className)}>
|
||||
<GroupMembersPanel />
|
||||
<DiscoverUserPanel key="discover-user" />
|
||||
<DiscoverTagPanel key="discover-tag" />
|
||||
<AppFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PostMetaPanels(props: {
|
||||
className?: string;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className={classNames("app__meta-content", props.className)}>
|
||||
<PostModerationPanel />
|
||||
<DiscoverUserPanel key="discover-user" />
|
||||
<DiscoverTagPanel key="discover-tag" />
|
||||
<AppFooter />
|
||||
</div>
|
||||
);
|
||||
function PostMetaPanels(props: { className?: string }): ReactElement {
|
||||
return (
|
||||
<div className={classNames('app__meta-content', props.className)}>
|
||||
<PostModerationPanel />
|
||||
<DiscoverUserPanel key="discover-user" />
|
||||
<DiscoverTagPanel key="discover-tag" />
|
||||
<AppFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AppFooter(): ReactElement {
|
||||
return (
|
||||
<div className="app__meta-content__footer p-2 my-2 flex flex-row">
|
||||
<div className="text-gray-500 text-xs flex flex-row flex-nowrap mr-4 mb-4 items-center">
|
||||
<Icon className="mr-2" fa="fas fa-book" />
|
||||
<a
|
||||
className="text-gray-500"
|
||||
href="https://docs.zkitter.com"
|
||||
target="_blank"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs flex flex-row flex-nowrap mr-4 mb-4 items-center">
|
||||
<Icon className="mr-2" fa="fab fa-github" />
|
||||
<a
|
||||
className="text-gray-500"
|
||||
href="https://github.com/zkitter"
|
||||
target="_blank"
|
||||
>
|
||||
Github
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs flex flex-row flex-nowrap mr-4 mb-4 items-center">
|
||||
<Icon className="mr-2" fa="fab fa-twitter" />
|
||||
<a
|
||||
className="text-gray-500"
|
||||
href="https://twitter.com/zkitterdev"
|
||||
target="_blank"
|
||||
>
|
||||
Twitter
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs flex flex-row flex-nowrap mb-4 items-center">
|
||||
<Icon className="mr-2" fa="fab fa-discord" />
|
||||
<a
|
||||
className="text-gray-500"
|
||||
href="https://discord.com/invite/GVP9MghwXc"
|
||||
target="_blank"
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="app__meta-content__footer p-2 my-2 flex flex-row">
|
||||
<div className="text-gray-500 text-xs flex flex-row flex-nowrap mr-4 mb-4 items-center">
|
||||
<Icon className="mr-2" fa="fas fa-book" />
|
||||
<a className="text-gray-500" href="https://docs.zkitter.com" target="_blank">
|
||||
Docs
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs flex flex-row flex-nowrap mr-4 mb-4 items-center">
|
||||
<Icon className="mr-2" fa="fab fa-github" />
|
||||
<a className="text-gray-500" href="https://github.com/zkitter" target="_blank">
|
||||
Github
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs flex flex-row flex-nowrap mr-4 mb-4 items-center">
|
||||
<Icon className="mr-2" fa="fab fa-twitter" />
|
||||
<a className="text-gray-500" href="https://twitter.com/zkitterdev" target="_blank">
|
||||
Twitter
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs flex flex-row flex-nowrap mb-4 items-center">
|
||||
<Icon className="mr-2" fa="fab fa-discord" />
|
||||
<a className="text-gray-500" href="https://discord.com/invite/GVP9MghwXc" target="_blank">
|
||||
Discord
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,130 +1,112 @@
|
||||
import React, {MouseEventHandler, ReactElement, ReactNode, ReactNodeArray} from 'react';
|
||||
import React, { MouseEventHandler, ReactElement, ReactNode, ReactNodeArray } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './modal.scss';
|
||||
import Icon from "../Icon";
|
||||
import classNames from "classnames";
|
||||
import CancelSVG from "../../../static/icons/cancel.svg";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
|
||||
import Icon from '../Icon';
|
||||
import classNames from 'classnames';
|
||||
import CancelSVG from '../../../static/icons/cancel.svg';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
onClose: MouseEventHandler;
|
||||
children: ReactNode | ReactNode[];
|
||||
}
|
||||
className?: string;
|
||||
onClose: MouseEventHandler;
|
||||
children: ReactNode | ReactNode[];
|
||||
};
|
||||
|
||||
export default function Modal(props: Props): ReactElement {
|
||||
const { className, onClose, children } = props;
|
||||
const theme = useThemeContext();
|
||||
const { className, onClose, children } = props;
|
||||
const theme = useThemeContext();
|
||||
|
||||
const modalRoot = document.querySelector('#modal-root');
|
||||
const modalRoot = document.querySelector('#modal-root');
|
||||
|
||||
if (!modalRoot) return <></>;
|
||||
if (!modalRoot) return <></>;
|
||||
|
||||
// @ts-ignore
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-black bg-opacity-80',
|
||||
"modal__overlay",
|
||||
theme,
|
||||
)}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onClose && onClose(e);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
`modal__wrapper`,
|
||||
{
|
||||
'bg-white': theme !== 'dark',
|
||||
'bg-dark': theme === 'dark',
|
||||
},
|
||||
className,
|
||||
)}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
modalRoot,
|
||||
);
|
||||
// @ts-ignore
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
className={classNames('bg-black bg-opacity-80', 'modal__overlay', theme)}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onClose && onClose(e);
|
||||
}}>
|
||||
<div
|
||||
className={classNames(
|
||||
`modal__wrapper`,
|
||||
{
|
||||
'bg-white': theme !== 'dark',
|
||||
'bg-dark': theme === 'dark',
|
||||
},
|
||||
className
|
||||
)}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
modalRoot
|
||||
);
|
||||
}
|
||||
|
||||
type HeaderProps = {
|
||||
onClose?: () => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
onClose?: () => void;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function ModalHeader(props: HeaderProps): ReactElement {
|
||||
const theme = useThemeContext();
|
||||
return (
|
||||
<div className={classNames("border-b modal__header", {
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}>
|
||||
<div className="modal__header__title">
|
||||
{props.children}
|
||||
</div>
|
||||
<div className="modal__header__content">
|
||||
{
|
||||
props.onClose && (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row items-center justify-center",
|
||||
"p-2 rounded-full opacity-50",
|
||||
"hover:opacity-100",
|
||||
{
|
||||
'text-black': theme !== 'dark',
|
||||
'text-white': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
fa="fas fa-times"
|
||||
size={1}
|
||||
onClick={props.onClose}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const theme = useThemeContext();
|
||||
return (
|
||||
<div
|
||||
className={classNames('border-b modal__header', {
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}>
|
||||
<div className="modal__header__title">{props.children}</div>
|
||||
<div className="modal__header__content">
|
||||
{props.onClose && (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row items-center justify-center',
|
||||
'p-2 rounded-full opacity-50',
|
||||
'hover:opacity-100',
|
||||
{
|
||||
'text-black': theme !== 'dark',
|
||||
'text-white': theme === 'dark',
|
||||
}
|
||||
)}>
|
||||
<Icon fa="fas fa-times" size={1} onClick={props.onClose} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ContentProps = {
|
||||
children: ReactNode | ReactNodeArray;
|
||||
className?: string;
|
||||
}
|
||||
children: ReactNode | ReactNodeArray;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ModalContent(props: ContentProps): ReactElement {
|
||||
return (
|
||||
<div className={classNames("modal__content", props.className)}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
return <div className={classNames('modal__content', props.className)}>{props.children}</div>;
|
||||
}
|
||||
|
||||
type FooterProps = {
|
||||
children: ReactNode | ReactNodeArray;
|
||||
className?: string;
|
||||
}
|
||||
children: ReactNode | ReactNodeArray;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ModalFooter(props: FooterProps): ReactElement {
|
||||
const theme = useThemeContext();
|
||||
const theme = useThemeContext();
|
||||
|
||||
return (
|
||||
<div className={classNames(
|
||||
"border-t modal__footer",
|
||||
{
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
},
|
||||
props.className
|
||||
)}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'border-t modal__footer',
|
||||
{
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
},
|
||||
props.className
|
||||
)}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.modal {
|
||||
@extend %col-nowrap;
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
&__wrapper {
|
||||
margin: 3rem auto;
|
||||
border-radius: .5rem;
|
||||
border-radius: 0.5rem;
|
||||
z-index: 200;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
@extend %row-nowrap;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
padding: .5rem 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&__title {
|
||||
font-weight: 500;
|
||||
@@ -55,7 +55,7 @@
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: .8125rem;
|
||||
font-size: 0.8125rem;
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
@@ -68,4 +68,4 @@
|
||||
flex: 0 0 auto;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,139 +1,115 @@
|
||||
import React, {ReactElement, ReactNode, useEffect, useState} from "react";
|
||||
import Icon from "../Icon";
|
||||
import "./moderation-btn.scss";
|
||||
import classNames from "classnames";
|
||||
import Modal, {ModalContent, ModalFooter, ModalHeader} from "../Modal";
|
||||
import Button from "../Button";
|
||||
import {ModerationMessageSubType} from "../../util/message";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import React, { ReactElement, ReactNode, useEffect, useState } from 'react';
|
||||
import Icon from '../Icon';
|
||||
import './moderation-btn.scss';
|
||||
import classNames from 'classnames';
|
||||
import Modal, { ModalContent, ModalFooter, ModalHeader } from '../Modal';
|
||||
import Button from '../Button';
|
||||
import { ModerationMessageSubType } from '../../util/message';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
onChange?: (type: ModerationMessageSubType|null) => void;
|
||||
currentType?: ModerationMessageSubType|null;
|
||||
}
|
||||
className?: string;
|
||||
onChange?: (type: ModerationMessageSubType | null) => void;
|
||||
currentType?: ModerationMessageSubType | null;
|
||||
};
|
||||
|
||||
export default function ModerationButton(props: Props): ReactElement {
|
||||
const {
|
||||
className = '',
|
||||
currentType = null,
|
||||
onChange,
|
||||
} = props;
|
||||
const { className = '', currentType = null, onChange } = props;
|
||||
|
||||
const [showingModal, showModal] = useState(false);
|
||||
const [replyType, setReplyType] = useState<ModerationMessageSubType|null>(currentType);
|
||||
const theme = useThemeContext();
|
||||
const [showingModal, showModal] = useState(false);
|
||||
const [replyType, setReplyType] = useState<ModerationMessageSubType | null>(currentType);
|
||||
const theme = useThemeContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) onChange(replyType);
|
||||
}, [replyType]);
|
||||
useEffect(() => {
|
||||
if (onChange) onChange(replyType);
|
||||
}, [replyType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ showingModal && (
|
||||
<Modal className="moderation-modal" onClose={() => showModal(false)}>
|
||||
<ModalHeader
|
||||
onClose={() => showModal(false)}
|
||||
>
|
||||
<b>Thread Moderation</b>
|
||||
</ModalHeader>
|
||||
<ModalContent className="p-4">
|
||||
<div className="flex flex-col justify-center">
|
||||
{renderReplyOption(null, replyType, setReplyType)}
|
||||
{renderReplyOption(ModerationMessageSubType.ThreadBlock, replyType, setReplyType)}
|
||||
{renderReplyOption(ModerationMessageSubType.ThreadFollow, replyType, setReplyType)}
|
||||
{renderReplyOption(ModerationMessageSubType.ThreadMention, replyType, setReplyType)}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
<button
|
||||
className={classNames(
|
||||
"flex flex-row items-center text-blue-300 font-bold",
|
||||
{
|
||||
'hover:bg-blue-50 hover:text-blue-400': theme !== 'dark',
|
||||
'hover:bg-blue-900 hover:text-blue-600': theme === 'dark',
|
||||
},
|
||||
"moderation-btn",
|
||||
className,
|
||||
)}
|
||||
onClick={() => showModal(true)}
|
||||
>
|
||||
<Icon
|
||||
fa={getFA(replyType)}
|
||||
size={.875}
|
||||
/>
|
||||
<div
|
||||
className="text-sm ml-2 moderation-btn__label"
|
||||
>
|
||||
{getLabel(replyType)}
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
{showingModal && (
|
||||
<Modal className="moderation-modal" onClose={() => showModal(false)}>
|
||||
<ModalHeader onClose={() => showModal(false)}>
|
||||
<b>Thread Moderation</b>
|
||||
</ModalHeader>
|
||||
<ModalContent className="p-4">
|
||||
<div className="flex flex-col justify-center">
|
||||
{renderReplyOption(null, replyType, setReplyType)}
|
||||
{renderReplyOption(ModerationMessageSubType.ThreadBlock, replyType, setReplyType)}
|
||||
{renderReplyOption(ModerationMessageSubType.ThreadFollow, replyType, setReplyType)}
|
||||
{renderReplyOption(ModerationMessageSubType.ThreadMention, replyType, setReplyType)}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
<button
|
||||
className={classNames(
|
||||
'flex flex-row items-center text-blue-300 font-bold',
|
||||
{
|
||||
'hover:bg-blue-50 hover:text-blue-400': theme !== 'dark',
|
||||
'hover:bg-blue-900 hover:text-blue-600': theme === 'dark',
|
||||
},
|
||||
'moderation-btn',
|
||||
className
|
||||
)}
|
||||
onClick={() => showModal(true)}>
|
||||
<Icon fa={getFA(replyType)} size={0.875} />
|
||||
<div className="text-sm ml-2 moderation-btn__label">{getLabel(replyType)}</div>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderReplyOption(
|
||||
replyType: ModerationMessageSubType|null,
|
||||
active: ModerationMessageSubType|null,
|
||||
setReplyType: (replyType: ModerationMessageSubType|null) => void,
|
||||
replyType: ModerationMessageSubType | null,
|
||||
active: ModerationMessageSubType | null,
|
||||
setReplyType: (replyType: ModerationMessageSubType | null) => void
|
||||
): ReactNode {
|
||||
const fa = getFA(replyType);
|
||||
const label = getLabel(replyType);
|
||||
const fa = getFA(replyType);
|
||||
const label = getLabel(replyType);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row items-center p-2 moderation-btn__reply-option",
|
||||
{
|
||||
'moderation-btn__reply-option--active': active === replyType,
|
||||
'hover:text-blue-400': active !== replyType,
|
||||
}
|
||||
)}
|
||||
onClick={() => setReplyType(replyType)}
|
||||
>
|
||||
<Icon
|
||||
className={classNames(
|
||||
"p-2 rounded-full border border-2",
|
||||
{
|
||||
'border-gray-200 text-gray-400 hover:border-blue-400 hover:text-blue-400': active !== replyType,
|
||||
'border-blue-400 bg-blue-400 text-white': active === replyType,
|
||||
}
|
||||
)}
|
||||
fa={fa}
|
||||
size={.875}
|
||||
/>
|
||||
<div
|
||||
className="text-light ml-4"
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={classNames('flex flex-row items-center p-2 moderation-btn__reply-option', {
|
||||
'moderation-btn__reply-option--active': active === replyType,
|
||||
'hover:text-blue-400': active !== replyType,
|
||||
})}
|
||||
onClick={() => setReplyType(replyType)}>
|
||||
<Icon
|
||||
className={classNames('p-2 rounded-full border border-2', {
|
||||
'border-gray-200 text-gray-400 hover:border-blue-400 hover:text-blue-400':
|
||||
active !== replyType,
|
||||
'border-blue-400 bg-blue-400 text-white': active === replyType,
|
||||
})}
|
||||
fa={fa}
|
||||
size={0.875}
|
||||
/>
|
||||
<div className="text-light ml-4">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getFA(replyType: ModerationMessageSubType | null): string {
|
||||
switch (replyType) {
|
||||
case ModerationMessageSubType.ThreadBlock:
|
||||
return 'fas fa-shield-alt';
|
||||
case ModerationMessageSubType.ThreadFollow:
|
||||
return 'fas fa-user-check';
|
||||
case ModerationMessageSubType.ThreadMention:
|
||||
return 'fas fa-at';
|
||||
default:
|
||||
return 'fas fa-globe';
|
||||
}
|
||||
switch (replyType) {
|
||||
case ModerationMessageSubType.ThreadBlock:
|
||||
return 'fas fa-shield-alt';
|
||||
case ModerationMessageSubType.ThreadFollow:
|
||||
return 'fas fa-user-check';
|
||||
case ModerationMessageSubType.ThreadMention:
|
||||
return 'fas fa-at';
|
||||
default:
|
||||
return 'fas fa-globe';
|
||||
}
|
||||
}
|
||||
|
||||
function getLabel(replyType: ModerationMessageSubType | null): string {
|
||||
switch (replyType) {
|
||||
case ModerationMessageSubType.ThreadBlock:
|
||||
return 'Hide replies that you blocked';
|
||||
case ModerationMessageSubType.ThreadFollow:
|
||||
return 'Show replies that you followed or liked';
|
||||
case ModerationMessageSubType.ThreadMention:
|
||||
return 'Show replies from people you mentioned';
|
||||
default:
|
||||
return 'Show reply from everyone';
|
||||
}
|
||||
switch (replyType) {
|
||||
case ModerationMessageSubType.ThreadBlock:
|
||||
return 'Hide replies that you blocked';
|
||||
case ModerationMessageSubType.ThreadFollow:
|
||||
return 'Show replies that you followed or liked';
|
||||
case ModerationMessageSubType.ThreadMention:
|
||||
return 'Show replies from people you mentioned';
|
||||
default:
|
||||
return 'Show reply from everyone';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.moderation-btn {
|
||||
border-radius: 1rem;
|
||||
@@ -49,4 +49,4 @@
|
||||
max-height: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,154 +1,138 @@
|
||||
import React, {ReactElement, useEffect, useState} from "react";
|
||||
import {getUser, useUser} from "../../ducks/users";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {getName} from "../../util/user";
|
||||
import Icon from "../Icon";
|
||||
import TwitterPaper from "../../../static/icons/twitter-paper.png";
|
||||
import TwitterBronze from "../../../static/icons/twitter-bronze.png";
|
||||
import TwitterSilver from "../../../static/icons/twitter-silver.png";
|
||||
import TwitterGold from "../../../static/icons/twitter-gold.png";
|
||||
import Popoverable from "../Popoverable";
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import { getUser, useUser } from '../../ducks/users';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { getName } from '../../util/user';
|
||||
import Icon from '../Icon';
|
||||
import TwitterPaper from '../../../static/icons/twitter-paper.png';
|
||||
import TwitterBronze from '../../../static/icons/twitter-bronze.png';
|
||||
import TwitterSilver from '../../../static/icons/twitter-silver.png';
|
||||
import TwitterGold from '../../../static/icons/twitter-gold.png';
|
||||
import Popoverable from '../Popoverable';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
address?: string;
|
||||
interepProvider?: string;
|
||||
interepGroup?: string;
|
||||
group?: string | null;
|
||||
className?: string;
|
||||
address?: string;
|
||||
interepProvider?: string;
|
||||
interepGroup?: string;
|
||||
group?: string | null;
|
||||
};
|
||||
|
||||
const GROUP_TO_NICKNAME: {
|
||||
[group: string]: string;
|
||||
[group: string]: string;
|
||||
} = {
|
||||
'zksocial_all': 'Anonymous',
|
||||
'semaphore_taz_members': 'A TAZ Member',
|
||||
'interrep_twitter_unrated': 'A Twitter user',
|
||||
'interrep_twitter_bronze': 'A Twitter user with 500+ followers',
|
||||
'interrep_twitter_silver': 'A Twitter user with 2k+ followers',
|
||||
'interrep_twitter_gold': 'A Twitter user with 7k+ followers',
|
||||
}
|
||||
zksocial_all: 'Anonymous',
|
||||
semaphore_taz_members: 'A TAZ Member',
|
||||
interrep_twitter_unrated: 'A Twitter user',
|
||||
interrep_twitter_bronze: 'A Twitter user with 500+ followers',
|
||||
interrep_twitter_silver: 'A Twitter user with 2k+ followers',
|
||||
interrep_twitter_gold: 'A Twitter user with 7k+ followers',
|
||||
};
|
||||
|
||||
export default function Nickname(props: Props): ReactElement {
|
||||
const { address, interepProvider, interepGroup, className = '', group } = props;
|
||||
const [username, setUsername] = useState('');
|
||||
const user = useUser(username);
|
||||
const dispatch = useDispatch();
|
||||
const { address, interepProvider, interepGroup, className = '', group } = props;
|
||||
const [username, setUsername] = useState('');
|
||||
const user = useUser(username);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const badges = [];
|
||||
const [protocol, groupName] = props.group?.split('_') || [];
|
||||
const badges = [];
|
||||
const [protocol, groupName] = props.group?.split('_') || [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!user && address) {
|
||||
dispatch(getUser(address));
|
||||
} else if (!user && protocol === 'custom') {
|
||||
dispatch(getUser(groupName));
|
||||
}
|
||||
}, [user, address, groupName, protocol]);
|
||||
|
||||
useEffect(() => {
|
||||
if (protocol === 'custom') {
|
||||
setUsername(groupName);
|
||||
} else if (address) {
|
||||
setUsername(address);
|
||||
}
|
||||
}, [address, groupName, protocol]);
|
||||
|
||||
|
||||
if (user) {
|
||||
return (
|
||||
<div className={`flex flex-row flex-nowrap items-center ${className}`}>
|
||||
{getName(user)}
|
||||
</div>
|
||||
)
|
||||
useEffect(() => {
|
||||
if (!user && address) {
|
||||
dispatch(getUser(address));
|
||||
} else if (!user && protocol === 'custom') {
|
||||
dispatch(getUser(groupName));
|
||||
}
|
||||
}, [user, address, groupName, protocol]);
|
||||
|
||||
useEffect(() => {
|
||||
if (protocol === 'custom') {
|
||||
return (
|
||||
<div className={`flex flex-row flex-nowrap items-center ${className}`}>
|
||||
{getName(user)}
|
||||
</div>
|
||||
)
|
||||
setUsername(groupName);
|
||||
} else if (address) {
|
||||
setUsername(address);
|
||||
}
|
||||
}, [address, groupName, protocol]);
|
||||
|
||||
if (group) {
|
||||
return (
|
||||
<div className={`flex flex-row flex-nowrap items-center text-sm ${className}`}>
|
||||
{GROUP_TO_NICKNAME[group] || 'Anonymous'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (user) {
|
||||
return (
|
||||
<div className={`flex flex-row flex-nowrap items-center ${className}`}>{getName(user)}</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (interepProvider && interepGroup) {
|
||||
if (/twitter/i.test(interepProvider)) {
|
||||
if (/unrated/i.test(interepGroup)) {
|
||||
badges.push(
|
||||
<Badge
|
||||
key={interepProvider + '_' + interepGroup}
|
||||
label="<500 Twitter followers"
|
||||
url={TwitterPaper}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (protocol === 'custom') {
|
||||
return (
|
||||
<div className={`flex flex-row flex-nowrap items-center ${className}`}>{getName(user)}</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (/bronze/i.test(interepGroup)) {
|
||||
badges.push(
|
||||
<Badge
|
||||
key={interepProvider + '_' + interepGroup}
|
||||
label="500+ Twitter followers"
|
||||
url={TwitterBronze}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (group) {
|
||||
return (
|
||||
<div className={`flex flex-row flex-nowrap items-center text-sm ${className}`}>
|
||||
{GROUP_TO_NICKNAME[group] || 'Anonymous'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (/silver/i.test(interepGroup)) {
|
||||
badges.push(
|
||||
<Badge
|
||||
key={interepProvider + '_' + interepGroup}
|
||||
label="2000+ Twitter followers"
|
||||
url={TwitterSilver}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (interepProvider && interepGroup) {
|
||||
if (/twitter/i.test(interepProvider)) {
|
||||
if (/unrated/i.test(interepGroup)) {
|
||||
badges.push(
|
||||
<Badge
|
||||
key={interepProvider + '_' + interepGroup}
|
||||
label="<500 Twitter followers"
|
||||
url={TwitterPaper}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (/gold/i.test(interepGroup)) {
|
||||
badges.push(
|
||||
<Badge
|
||||
key={interepProvider + '_' + interepGroup}
|
||||
label="7000+ Twitter followers"
|
||||
url={TwitterGold}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (/bronze/i.test(interepGroup)) {
|
||||
badges.push(
|
||||
<Badge
|
||||
key={interepProvider + '_' + interepGroup}
|
||||
label="500+ Twitter followers"
|
||||
url={TwitterBronze}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-row flex-nowrap items-center text-sm ${className}`}>
|
||||
Anonymous
|
||||
<div className="flex flex-row flex-nowrap items-center ml-2">
|
||||
{ badges }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
if (/silver/i.test(interepGroup)) {
|
||||
badges.push(
|
||||
<Badge
|
||||
key={interepProvider + '_' + interepGroup}
|
||||
label="2000+ Twitter followers"
|
||||
url={TwitterSilver}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (/gold/i.test(interepGroup)) {
|
||||
badges.push(
|
||||
<Badge
|
||||
key={interepProvider + '_' + interepGroup}
|
||||
label="7000+ Twitter followers"
|
||||
url={TwitterGold}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-row flex-nowrap items-center text-sm ${className}`}
|
||||
>
|
||||
Anonymous
|
||||
</div>
|
||||
<div className={`flex flex-row flex-nowrap items-center text-sm ${className}`}>
|
||||
Anonymous
|
||||
<div className="flex flex-row flex-nowrap items-center ml-2">{badges}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-row flex-nowrap items-center text-sm ${className}`}>Anonymous</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Badge(props: { url: string; label: string }): ReactElement {
|
||||
return (
|
||||
<Popoverable label={props.label}>
|
||||
<Icon
|
||||
className="shadow rounded-full"
|
||||
url={props.url}
|
||||
size={1}
|
||||
/>
|
||||
</Popoverable>
|
||||
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Popoverable label={props.label}>
|
||||
<Icon className="shadow rounded-full" url={props.url} size={1} />
|
||||
</Popoverable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import React, {ReactElement, ReactNode, ReactNodeArray} from "react";
|
||||
import classNames from "classnames";
|
||||
import "./notif-box.scss";
|
||||
import React, { ReactElement, ReactNode, ReactNodeArray } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import './notif-box.scss';
|
||||
|
||||
type Props = {
|
||||
type?: 'warning' | 'info' | 'error';
|
||||
children?: ReactNode | ReactNodeArray;
|
||||
className?: string;
|
||||
type?: 'warning' | 'info' | 'error';
|
||||
children?: ReactNode | ReactNodeArray;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function NotificationBox(props: Props): ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={classNames('notif-box', {
|
||||
'notif-box--info': props.type === 'info',
|
||||
'notif-box--warning': props.type === 'warning',
|
||||
'notif-box--error': props.type === 'error',
|
||||
}, props.className)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'notif-box',
|
||||
{
|
||||
'notif-box--info': props.type === 'info',
|
||||
'notif-box--warning': props.type === 'warning',
|
||||
'notif-box--error': props.type === 'error',
|
||||
},
|
||||
props.className
|
||||
)}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.notif-box {
|
||||
background-color: rgba($primary-color, .1);
|
||||
border: 1px solid rgba($primary-color, .5);
|
||||
background-color: rgba($primary-color, 0.1);
|
||||
border: 1px solid rgba($primary-color, 0.5);
|
||||
color: darken($primary-color, 10);
|
||||
border-radius: .75rem;
|
||||
font-size: .875rem;
|
||||
padding: .75rem 1rem;
|
||||
}
|
||||
border-radius: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
@@ -1,64 +1,63 @@
|
||||
import React, {MouseEventHandler, ReactElement, useEffect} from "react";
|
||||
import {fetchMeta, useGoToPost, usePost} from "../../ducks/posts";
|
||||
import Post from "../Post";
|
||||
import {Post as PostMessage, PostMessageSubType} from "../../util/message";
|
||||
import classNames from "classnames";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {useLoggedIn} from "../../ducks/web3";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import React, { MouseEventHandler, ReactElement, useEffect } from 'react';
|
||||
import { fetchMeta, useGoToPost, usePost } from '../../ducks/posts';
|
||||
import Post from '../Post';
|
||||
import { Post as PostMessage, PostMessageSubType } from '../../util/message';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLoggedIn } from '../../ducks/web3';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
type Props = {
|
||||
level?: number;
|
||||
messageId: string;
|
||||
className?: string;
|
||||
postClassName?: string;
|
||||
onClick?: MouseEventHandler;
|
||||
onSuccessPost?: (post: PostMessage) => void;
|
||||
expand?: boolean;
|
||||
level?: number;
|
||||
messageId: string;
|
||||
className?: string;
|
||||
postClassName?: string;
|
||||
onClick?: MouseEventHandler;
|
||||
onSuccessPost?: (post: PostMessage) => void;
|
||||
expand?: boolean;
|
||||
};
|
||||
|
||||
export default function ParentThread(props: Props): ReactElement {
|
||||
const post = usePost(props.messageId);
|
||||
// @ts-ignore
|
||||
const parent = [PostMessageSubType.Reply, PostMessageSubType.MirrorReply].includes(post?.subtype)
|
||||
? post?.payload.reference
|
||||
: '';
|
||||
const gotoPost = useGoToPost();
|
||||
const dispatch = useDispatch();
|
||||
const loggedIn = useLoggedIn();
|
||||
const theme = useThemeContext();
|
||||
const post = usePost(props.messageId);
|
||||
// @ts-ignore
|
||||
const parent = [PostMessageSubType.Reply, PostMessageSubType.MirrorReply].includes(post?.subtype)
|
||||
? post?.payload.reference
|
||||
: '';
|
||||
const gotoPost = useGoToPost();
|
||||
const dispatch = useDispatch();
|
||||
const loggedIn = useLoggedIn();
|
||||
const theme = useThemeContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async function onPostViewMount() {
|
||||
if (!parent) return;
|
||||
await dispatch(fetchMeta(parent));
|
||||
})();
|
||||
useEffect(() => {
|
||||
(async function onPostViewMount() {
|
||||
if (!parent) return;
|
||||
await dispatch(fetchMeta(parent));
|
||||
})();
|
||||
}, [loggedIn, parent]);
|
||||
|
||||
}, [loggedIn, parent]);
|
||||
if (!parent) return <></>;
|
||||
|
||||
if (!parent) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ParentThread
|
||||
className={props.className}
|
||||
messageId={parent}
|
||||
onSuccessPost={props.onSuccessPost}
|
||||
/>
|
||||
<Post
|
||||
messageId={parent}
|
||||
className={classNames(
|
||||
"cursor-pointer hover:bg-gray-50 parent-post",
|
||||
{
|
||||
'hover:bg-gray-50 ': theme !== 'dark',
|
||||
'hover:bg-gray-900 ': theme === 'dark',
|
||||
},
|
||||
props.className,
|
||||
)}
|
||||
onClick={() => gotoPost(parent)}
|
||||
onSuccessPost={props.onSuccessPost}
|
||||
isParent
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ParentThread
|
||||
className={props.className}
|
||||
messageId={parent}
|
||||
onSuccessPost={props.onSuccessPost}
|
||||
/>
|
||||
<Post
|
||||
messageId={parent}
|
||||
className={classNames(
|
||||
'cursor-pointer hover:bg-gray-50 parent-post',
|
||||
{
|
||||
'hover:bg-gray-50 ': theme !== 'dark',
|
||||
'hover:bg-gray-900 ': theme === 'dark',
|
||||
},
|
||||
props.className
|
||||
)}
|
||||
onClick={() => gotoPost(parent)}
|
||||
onSuccessPost={props.onSuccessPost}
|
||||
isParent
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import React, {ReactElement, ReactNode} from "react";
|
||||
import "./popoverable.scss";
|
||||
import React, { ReactElement, ReactNode } from 'react';
|
||||
import './popoverable.scss';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function Popoverable(props: Props): ReactElement {
|
||||
return (
|
||||
<div className="popoverable" title={props.label}>
|
||||
{/*<div className="popoverable__label">{props.label}</div>*/}
|
||||
<>
|
||||
{props.children}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="popoverable" title={props.label}>
|
||||
{/*<div className="popoverable__label">{props.label}</div>*/}
|
||||
<>{props.children}</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.popoverable {
|
||||
position: relative;
|
||||
@@ -11,12 +11,12 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: fit-content;
|
||||
background-color: rgba($black, .8);
|
||||
color: rgba($white, .9);
|
||||
background-color: rgba($black, 0.8);
|
||||
color: rgba($white, 0.9);
|
||||
white-space: nowrap;
|
||||
font-size: .75rem;
|
||||
padding: 0 .5rem;
|
||||
border-radius: .125rem;
|
||||
font-size: 0.75rem;
|
||||
padding: 0 0.5rem;
|
||||
border-radius: 0.125rem;
|
||||
font-weight: 400;
|
||||
transition: opacity 200ms ease-in-out;
|
||||
transition-delay: 500ms;
|
||||
@@ -31,4 +31,4 @@
|
||||
max-height: 100rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,11 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.post {
|
||||
position: relative;
|
||||
transition: border-color 150ms ease-in,
|
||||
background-color 150ms ease-in-out;
|
||||
transition: border-color 150ms ease-in, background-color 150ms ease-in-out;
|
||||
|
||||
&__footer {
|
||||
margin-left: -.3125rem;
|
||||
margin-left: -0.3125rem;
|
||||
}
|
||||
|
||||
&__parent-line {
|
||||
@@ -34,14 +33,14 @@
|
||||
|
||||
.dark {
|
||||
.post-button {
|
||||
color: rgba($white, .5);
|
||||
color: rgba($white, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.post-button {
|
||||
color: rgba($black, .5);
|
||||
color: rgba($black, 0.5);
|
||||
height: 2rem;
|
||||
font-size: .9375rem;
|
||||
font-size: 0.9375rem;
|
||||
|
||||
.icon {
|
||||
@extend %row-nowrap;
|
||||
@@ -86,9 +85,9 @@
|
||||
border-color: $gray-800;
|
||||
}
|
||||
|
||||
.thread__content{
|
||||
.thread__content {
|
||||
.thread > .post {
|
||||
border-left: .25rem solid $gray-800;
|
||||
border-left: 0.25rem solid $gray-800;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,10 +128,9 @@
|
||||
border-top: 1px solid $gray-100;
|
||||
}
|
||||
|
||||
|
||||
.thread__content{
|
||||
.thread__content {
|
||||
.thread > .post {
|
||||
border-left: .25rem solid $gray-100;
|
||||
border-left: 0.25rem solid $gray-100;
|
||||
|
||||
&:hover {
|
||||
border-color: $gray-400;
|
||||
@@ -168,4 +166,4 @@
|
||||
max-height: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {act} from "react-dom/test-utils";
|
||||
import {Provider} from "react-redux";
|
||||
import Post from "./index";
|
||||
import {Post as PostMessage} from "../../util/message";
|
||||
import {dispatchSpy, ducks, gunStub, store} from "../../util/testUtils";
|
||||
import {MessageType, PostMessageSubType} from "../../util/message";
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Provider } from 'react-redux';
|
||||
import Post from './index';
|
||||
import { Post as PostMessage } from '../../util/message';
|
||||
import { dispatchSpy, ducks, gunStub, store } from '../../util/testUtils';
|
||||
import { MessageType, PostMessageSubType } from '../../util/message';
|
||||
|
||||
describe('<Post>', () => {
|
||||
const root = document.createElement('div');
|
||||
const post = new PostMessage({
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.Default,
|
||||
payload: {
|
||||
content: 'hello',
|
||||
},
|
||||
});
|
||||
const json = post.toJSON();
|
||||
const root = document.createElement('div');
|
||||
const post = new PostMessage({
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.Default,
|
||||
payload: {
|
||||
content: 'hello',
|
||||
},
|
||||
});
|
||||
const json = post.toJSON();
|
||||
|
||||
it ('should mount', async () => {
|
||||
act(() => {
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Post messageId={json.messageId} />
|
||||
</Provider>,
|
||||
root,
|
||||
)
|
||||
});
|
||||
|
||||
expect(root.innerHTML).toBeTruthy();
|
||||
it('should mount', async () => {
|
||||
act(() => {
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Post messageId={json.messageId} />
|
||||
</Provider>,
|
||||
root
|
||||
);
|
||||
});
|
||||
|
||||
it ('should update', async () => {
|
||||
store.dispatch(ducks.posts.setPost(post))
|
||||
expect(root.textContent).toBe('Anonymous•a few secondshello000');
|
||||
});
|
||||
expect(root.innerHTML).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should update', async () => {
|
||||
store.dispatch(ducks.posts.setPost(post));
|
||||
expect(root.textContent).toBe('Anonymous•a few secondshello000');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,167 +1,159 @@
|
||||
import React, {ReactElement, useCallback, useEffect, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {useUser} from "../../ducks/users";
|
||||
import Icon from "../Icon";
|
||||
import SpinnerGIF from "../../../static/icons/spinner.gif";
|
||||
import {useHistory, useParams} from "react-router";
|
||||
import {getHandle, getUsername} from "../../util/user";
|
||||
import "./post-mod-panel.scss";
|
||||
import {useMeta, usePost} from "../../ducks/posts";
|
||||
import {ModerationMessageSubType, PostMessageSubType} from "../../util/message";
|
||||
import SwitchButton from "../SwitchButton";
|
||||
import {unmoderate, usePostModeration} from "../../ducks/mods";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useUser } from '../../ducks/users';
|
||||
import Icon from '../Icon';
|
||||
import SpinnerGIF from '../../../static/icons/spinner.gif';
|
||||
import { useHistory, useParams } from 'react-router';
|
||||
import { getHandle, getUsername } from '../../util/user';
|
||||
import './post-mod-panel.scss';
|
||||
import { useMeta, usePost } from '../../ducks/posts';
|
||||
import { ModerationMessageSubType, PostMessageSubType } from '../../util/message';
|
||||
import SwitchButton from '../SwitchButton';
|
||||
import { unmoderate, usePostModeration } from '../../ducks/mods';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
export default function PostModerationPanel(): ReactElement {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const {name, hash} = useParams<{name?: string; hash: string}>();
|
||||
const dispatch = useDispatch();
|
||||
const messageId = [name, hash].join('/');
|
||||
const originPost = usePost(messageId);
|
||||
const meta = useMeta(originPost?.subtype === PostMessageSubType.Repost ? originPost.payload.reference : messageId);
|
||||
const threadmod = usePostModeration(meta?.rootId);
|
||||
const [unmoderated, setUnmoderated] = useState(false);
|
||||
const theme = useThemeContext();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { name, hash } = useParams<{ name?: string; hash: string }>();
|
||||
const dispatch = useDispatch();
|
||||
const messageId = [name, hash].join('/');
|
||||
const originPost = usePost(messageId);
|
||||
const meta = useMeta(
|
||||
originPost?.subtype === PostMessageSubType.Repost ? originPost.payload.reference : messageId
|
||||
);
|
||||
const threadmod = usePostModeration(meta?.rootId);
|
||||
const [unmoderated, setUnmoderated] = useState(false);
|
||||
const theme = useThemeContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async function onPostModerationPanelMount() {
|
||||
setUnmoderated(!!threadmod?.unmoderated);
|
||||
})();
|
||||
}, [threadmod]);
|
||||
useEffect(() => {
|
||||
(async function onPostModerationPanelMount() {
|
||||
setUnmoderated(!!threadmod?.unmoderated);
|
||||
})();
|
||||
}, [threadmod]);
|
||||
|
||||
const toggleModeration = useCallback(() => {
|
||||
if (!meta?.rootId) return;
|
||||
dispatch(unmoderate(meta?.rootId, !unmoderated));
|
||||
setUnmoderated(!unmoderated);
|
||||
}, [unmoderated, meta?.rootId]);
|
||||
const toggleModeration = useCallback(() => {
|
||||
if (!meta?.rootId) return;
|
||||
dispatch(unmoderate(meta?.rootId, !unmoderated));
|
||||
setUnmoderated(!unmoderated);
|
||||
}, [unmoderated, meta?.rootId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap flex-grow border rounded-xl mt-2',
|
||||
'meta-group post-mod-panel',
|
||||
{
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
}
|
||||
)}>
|
||||
<div
|
||||
className={classNames('px-4 py-2 font-bold text-lg border-b', {
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}>
|
||||
Moderation
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap py-1">
|
||||
{loading && <Icon className="self-center my-4" url={SpinnerGIF} size={3} />}
|
||||
<div className="flex flex-row items-center justify-center px-4 py-2">
|
||||
<Icon
|
||||
className={classNames(
|
||||
'flex flex-col flex-nowrap flex-grow border rounded-xl mt-2',
|
||||
'meta-group post-mod-panel',
|
||||
{
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
}
|
||||
'flex flex-row items-center justify-center flex-shrink-0 flex-grow-0',
|
||||
'h-9 w-9 p-2 rounded-full border-2',
|
||||
{
|
||||
'border-black text-black': theme !== 'dark',
|
||||
'border-white text-white': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className={classNames("px-4 py-2 font-bold text-lg border-b", {
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}>
|
||||
Moderation
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap py-1">
|
||||
{ loading && <Icon className="self-center my-4" url={SpinnerGIF} size={3} /> }
|
||||
<div className="flex flex-row items-center justify-center px-4 py-2">
|
||||
<Icon
|
||||
className={classNames(
|
||||
"flex flex-row items-center justify-center flex-shrink-0 flex-grow-0",
|
||||
"h-9 w-9 p-2 rounded-full border-2",
|
||||
{
|
||||
"border-black text-black": theme !== 'dark',
|
||||
"border-white text-white": theme === 'dark',
|
||||
}
|
||||
)}
|
||||
fa={getFA(meta?.moderation)}
|
||||
size={.875}
|
||||
/>
|
||||
<div className="flex-grow flex-shrink w-0 text-light ml-2">
|
||||
<PanelTextContent />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
meta?.moderation && (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row items-center text-sm",
|
||||
"px-4 py-2",
|
||||
{
|
||||
"bg-gray-100 text-gray-500": theme !== 'dark',
|
||||
"bg-gray-900 text-gray-500": theme === 'dark',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex-grow flex-shrink w-0 mr-2">
|
||||
You can switch off moderation for this post temporarily
|
||||
</div>
|
||||
<SwitchButton
|
||||
className="flex-grow-0 flex-shrink-0 ml-4"
|
||||
checked={!unmoderated}
|
||||
onChange={toggleModeration}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
fa={getFA(meta?.moderation)}
|
||||
size={0.875}
|
||||
/>
|
||||
<div className="flex-grow flex-shrink w-0 text-light ml-2">
|
||||
<PanelTextContent />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
{meta?.moderation && (
|
||||
<div
|
||||
className={classNames('flex flex-row items-center text-sm', 'px-4 py-2', {
|
||||
'bg-gray-100 text-gray-500': theme !== 'dark',
|
||||
'bg-gray-900 text-gray-500': theme === 'dark',
|
||||
})}>
|
||||
<div className="flex-grow flex-shrink w-0 mr-2">
|
||||
You can switch off moderation for this post temporarily
|
||||
</div>
|
||||
<SwitchButton
|
||||
className="flex-grow-0 flex-shrink-0 ml-4"
|
||||
checked={!unmoderated}
|
||||
onChange={toggleModeration}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getFA(moderation?: ModerationMessageSubType | null): string {
|
||||
switch(moderation) {
|
||||
case ModerationMessageSubType.ThreadBlock:
|
||||
return 'fas fa-shield-alt';
|
||||
case ModerationMessageSubType.ThreadFollow:
|
||||
return 'fas fa-user-check';
|
||||
case ModerationMessageSubType.ThreadMention:
|
||||
return 'fas fa-at';
|
||||
default:
|
||||
return 'fas fa-globe';
|
||||
}
|
||||
switch (moderation) {
|
||||
case ModerationMessageSubType.ThreadBlock:
|
||||
return 'fas fa-shield-alt';
|
||||
case ModerationMessageSubType.ThreadFollow:
|
||||
return 'fas fa-user-check';
|
||||
case ModerationMessageSubType.ThreadMention:
|
||||
return 'fas fa-at';
|
||||
default:
|
||||
return 'fas fa-globe';
|
||||
}
|
||||
}
|
||||
|
||||
function PanelTextContent (): ReactElement {
|
||||
const {name, hash} = useParams<{name?: string; hash: string}>();
|
||||
const messageId = [name, hash].join('/');
|
||||
const originPost = usePost(messageId);
|
||||
const meta = useMeta(originPost?.subtype === PostMessageSubType.Repost ? originPost.payload.reference : messageId);
|
||||
const root = usePost(meta?.rootId || undefined);
|
||||
const op = useUser(root?.creator);
|
||||
const history = useHistory();
|
||||
function PanelTextContent(): ReactElement {
|
||||
const { name, hash } = useParams<{ name?: string; hash: string }>();
|
||||
const messageId = [name, hash].join('/');
|
||||
const originPost = usePost(messageId);
|
||||
const meta = useMeta(
|
||||
originPost?.subtype === PostMessageSubType.Repost ? originPost.payload.reference : messageId
|
||||
);
|
||||
const root = usePost(meta?.rootId || undefined);
|
||||
const op = useUser(root?.creator);
|
||||
const history = useHistory();
|
||||
|
||||
const handle = getHandle(op);
|
||||
const mention = (
|
||||
<a
|
||||
className="hashtag cursor-pointer ml-1"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
history.push(`/${encodeURIComponent(getUsername(op))}/`)
|
||||
}}
|
||||
>
|
||||
@{handle}
|
||||
</a>
|
||||
);
|
||||
const handle = getHandle(op);
|
||||
const mention = (
|
||||
<a
|
||||
className="hashtag cursor-pointer ml-1"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
history.push(`/${encodeURIComponent(getUsername(op))}/`);
|
||||
}}>
|
||||
@{handle}
|
||||
</a>
|
||||
);
|
||||
|
||||
switch(meta?.moderation) {
|
||||
case ModerationMessageSubType.ThreadBlock:
|
||||
return (
|
||||
<div>
|
||||
Hide replies blocked by
|
||||
{mention}
|
||||
</div>
|
||||
);
|
||||
case ModerationMessageSubType.ThreadFollow:
|
||||
return (
|
||||
<div>
|
||||
Show only replies liked or followed
|
||||
{mention}
|
||||
</div>
|
||||
);
|
||||
case ModerationMessageSubType.ThreadMention:
|
||||
return (
|
||||
<div>
|
||||
Show only replies from users mentioned by
|
||||
{mention}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div>
|
||||
Show all contents
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
switch (meta?.moderation) {
|
||||
case ModerationMessageSubType.ThreadBlock:
|
||||
return (
|
||||
<div>
|
||||
Hide replies blocked by
|
||||
{mention}
|
||||
</div>
|
||||
);
|
||||
case ModerationMessageSubType.ThreadFollow:
|
||||
return (
|
||||
<div>
|
||||
Show only replies liked or followed
|
||||
{mention}
|
||||
</div>
|
||||
);
|
||||
case ModerationMessageSubType.ThreadMention:
|
||||
return (
|
||||
<div>
|
||||
Show only replies from users mentioned by
|
||||
{mention}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <div>Show all contents</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.post-mod-panel {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,44 @@
|
||||
import React, {ReactElement, useCallback, useState} from "react";
|
||||
import QrReader from "react-qr-reader";
|
||||
import {Identity} from "../../serviceWorkers/identity";
|
||||
import {postWorkerMessage} from "../../util/sw";
|
||||
import {addIdentity, selectIdentity, setIdentity} from "../../serviceWorkers/util";
|
||||
import React, { ReactElement, useCallback, useState } from 'react';
|
||||
import QrReader from 'react-qr-reader';
|
||||
import { Identity } from '../../serviceWorkers/identity';
|
||||
import { postWorkerMessage } from '../../util/sw';
|
||||
import { addIdentity, selectIdentity, setIdentity } from '../../serviceWorkers/util';
|
||||
|
||||
export default function QRScanner(props: {
|
||||
onSuccess?: () => void;
|
||||
}): ReactElement {
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [scannedData, setScannedData] = useState('');
|
||||
export default function QRScanner(props: { onSuccess?: () => void }): ReactElement {
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [scannedData, setScannedData] = useState('');
|
||||
|
||||
const onScan = useCallback(async (data: string | null) => {
|
||||
if (!data) return;
|
||||
const onScan = useCallback(async (data: string | null) => {
|
||||
if (!data) return;
|
||||
|
||||
setScannedData(data);
|
||||
setScannedData(data);
|
||||
|
||||
try {
|
||||
const identity: Identity = JSON.parse(data);
|
||||
if (identity.type === 'gun' && !identity.privateKey) return;
|
||||
if (identity.type === 'interrep' && !identity.serializedIdentity) return;
|
||||
await postWorkerMessage(setIdentity(identity));
|
||||
// await postWorkerMessage(selectIdentity(identity.publicKey));
|
||||
if (props.onSuccess) props.onSuccess();
|
||||
} catch (e) {
|
||||
setErrorMessage(e.message);
|
||||
}
|
||||
}, [])
|
||||
try {
|
||||
const identity: Identity = JSON.parse(data);
|
||||
if (identity.type === 'gun' && !identity.privateKey) return;
|
||||
if (identity.type === 'interrep' && !identity.serializedIdentity) return;
|
||||
await postWorkerMessage(setIdentity(identity));
|
||||
// await postWorkerMessage(selectIdentity(identity.publicKey));
|
||||
if (props.onSuccess) props.onSuccess();
|
||||
} catch (e) {
|
||||
setErrorMessage(e.message);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onError = useCallback((err: string) => {
|
||||
setErrorMessage(err);
|
||||
}, [])
|
||||
const onError = useCallback((err: string) => {
|
||||
setErrorMessage(err);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="qr-scanner">
|
||||
<div className="text-light text-center px-3 py-2 font-semibold">
|
||||
On desktop, you can export your private key to QR code by logging in and clicking "Export Private Key"
|
||||
</div>
|
||||
<QrReader
|
||||
delay={300}
|
||||
onScan={onScan}
|
||||
onError={onError}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
{ errorMessage && <div className="error-message text-xs text-center text-red-500 m-2">{errorMessage}</div> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="qr-scanner">
|
||||
<div className="text-light text-center px-3 py-2 font-semibold">
|
||||
On desktop, you can export your private key to QR code by logging in and clicking "Export
|
||||
Private Key"
|
||||
</div>
|
||||
<QrReader delay={300} onScan={onScan} onError={onError} style={{ width: '100%' }} />
|
||||
{errorMessage && (
|
||||
<div className="error-message text-xs text-center text-red-500 m-2">{errorMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React, {ChangeEventHandler, ReactElement} from "react";
|
||||
import "./switch-button.scss";
|
||||
import classNames from "classnames";
|
||||
import React, { ChangeEventHandler, ReactElement } from 'react';
|
||||
import './switch-button.scss';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
checked?: boolean;
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
className?: string;
|
||||
}
|
||||
checked?: boolean;
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function SwitchButton(props: Props): ReactElement {
|
||||
return (
|
||||
<div className={classNames("switch-button", props.className)}>
|
||||
<input type="checkbox" onChange={props.onChange} checked={props.checked}/>
|
||||
<span className="slider round" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={classNames('switch-button', props.className)}>
|
||||
<input type="checkbox" onChange={props.onChange} checked={props.checked} />
|
||||
<span className="slider round" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.switch-button {
|
||||
cursor: pointer;
|
||||
@@ -31,20 +31,20 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: $gray-200;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
content: '';
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
@@ -68,4 +68,4 @@ input:checked + .slider:before {
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +1,86 @@
|
||||
import React, {ReactElement, useCallback, useEffect, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import Post from "../Post";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {fetchHomeFeed, fetchTagFeed, useGoToPost} from "../../ducks/posts";
|
||||
import "./tag-feed.scss";
|
||||
import {useLoggedIn} from "../../ducks/web3";
|
||||
import {useHistory, useParams} from "react-router";
|
||||
import InfiniteScrollable from "../InfiniteScrollable";
|
||||
import {useSelectedLocalId} from "../../ducks/worker";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Post from '../Post';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { fetchHomeFeed, fetchTagFeed, useGoToPost } from '../../ducks/posts';
|
||||
import './tag-feed.scss';
|
||||
import { useLoggedIn } from '../../ducks/web3';
|
||||
import { useHistory, useParams } from 'react-router';
|
||||
import InfiniteScrollable from '../InfiniteScrollable';
|
||||
import { useSelectedLocalId } from '../../ducks/worker';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
export default function TagFeed(): ReactElement {
|
||||
const {tagName} = useParams<{tagName: string}>();
|
||||
const [limit, setLimit] = useState(20);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [order, setOrder] = useState<string[]>([]);
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const loggedIn = useLoggedIn();
|
||||
const tag = decodeURIComponent(tagName);
|
||||
const selected = useSelectedLocalId();
|
||||
const theme = useThemeContext();
|
||||
const { tagName } = useParams<{ tagName: string }>();
|
||||
const [limit, setLimit] = useState(20);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [order, setOrder] = useState<string[]>([]);
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const loggedIn = useLoggedIn();
|
||||
const tag = decodeURIComponent(tagName);
|
||||
const selected = useSelectedLocalId();
|
||||
const theme = useThemeContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async function onTagFeedMount() {
|
||||
setFetching(true);
|
||||
try {
|
||||
setOrder([]);
|
||||
setOffset(0);
|
||||
await fetchMore(true);
|
||||
} finally {
|
||||
setFetching(false);
|
||||
}
|
||||
})();
|
||||
}, [loggedIn, tag, selected]);
|
||||
useEffect(() => {
|
||||
(async function onTagFeedMount() {
|
||||
setFetching(true);
|
||||
try {
|
||||
setOrder([]);
|
||||
setOffset(0);
|
||||
await fetchMore(true);
|
||||
} finally {
|
||||
setFetching(false);
|
||||
}
|
||||
})();
|
||||
}, [loggedIn, tag, selected]);
|
||||
|
||||
const fetchMore = useCallback(async (reset = false) => {
|
||||
if (reset) {
|
||||
const messageIds: any = await dispatch(fetchTagFeed(tag, 20, 0));
|
||||
setOffset(20);
|
||||
setOrder(messageIds);
|
||||
} else {
|
||||
if (order.length % limit) return;
|
||||
const messageIds: any = await dispatch(fetchTagFeed(tag, limit, offset));
|
||||
setOffset(offset + limit);
|
||||
setOrder(order.concat(messageIds));
|
||||
}
|
||||
}, [limit, offset, order, tag]);
|
||||
const fetchMore = useCallback(
|
||||
async (reset = false) => {
|
||||
if (reset) {
|
||||
const messageIds: any = await dispatch(fetchTagFeed(tag, 20, 0));
|
||||
setOffset(20);
|
||||
setOrder(messageIds);
|
||||
} else {
|
||||
if (order.length % limit) return;
|
||||
const messageIds: any = await dispatch(fetchTagFeed(tag, limit, offset));
|
||||
setOffset(offset + limit);
|
||||
setOrder(order.concat(messageIds));
|
||||
}
|
||||
},
|
||||
[limit, offset, order, tag]
|
||||
);
|
||||
|
||||
const gotoPost = useGoToPost();
|
||||
const gotoPost = useGoToPost();
|
||||
|
||||
return (
|
||||
<InfiniteScrollable
|
||||
className={classNames('flex-grow home-feed',
|
||||
'mx-4 py-2',
|
||||
{},
|
||||
)}
|
||||
bottomOffset={128}
|
||||
onScrolledToBottom={fetchMore}
|
||||
>
|
||||
{
|
||||
!order.length && !fetching && (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap items-center justify-center',
|
||||
'py-6 px-4 border border-gray-200 rounded-xl text-sm text-gray-300',
|
||||
)}
|
||||
>
|
||||
Nothing to see here yet
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
order.map((messageId, i) => {
|
||||
return (
|
||||
<Post
|
||||
key={messageId}
|
||||
className={classNames(
|
||||
"rounded-xl transition-colors mb-1 cursor-pointer border",
|
||||
{
|
||||
"hover:border-gray-300 border-gray-200": theme !== 'dark',
|
||||
"hover:border-gray-700 border-gray-800": theme === 'dark',
|
||||
},
|
||||
)}
|
||||
messageId={messageId}
|
||||
onClick={() => gotoPost(messageId)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</InfiniteScrollable>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<InfiniteScrollable
|
||||
className={classNames('flex-grow home-feed', 'mx-4 py-2', {})}
|
||||
bottomOffset={128}
|
||||
onScrolledToBottom={fetchMore}>
|
||||
{!order.length && !fetching && (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap items-center justify-center',
|
||||
'py-6 px-4 border border-gray-200 rounded-xl text-sm text-gray-300'
|
||||
)}>
|
||||
Nothing to see here yet
|
||||
</div>
|
||||
)}
|
||||
{order.map((messageId, i) => {
|
||||
return (
|
||||
<Post
|
||||
key={messageId}
|
||||
className={classNames('rounded-xl transition-colors mb-1 cursor-pointer border', {
|
||||
'hover:border-gray-300 border-gray-200': theme !== 'dark',
|
||||
'hover:border-gray-700 border-gray-800': theme === 'dark',
|
||||
})}
|
||||
messageId={messageId}
|
||||
onClick={() => gotoPost(messageId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</InfiniteScrollable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0; /* Remove scrollbar space */
|
||||
background: transparent; /* Optional: just make scrollbar invisible */
|
||||
width: 0; /* Remove scrollbar space */
|
||||
background: transparent; /* Optional: just make scrollbar invisible */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +1,91 @@
|
||||
import React, {ReactElement, useEffect, useState} from "react";
|
||||
import Modal, {ModalContent, ModalFooter, ModalHeader} from "../Modal";
|
||||
import TazHero from "../../../static/icons/taz_hero.png";
|
||||
import "./taz-modal.scss";
|
||||
import Button from "../Button";
|
||||
import {useLocation} from "react-router";
|
||||
import {Identity} from "@semaphore-protocol/identity";
|
||||
import {findProof} from "../../util/merkle";
|
||||
import {postWorkerMessage} from "../../util/sw";
|
||||
import {setIdentity} from "../../serviceWorkers/util";
|
||||
import {ViewType} from "../../pages/SignupView";
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import Modal, { ModalContent, ModalFooter, ModalHeader } from '../Modal';
|
||||
import TazHero from '../../../static/icons/taz_hero.png';
|
||||
import './taz-modal.scss';
|
||||
import Button from '../Button';
|
||||
import { useLocation } from 'react-router';
|
||||
import { Identity } from '@semaphore-protocol/identity';
|
||||
import { findProof } from '../../util/merkle';
|
||||
import { postWorkerMessage } from '../../util/sw';
|
||||
import { setIdentity } from '../../serviceWorkers/util';
|
||||
import { ViewType } from '../../pages/SignupView';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
tazIdentity: string[] | null
|
||||
onClose: () => void;
|
||||
tazIdentity: string[] | null;
|
||||
};
|
||||
|
||||
export default function TazModal(props: Props): ReactElement {
|
||||
const { tazIdentity } = props;
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const { tazIdentity } = props;
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const serializedIdentity = JSON.stringify(tazIdentity);
|
||||
const zkIdentity = new Identity(serializedIdentity);
|
||||
const idCommitmentBigInt = zkIdentity.generateCommitment();
|
||||
const pathData = await findProof('semaphore_taz_members', idCommitmentBigInt.toString(16));
|
||||
if (pathData) {
|
||||
await postWorkerMessage(setIdentity({
|
||||
type: 'taz',
|
||||
identityCommitment: idCommitmentBigInt.toString(),
|
||||
serializedIdentity: serializedIdentity,
|
||||
identityPath: {
|
||||
path_index: pathData.pathIndices,
|
||||
path_elements: pathData.siblings,
|
||||
root: pathData.root,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
setErrorMessage(`Cannot find ${idCommitmentBigInt.toString()} in TAZ Group`);
|
||||
}
|
||||
} catch (e) {
|
||||
setErrorMessage(e.message);
|
||||
}
|
||||
|
||||
})();
|
||||
}, [tazIdentity]);
|
||||
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<Modal className="w-148 taz-modal" onClose={props.onClose}>
|
||||
<ModalHeader>
|
||||
Invalid Identity
|
||||
</ModalHeader>
|
||||
<ModalContent className="p-4">
|
||||
<div className="my-2 text-light">
|
||||
Something went wrong while importing your identity :(
|
||||
<div className="mt-4">Please try again in a few minutes.</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button btnType="primary" onClick={props.onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const serializedIdentity = JSON.stringify(tazIdentity);
|
||||
const zkIdentity = new Identity(serializedIdentity);
|
||||
const idCommitmentBigInt = zkIdentity.generateCommitment();
|
||||
const pathData = await findProof('semaphore_taz_members', idCommitmentBigInt.toString(16));
|
||||
if (pathData) {
|
||||
await postWorkerMessage(
|
||||
setIdentity({
|
||||
type: 'taz',
|
||||
identityCommitment: idCommitmentBigInt.toString(),
|
||||
serializedIdentity: serializedIdentity,
|
||||
identityPath: {
|
||||
path_index: pathData.pathIndices,
|
||||
path_elements: pathData.siblings,
|
||||
root: pathData.root,
|
||||
},
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setErrorMessage(`Cannot find ${idCommitmentBigInt.toString()} in TAZ Group`);
|
||||
}
|
||||
} catch (e) {
|
||||
setErrorMessage(e.message);
|
||||
}
|
||||
})();
|
||||
}, [tazIdentity]);
|
||||
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<Modal className="w-148 taz-modal" onClose={() => null}>
|
||||
<ModalHeader>
|
||||
Your TAZ identity is imported successfully!
|
||||
</ModalHeader>
|
||||
<ModalContent className="p-4">
|
||||
<div
|
||||
className="taz-modal__hero"
|
||||
style={{ backgroundImage: `url(${TazHero})` }}
|
||||
/>
|
||||
<div className="my-2 text-light">
|
||||
The Temporary Anonymous Zone is a community hub at Devcon VI. Visitors can play with apps using an anonymous identity that uses the zero-knowledge protocol, Semaphore.
|
||||
<br />
|
||||
<div className="mt-2">
|
||||
You can start posting or chatting with other users anonymously.
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button btnType="primary" onClick={props.onClose}>
|
||||
Experience Zero-Knowledge
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
<Modal className="w-148 taz-modal" onClose={props.onClose}>
|
||||
<ModalHeader>Invalid Identity</ModalHeader>
|
||||
<ModalContent className="p-4">
|
||||
<div className="my-2 text-light">
|
||||
Something went wrong while importing your identity :(
|
||||
<div className="mt-4">Please try again in a few minutes.</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button btnType="primary" onClick={props.onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal className="w-148 taz-modal" onClose={() => null}>
|
||||
<ModalHeader>Your TAZ identity is imported successfully!</ModalHeader>
|
||||
<ModalContent className="p-4">
|
||||
<div className="taz-modal__hero" style={{ backgroundImage: `url(${TazHero})` }} />
|
||||
<div className="my-2 text-light">
|
||||
The Temporary Anonymous Zone is a community hub at Devcon VI. Visitors can play with apps
|
||||
using an anonymous identity that uses the zero-knowledge protocol, Semaphore.
|
||||
<br />
|
||||
<div className="mt-2">
|
||||
You can start posting or chatting with other users anonymously.
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button btnType="primary" onClick={props.onClose}>
|
||||
Experience Zero-Knowledge
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.taz-modal {
|
||||
&__hero {
|
||||
@@ -8,4 +8,4 @@
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,30 @@
|
||||
import React, {TextareaHTMLAttributes, ReactElement, LegacyRef} from "react";
|
||||
import classNames from "classnames";
|
||||
import "./textarea.scss";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import React, { TextareaHTMLAttributes, ReactElement, LegacyRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import './textarea.scss';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
type Props = {
|
||||
ref?: LegacyRef<HTMLTextAreaElement>;
|
||||
label?: string;
|
||||
errorMessage?: string;
|
||||
ref?: LegacyRef<HTMLTextAreaElement>;
|
||||
label?: string;
|
||||
errorMessage?: string;
|
||||
} & TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||
|
||||
export default function Textarea(props: Props): ReactElement {
|
||||
const {
|
||||
label,
|
||||
errorMessage,
|
||||
className,
|
||||
...textareaProps
|
||||
} = props;
|
||||
const { label, errorMessage, className, ...textareaProps } = props;
|
||||
|
||||
const theme = useThemeContext();
|
||||
const theme = useThemeContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"rounded-lg textarea-group",
|
||||
className,
|
||||
{
|
||||
'bg-gray-100 text-gray-300': props.disabled && theme !== 'dark',
|
||||
'bg-gray-900 text-gray-600': props.disabled && theme === 'dark',
|
||||
'focus-within:border-gray-400 ': !textareaProps.readOnly && theme !== 'dark',
|
||||
'focus-within:border-gray-600 border-gray-800': !textareaProps.readOnly && theme === 'dark',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{ label && <div className="textarea-group__label">{label}</div> }
|
||||
<textarea
|
||||
ref={props.ref}
|
||||
{...textareaProps}
|
||||
/>
|
||||
{ errorMessage && <small className="error-message">{errorMessage}</small> }
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={classNames('rounded-lg textarea-group', className, {
|
||||
'bg-gray-100 text-gray-300': props.disabled && theme !== 'dark',
|
||||
'bg-gray-900 text-gray-600': props.disabled && theme === 'dark',
|
||||
'focus-within:border-gray-400 ': !textareaProps.readOnly && theme !== 'dark',
|
||||
'focus-within:border-gray-600 border-gray-800': !textareaProps.readOnly && theme === 'dark',
|
||||
})}>
|
||||
{label && <div className="textarea-group__label">{label}</div>}
|
||||
<textarea ref={props.ref} {...textareaProps} />
|
||||
{errorMessage && <small className="error-message">{errorMessage}</small>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.dark {
|
||||
.textarea-group {
|
||||
@@ -16,11 +16,11 @@
|
||||
&__label {
|
||||
background: white;
|
||||
display: inline-block;
|
||||
top: -.625rem;
|
||||
top: -0.625rem;
|
||||
width: fit-content;
|
||||
font-size: 12px;
|
||||
padding: 0 .25rem 0 .1875rem;
|
||||
margin-left: .625rem;
|
||||
padding: 0 0.25rem 0 0.1875rem;
|
||||
margin-left: 0.625rem;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
resize: none;
|
||||
padding: .5rem .75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
|
||||
@@ -36,4 +36,4 @@
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import React, {ReactElement, ReactNode, useContext} from "react";
|
||||
import {useSetting} from "../../ducks/app";
|
||||
import React, { ReactElement, ReactNode, useContext } from 'react';
|
||||
import { useSetting } from '../../ducks/app';
|
||||
|
||||
const ThemeContext = React.createContext('light');
|
||||
|
||||
export const ThemeProvider = (props: { children: ReactNode }): ReactElement => {
|
||||
const setting = useSetting();
|
||||
const setting = useSetting();
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={setting.theme}>
|
||||
{props.children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
return <ThemeContext.Provider value={setting.theme}>{props.children}</ThemeContext.Provider>;
|
||||
};
|
||||
|
||||
export const useThemeContext = () => useContext(ThemeContext);
|
||||
|
||||
@@ -1,131 +1,118 @@
|
||||
import React, {MouseEventHandler, ReactElement, useCallback, useEffect, useState} from "react";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {useHistory, useParams} from "react-router";
|
||||
import {fetchPost, fetchReplies, useGoToPost, useMeta} from "../../ducks/posts";
|
||||
import classNames from "classnames";
|
||||
import Post from "../Post";
|
||||
import {parseMessageId, Post as PostMessage} from "../../util/message";
|
||||
import {usePostModeration} from "../../ducks/mods";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import React, { MouseEventHandler, ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory, useParams } from 'react-router';
|
||||
import { fetchPost, fetchReplies, useGoToPost, useMeta } from '../../ducks/posts';
|
||||
import classNames from 'classnames';
|
||||
import Post from '../Post';
|
||||
import { parseMessageId, Post as PostMessage } from '../../util/message';
|
||||
import { usePostModeration } from '../../ducks/mods';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
|
||||
type Props = {
|
||||
level?: number;
|
||||
messageId: string;
|
||||
className?: string;
|
||||
postClassName?: string;
|
||||
onClick?: MouseEventHandler;
|
||||
clearObserver?: () => void;
|
||||
expand?: boolean;
|
||||
onSuccessPost?: (post: PostMessage) => void;
|
||||
level?: number;
|
||||
messageId: string;
|
||||
className?: string;
|
||||
postClassName?: string;
|
||||
onClick?: MouseEventHandler;
|
||||
clearObserver?: () => void;
|
||||
expand?: boolean;
|
||||
onSuccessPost?: (post: PostMessage) => void;
|
||||
};
|
||||
|
||||
export default function Thread(props: Props): ReactElement {
|
||||
const { messageId, level = 0 } = props;
|
||||
const [limit, setLimit] = useState(20);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [order, setOrder] = useState<string[]>([]);
|
||||
const dispatch = useDispatch();
|
||||
const meta = useMeta(messageId);
|
||||
const modOverride = usePostModeration(meta?.rootId);
|
||||
const [end, setEnd] = useState(false);
|
||||
const theme = useThemeContext();
|
||||
const { messageId, level = 0 } = props;
|
||||
const [limit, setLimit] = useState(20);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [order, setOrder] = useState<string[]>([]);
|
||||
const dispatch = useDispatch();
|
||||
const meta = useMeta(messageId);
|
||||
const modOverride = usePostModeration(meta?.rootId);
|
||||
const [end, setEnd] = useState(false);
|
||||
const theme = useThemeContext();
|
||||
|
||||
const fetchMore = useCallback(async (reset = false) => {
|
||||
let messageIds: any;
|
||||
if (reset) {
|
||||
messageIds = await dispatch(fetchReplies(messageId, 20, 0));
|
||||
setOffset(messageIds.length);
|
||||
setOrder(messageIds);
|
||||
} else {
|
||||
messageIds = await dispatch(fetchReplies(messageId, limit, offset));
|
||||
setOffset(offset + messageIds.length);
|
||||
setOrder(order.concat(messageIds));
|
||||
}
|
||||
const fetchMore = useCallback(
|
||||
async (reset = false) => {
|
||||
let messageIds: any;
|
||||
if (reset) {
|
||||
messageIds = await dispatch(fetchReplies(messageId, 20, 0));
|
||||
setOffset(messageIds.length);
|
||||
setOrder(messageIds);
|
||||
} else {
|
||||
messageIds = await dispatch(fetchReplies(messageId, limit, offset));
|
||||
setOffset(offset + messageIds.length);
|
||||
setOrder(order.concat(messageIds));
|
||||
}
|
||||
|
||||
if (!messageIds.length) {
|
||||
setEnd(true);
|
||||
} else {
|
||||
setEnd(false);
|
||||
}
|
||||
}, [limit, offset, order, messageId]);
|
||||
if (!messageIds.length) {
|
||||
setEnd(true);
|
||||
} else {
|
||||
setEnd(false);
|
||||
}
|
||||
},
|
||||
[limit, offset, order, messageId]
|
||||
);
|
||||
|
||||
const showMore = useCallback(async () => {
|
||||
props.clearObserver && props.clearObserver();
|
||||
setOrder([]);
|
||||
await fetchMore();
|
||||
}, [fetchMore, messageId]);
|
||||
const showMore = useCallback(async () => {
|
||||
props.clearObserver && props.clearObserver();
|
||||
setOrder([]);
|
||||
await fetchMore();
|
||||
}, [fetchMore, messageId]);
|
||||
|
||||
const gotoPost = useGoToPost();
|
||||
const gotoPost = useGoToPost();
|
||||
|
||||
useEffect(() => {
|
||||
(async function onThreadMount() {
|
||||
if (!messageId || level >= 3) return;
|
||||
await fetchMore(true);
|
||||
})();
|
||||
}, [messageId, level, modOverride?.unmoderated]);
|
||||
useEffect(() => {
|
||||
(async function onThreadMount() {
|
||||
if (!messageId || level >= 3) return;
|
||||
await fetchMore(true);
|
||||
})();
|
||||
}, [messageId, level, modOverride?.unmoderated]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('thread', props.className)}
|
||||
>
|
||||
<Post
|
||||
className={classNames("mb-0.5", props.postClassName)}
|
||||
messageId={messageId}
|
||||
onClick={props.onClick}
|
||||
onSuccessPost={props.onSuccessPost}
|
||||
/>
|
||||
return (
|
||||
<div className={classNames('thread', props.className)}>
|
||||
<Post
|
||||
className={classNames('mb-0.5', props.postClassName)}
|
||||
messageId={messageId}
|
||||
onClick={props.onClick}
|
||||
onSuccessPost={props.onSuccessPost}
|
||||
/>
|
||||
<div className={classNames('pl-4', 'thread__content')}>
|
||||
{order.map(messageId => {
|
||||
return (
|
||||
<div key={messageId} className="pt-1">
|
||||
<Thread
|
||||
key={messageId}
|
||||
level={level + 1}
|
||||
postClassName={classNames('transition-colors cursor-pointer', 'border-l-4 mr-1', {
|
||||
'hover:border-gray-300 border-gray-200 bg-gray-50 ': theme !== 'dark',
|
||||
'hover:border-gray-700 border-gray-800 bg-gray-900': theme === 'dark',
|
||||
})}
|
||||
messageId={messageId}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
gotoPost(messageId);
|
||||
}}
|
||||
clearObserver={props.clearObserver}
|
||||
onSuccessPost={props.onSuccessPost}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!end && order.length < meta.replyCount && (
|
||||
<div
|
||||
className={classNames(
|
||||
'pl-4',
|
||||
'thread__content',
|
||||
)}
|
||||
>
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap items-center justify-center',
|
||||
'p-4 text-blue-400 hover:text-blue-300 cursor-pointer hover:underline',
|
||||
'border-t',
|
||||
{
|
||||
order.map(messageId => {
|
||||
return (
|
||||
<div key={messageId} className="pt-1">
|
||||
<Thread
|
||||
key={messageId}
|
||||
level={level + 1}
|
||||
postClassName={classNames(
|
||||
"transition-colors cursor-pointer",
|
||||
"border-l-4 mr-1",
|
||||
{
|
||||
"hover:border-gray-300 border-gray-200 bg-gray-50 ": theme !== 'dark',
|
||||
"hover:border-gray-700 border-gray-800 bg-gray-900": theme === 'dark',
|
||||
},
|
||||
)}
|
||||
messageId={messageId}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
gotoPost(messageId);
|
||||
}}
|
||||
clearObserver={props.clearObserver}
|
||||
onSuccessPost={props.onSuccessPost}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
{
|
||||
!end && order.length < meta.replyCount && (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-nowrap items-center justify-center",
|
||||
"p-4 text-blue-400 hover:text-blue-300 cursor-pointer hover:underline",
|
||||
"border-t",
|
||||
{
|
||||
"border-gray-200 bg-white": theme !== 'dark',
|
||||
"border-gray-800 bg-dark": theme === 'dark',
|
||||
},
|
||||
)}
|
||||
onClick={showMore}
|
||||
>
|
||||
Show More
|
||||
</div>
|
||||
)
|
||||
'border-gray-200 bg-white': theme !== 'dark',
|
||||
'border-gray-800 bg-dark': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
onClick={showMore}>
|
||||
Show More
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,437 +1,387 @@
|
||||
import React, {ReactElement, useCallback, useContext, useEffect, useState} from "react";
|
||||
import Icon from "../Icon";
|
||||
import classNames from "classnames";
|
||||
import "./top-nav.scss"
|
||||
import {Route, Switch, useHistory, useLocation, useParams} from "react-router";
|
||||
import Web3Button from "../Web3Button";
|
||||
import {
|
||||
useAccount,
|
||||
useGunLoggedIn,
|
||||
useSemaphoreID,
|
||||
} from "../../ducks/web3";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {fetchAddressByName, useUser} from "../../ducks/users";
|
||||
import Web3 from "web3";
|
||||
import {getName} from "../../util/user";
|
||||
import {useSelectedLocalId} from "../../ducks/worker";
|
||||
import {fetchNameByAddress} from "../../util/web3";
|
||||
import React, { ReactElement, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import Icon from '../Icon';
|
||||
import classNames from 'classnames';
|
||||
import './top-nav.scss';
|
||||
import { Route, Switch, useHistory, useLocation, useParams } from 'react-router';
|
||||
import Web3Button from '../Web3Button';
|
||||
import { useAccount, useGunLoggedIn, useSemaphoreID } from '../../ducks/web3';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { fetchAddressByName, useUser } from '../../ducks/users';
|
||||
import Web3 from 'web3';
|
||||
import { getName } from '../../util/user';
|
||||
import { useSelectedLocalId } from '../../ducks/worker';
|
||||
import { fetchNameByAddress } from '../../util/web3';
|
||||
// import Logo from "../../../static/icons/applogo.svg";
|
||||
import Modal from "../Modal";
|
||||
import MetaPanel from "../MetaPanel";
|
||||
import {useThemeContext} from "../ThemeContext";
|
||||
import config from "../../util/config";
|
||||
import Modal from '../Modal';
|
||||
import MetaPanel from '../MetaPanel';
|
||||
import { useThemeContext } from '../ThemeContext';
|
||||
import config from '../../util/config';
|
||||
|
||||
export default function TopNav(): ReactElement {
|
||||
const theme = useThemeContext();
|
||||
const theme = useThemeContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex-shrink-0',
|
||||
'flex', 'flex-row', 'flex-nowrap', 'items-center',
|
||||
'border-b',
|
||||
'top-nav',
|
||||
{
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-nowrap items-center flex-grow flex-shrink-0",
|
||||
)}
|
||||
>
|
||||
<Switch>
|
||||
<Route path="/explore" component={GlobalHeaderGroup} />
|
||||
<Route path="/home" component={GlobalHeaderGroup} />
|
||||
<Route path="/tag/:tagName" component={TagHeaderGroup} />
|
||||
<Route path="/:name/status/:hash" component={PostHeaderGroup} />
|
||||
<Route path="/post/:hash" component={PostHeaderGroup} />
|
||||
<Route path="/create-local-backup" component={DefaultHeaderGroup} />
|
||||
<Route path="/onboarding/interrep" component={DefaultHeaderGroup} />
|
||||
<Route path="/connect/twitter" component={DefaultHeaderGroup} />
|
||||
<Route path="/signup" component={DefaultHeaderGroup} />
|
||||
<Route path="/notification" component={DefaultHeaderGroup} />
|
||||
<Route path="/chat/:chatId?" component={ChatHeaderGroup} />
|
||||
<Route path="/settings" component={SettingHeaderGroup} />
|
||||
<Route path="/:name" component={UserProfileHeaderGroup} />
|
||||
<Route>
|
||||
<DefaultHeaderGroup />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-row flex-nowrap items-center flex-grow-0 flex-shrink-0 mx-4 h-20 mobile-hidden"
|
||||
>
|
||||
<NavIconRow />
|
||||
<Web3Button
|
||||
className={classNames("rounded-xl top-nav__web3-btn border", {
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex-shrink-0',
|
||||
'flex',
|
||||
'flex-row',
|
||||
'flex-nowrap',
|
||||
'items-center',
|
||||
'border-b',
|
||||
'top-nav',
|
||||
{
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
}
|
||||
)}>
|
||||
<div className={classNames('flex flex-row flex-nowrap items-center flex-grow flex-shrink-0')}>
|
||||
<Switch>
|
||||
<Route path="/explore" component={GlobalHeaderGroup} />
|
||||
<Route path="/home" component={GlobalHeaderGroup} />
|
||||
<Route path="/tag/:tagName" component={TagHeaderGroup} />
|
||||
<Route path="/:name/status/:hash" component={PostHeaderGroup} />
|
||||
<Route path="/post/:hash" component={PostHeaderGroup} />
|
||||
<Route path="/create-local-backup" component={DefaultHeaderGroup} />
|
||||
<Route path="/onboarding/interrep" component={DefaultHeaderGroup} />
|
||||
<Route path="/connect/twitter" component={DefaultHeaderGroup} />
|
||||
<Route path="/signup" component={DefaultHeaderGroup} />
|
||||
<Route path="/notification" component={DefaultHeaderGroup} />
|
||||
<Route path="/chat/:chatId?" component={ChatHeaderGroup} />
|
||||
<Route path="/settings" component={SettingHeaderGroup} />
|
||||
<Route path="/:name" component={UserProfileHeaderGroup} />
|
||||
<Route>
|
||||
<DefaultHeaderGroup />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="flex flex-row flex-nowrap items-center flex-grow-0 flex-shrink-0 mx-4 h-20 mobile-hidden">
|
||||
<NavIconRow />
|
||||
<Web3Button
|
||||
className={classNames('rounded-xl top-nav__web3-btn border', {
|
||||
'border-gray-200': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavIconRow() {
|
||||
const loggedIn = useGunLoggedIn();
|
||||
const account = useAccount();
|
||||
const selectedLocalId = useSelectedLocalId();
|
||||
const [ensName, setEnsName] = useState('');
|
||||
const theme = useThemeContext();
|
||||
const loggedIn = useGunLoggedIn();
|
||||
const account = useAccount();
|
||||
const selectedLocalId = useSelectedLocalId();
|
||||
const [ensName, setEnsName] = useState('');
|
||||
const theme = useThemeContext();
|
||||
|
||||
let address = '';
|
||||
let address = '';
|
||||
|
||||
if (loggedIn) {
|
||||
address = selectedLocalId?.address || account;
|
||||
}
|
||||
if (loggedIn) {
|
||||
address = selectedLocalId?.address || account;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const ens = await fetchNameByAddress(address);
|
||||
setEnsName(ens);
|
||||
})();
|
||||
}, [address]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const ens = await fetchNameByAddress(address);
|
||||
setEnsName(ens);
|
||||
})();
|
||||
}, [address]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-nowrap items-center flex-shrink-0",
|
||||
"rounded-xl border",
|
||||
"p-1 mx-4 overflow-hidden",
|
||||
'mobile-hidden',
|
||||
{
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<TopNavIcon fa="fas fa-home" pathname="/home" disabled={!loggedIn} />
|
||||
<TopNavIcon fa="fas fa-envelope" pathname={`/chat`} disabled={!selectedLocalId} />
|
||||
<TopNavIcon fa="fas fa-globe-asia" pathname="/explore" />
|
||||
{/*<TopNavIcon fa="fas fa-bell" pathname="/notifications" />*/}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap items-center flex-shrink-0',
|
||||
'rounded-xl border',
|
||||
'p-1 mx-4 overflow-hidden',
|
||||
'mobile-hidden',
|
||||
{
|
||||
'border-gray-100': theme !== 'dark',
|
||||
'border-gray-800': theme === 'dark',
|
||||
}
|
||||
)}>
|
||||
<TopNavIcon fa="fas fa-home" pathname="/home" disabled={!loggedIn} />
|
||||
<TopNavIcon fa="fas fa-envelope" pathname={`/chat`} disabled={!selectedLocalId} />
|
||||
<TopNavIcon fa="fas fa-globe-asia" pathname="/explore" />
|
||||
{/*<TopNavIcon fa="fas fa-bell" pathname="/notifications" />*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DefaultHeaderGroup() {
|
||||
const loggedIn = useGunLoggedIn();
|
||||
const account = useAccount();
|
||||
const selectedLocalId = useSelectedLocalId();
|
||||
const [ensName, setEnsName] = useState('');
|
||||
const loggedIn = useGunLoggedIn();
|
||||
const account = useAccount();
|
||||
const selectedLocalId = useSelectedLocalId();
|
||||
const [ensName, setEnsName] = useState('');
|
||||
|
||||
let address = '';
|
||||
let address = '';
|
||||
|
||||
if (loggedIn) {
|
||||
address = selectedLocalId?.address || account;
|
||||
}
|
||||
if (loggedIn) {
|
||||
address = selectedLocalId?.address || account;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const ens = await fetchNameByAddress(address);
|
||||
setEnsName(ens);
|
||||
})();
|
||||
}, [address]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const ens = await fetchNameByAddress(address);
|
||||
setEnsName(ens);
|
||||
})();
|
||||
}, [address]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-nowrap items-center flex-shrink-0",
|
||||
"p-1 mx-4 overflow-hidden",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
url="/applogo.svg"
|
||||
size={2}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap items-center flex-shrink-0',
|
||||
'p-1 mx-4 overflow-hidden'
|
||||
)}>
|
||||
<Icon url="/applogo.svg" size={2} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GlobalHeaderGroup() {
|
||||
const loggedIn = useGunLoggedIn();
|
||||
const account = useAccount();
|
||||
const selectedLocalId = useSelectedLocalId();
|
||||
const [ensName, setEnsName] = useState('');
|
||||
const loggedIn = useGunLoggedIn();
|
||||
const account = useAccount();
|
||||
const selectedLocalId = useSelectedLocalId();
|
||||
const [ensName, setEnsName] = useState('');
|
||||
|
||||
let address = '';
|
||||
let address = '';
|
||||
|
||||
if (loggedIn) {
|
||||
address = selectedLocalId?.address || account;
|
||||
}
|
||||
if (loggedIn) {
|
||||
address = selectedLocalId?.address || account;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const ens = await fetchNameByAddress(address);
|
||||
setEnsName(ens);
|
||||
})();
|
||||
}, [address]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const ens = await fetchNameByAddress(address);
|
||||
setEnsName(ens);
|
||||
})();
|
||||
}, [address]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-nowrap flex-grow items-center flex-shrink-0",
|
||||
"p-1 mx-4 overflow-hidden",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
url="/applogo.svg"
|
||||
size={2}
|
||||
/>
|
||||
<TopNavContextButton />
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap flex-grow items-center flex-shrink-0',
|
||||
'p-1 mx-4 overflow-hidden'
|
||||
)}>
|
||||
<Icon url="/applogo.svg" size={2} />
|
||||
<TopNavContextButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TopNavContextButton(): ReactElement {
|
||||
const [showing, showModal] = useState(false);
|
||||
const [showing, showModal] = useState(false);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{showing && (
|
||||
<Modal
|
||||
className="meta-modal"
|
||||
onClose={() => showModal(false)}
|
||||
>
|
||||
<MetaPanel className="mobile-only" />
|
||||
</Modal>
|
||||
)}
|
||||
<div className="felx flex-row flex-nowrap flex-grow justify-end items-center mobile-only">
|
||||
<Icon
|
||||
className="justify-end text-gray-200 hover:text-blue-400"
|
||||
fa="fas fa-binoculars"
|
||||
onClick={() => showModal(true)}
|
||||
size={1.25}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
{showing && (
|
||||
<Modal className="meta-modal" onClose={() => showModal(false)}>
|
||||
<MetaPanel className="mobile-only" />
|
||||
</Modal>
|
||||
)}
|
||||
<div className="felx flex-row flex-nowrap flex-grow justify-end items-center mobile-only">
|
||||
<Icon
|
||||
className="justify-end text-gray-200 hover:text-blue-400"
|
||||
fa="fas fa-binoculars"
|
||||
onClick={() => showModal(true)}
|
||||
size={1.25}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingHeaderGroup() {
|
||||
const history = useHistory();
|
||||
const history = useHistory();
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
if (history.action !== 'POP') return history.goBack();
|
||||
history.push('/');
|
||||
}, [history]);
|
||||
const goBack = useCallback(() => {
|
||||
if (history.action !== 'POP') return history.goBack();
|
||||
history.push('/');
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-nowrap items-center flex-shrink-0",
|
||||
"rounded-xl p-1 mx-4 overflow-hidden",
|
||||
"profile-header-group",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className="w-8 h-8 flex flex-row items-center justify-center top-nav__back-icon"
|
||||
fa="fas fa-chevron-left"
|
||||
onClick={goBack}
|
||||
/>
|
||||
<div
|
||||
className="flex flex-row flex-nowrap items-center px-2 py-2 profile-header-group__title-group"
|
||||
>
|
||||
<div
|
||||
className="flex flex-col flex-nowrap justify-center ml-2 font-bold text-lg "
|
||||
>
|
||||
Settings
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap items-center flex-shrink-0',
|
||||
'rounded-xl p-1 mx-4 overflow-hidden',
|
||||
'profile-header-group'
|
||||
)}>
|
||||
<Icon
|
||||
className="w-8 h-8 flex flex-row items-center justify-center top-nav__back-icon"
|
||||
fa="fas fa-chevron-left"
|
||||
onClick={goBack}
|
||||
/>
|
||||
<div className="flex flex-row flex-nowrap items-center px-2 py-2 profile-header-group__title-group">
|
||||
<div className="flex flex-col flex-nowrap justify-center ml-2 font-bold text-lg ">
|
||||
Settings
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatHeaderGroup() {
|
||||
const history = useHistory();
|
||||
const {chatId} = useParams<{chatId: string}>();
|
||||
const history = useHistory();
|
||||
const { chatId } = useParams<{ chatId: string }>();
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
if (history.action !== 'POP') return history.goBack();
|
||||
history.push('/');
|
||||
}, [history, chatId]);
|
||||
const goBack = useCallback(() => {
|
||||
if (history.action !== 'POP') return history.goBack();
|
||||
history.push('/');
|
||||
}, [history, chatId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-nowrap items-center flex-shrink-0",
|
||||
"rounded-xl p-1 mx-4 overflow-hidden",
|
||||
"profile-header-group",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className="w-8 h-8 flex flex-row items-center justify-center top-nav__back-icon"
|
||||
fa="fas fa-chevron-left"
|
||||
onClick={goBack}
|
||||
/>
|
||||
<div
|
||||
className="flex flex-row flex-nowrap items-center px-2 py-2 profile-header-group__title-group"
|
||||
>
|
||||
<div
|
||||
className="flex flex-col flex-nowrap justify-center ml-2 font-bold text-lg "
|
||||
>
|
||||
Chat
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap items-center flex-shrink-0',
|
||||
'rounded-xl p-1 mx-4 overflow-hidden',
|
||||
'profile-header-group'
|
||||
)}>
|
||||
<Icon
|
||||
className="w-8 h-8 flex flex-row items-center justify-center top-nav__back-icon"
|
||||
fa="fas fa-chevron-left"
|
||||
onClick={goBack}
|
||||
/>
|
||||
<div className="flex flex-row flex-nowrap items-center px-2 py-2 profile-header-group__title-group">
|
||||
<div className="flex flex-col flex-nowrap justify-center ml-2 font-bold text-lg ">Chat</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserProfileHeaderGroup() {
|
||||
const {name} = useParams<{ name: string }>();
|
||||
const [username, setUsername] = useState('');
|
||||
const { name } = useParams<{ name: string }>();
|
||||
const [username, setUsername] = useState('');
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const user = useUser(username);
|
||||
const dispatch = useDispatch();
|
||||
const user = useUser(username);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!Web3.utils.isAddress(name)) {
|
||||
const address: any = await dispatch(fetchAddressByName(name));
|
||||
setUsername(address);
|
||||
} else {
|
||||
setUsername(name);
|
||||
}
|
||||
})();
|
||||
}, [name]);
|
||||
const history = useHistory();
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!Web3.utils.isAddress(name)) {
|
||||
const address: any = await dispatch(fetchAddressByName(name));
|
||||
setUsername(address);
|
||||
} else {
|
||||
setUsername(name);
|
||||
}
|
||||
})();
|
||||
}, [name]);
|
||||
const history = useHistory();
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
if (history.action !== 'POP') return history.goBack();
|
||||
history.push('/');
|
||||
}, [history]);
|
||||
const goBack = useCallback(() => {
|
||||
if (history.action !== 'POP') return history.goBack();
|
||||
history.push('/');
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-nowrap flex-grow items-center flex-shrink-0",
|
||||
"rounded-xl p-1 mx-4 overflow-hidden",
|
||||
"profile-header-group",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className="w-8 h-8 flex flex-row items-center justify-center top-nav__back-icon"
|
||||
fa="fas fa-chevron-left"
|
||||
onClick={goBack}
|
||||
/>
|
||||
<div
|
||||
className="flex flex-row flex-nowrap items-center px-2 py-2 profile-header-group__title-group"
|
||||
>
|
||||
<div
|
||||
className="flex flex-col flex-nowrap justify-center ml-2"
|
||||
>
|
||||
<div className="font-bold text-lg profile-header-group__title">
|
||||
{getName(user)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 profile-header-group__subtitle">
|
||||
{user?.meta?.postingCount || 0} Posts
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TopNavContextButton />
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap flex-grow items-center flex-shrink-0',
|
||||
'rounded-xl p-1 mx-4 overflow-hidden',
|
||||
'profile-header-group'
|
||||
)}>
|
||||
<Icon
|
||||
className="w-8 h-8 flex flex-row items-center justify-center top-nav__back-icon"
|
||||
fa="fas fa-chevron-left"
|
||||
onClick={goBack}
|
||||
/>
|
||||
<div className="flex flex-row flex-nowrap items-center px-2 py-2 profile-header-group__title-group">
|
||||
<div className="flex flex-col flex-nowrap justify-center ml-2">
|
||||
<div className="font-bold text-lg profile-header-group__title">{getName(user)}</div>
|
||||
<div className="text-xs text-gray-500 profile-header-group__subtitle">
|
||||
{user?.meta?.postingCount || 0} Posts
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
<TopNavContextButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TagHeaderGroup() {
|
||||
const history = useHistory();
|
||||
const {tagName} = useParams<{ tagName: string }>();
|
||||
const tag = decodeURIComponent(tagName);
|
||||
const history = useHistory();
|
||||
const { tagName } = useParams<{ tagName: string }>();
|
||||
const tag = decodeURIComponent(tagName);
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
if (history.action !== 'POP') return history.goBack();
|
||||
history.push('/');
|
||||
}, [history]);
|
||||
const goBack = useCallback(() => {
|
||||
if (history.action !== 'POP') return history.goBack();
|
||||
history.push('/');
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-grow flex-nowrap items-center flex-shrink-0",
|
||||
"rounded-xl p-1 mx-4 overflow-hidden",
|
||||
"tag-header-group",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className="w-8 h-8 flex flex-row items-center justify-center top-nav__back-icon"
|
||||
fa="fas fa-chevron-left"
|
||||
onClick={goBack}
|
||||
/>
|
||||
<div
|
||||
className="flex flex-row flex-nowrap items-center px-2 py-2"
|
||||
>
|
||||
<div className="flex flex-col flex-nowrap justify-center ml-2">
|
||||
<div className="font-bold text-xl tag-header-group__tag-text">
|
||||
{tag}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TopNavContextButton />
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-grow flex-nowrap items-center flex-shrink-0',
|
||||
'rounded-xl p-1 mx-4 overflow-hidden',
|
||||
'tag-header-group'
|
||||
)}>
|
||||
<Icon
|
||||
className="w-8 h-8 flex flex-row items-center justify-center top-nav__back-icon"
|
||||
fa="fas fa-chevron-left"
|
||||
onClick={goBack}
|
||||
/>
|
||||
<div className="flex flex-row flex-nowrap items-center px-2 py-2">
|
||||
<div className="flex flex-col flex-nowrap justify-center ml-2">
|
||||
<div className="font-bold text-xl tag-header-group__tag-text">{tag}</div>
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
<TopNavContextButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PostHeaderGroup() {
|
||||
const history = useHistory();
|
||||
const history = useHistory();
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
if (history.action !== 'POP') return history.goBack();
|
||||
history.push('/');
|
||||
}, [history]);
|
||||
const goBack = useCallback(() => {
|
||||
if (history.action !== 'POP') return history.goBack();
|
||||
history.push('/');
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-nowrap flex-grow items-center flex-shrink-0",
|
||||
"rounded-xl p-1 mx-4 overflow-hidden",
|
||||
"post-header-group",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className="w-8 h-8 flex flex-row items-center justify-center top-nav__back-icon"
|
||||
fa="fas fa-chevron-left"
|
||||
onClick={goBack}
|
||||
/>
|
||||
<div
|
||||
className="flex flex-row flex-nowrap items-center px-2 py-2"
|
||||
>
|
||||
<div className="flex flex-col flex-nowrap justify-center ml-2">
|
||||
<div className="font-bold text-xl top-nav__text-title">
|
||||
Post
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TopNavContextButton />
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap flex-grow items-center flex-shrink-0',
|
||||
'rounded-xl p-1 mx-4 overflow-hidden',
|
||||
'post-header-group'
|
||||
)}>
|
||||
<Icon
|
||||
className="w-8 h-8 flex flex-row items-center justify-center top-nav__back-icon"
|
||||
fa="fas fa-chevron-left"
|
||||
onClick={goBack}
|
||||
/>
|
||||
<div className="flex flex-row flex-nowrap items-center px-2 py-2">
|
||||
<div className="flex flex-col flex-nowrap justify-center ml-2">
|
||||
<div className="font-bold text-xl top-nav__text-title">Post</div>
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
<TopNavContextButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type TopNavIconProps = {
|
||||
fa: string;
|
||||
pathname: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
fa: string;
|
||||
pathname: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
function TopNavIcon(props: TopNavIconProps): ReactElement {
|
||||
const history = useHistory();
|
||||
const {pathname} = useLocation();
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<Icon
|
||||
className={classNames(
|
||||
'flex', 'flex-row', 'items-center', 'justify-center',
|
||||
'top-nav__icon',
|
||||
{
|
||||
'top-nav__icon--selected': pathname === props.pathname,
|
||||
'top-nav__icon--disabled': props.disabled,
|
||||
}
|
||||
)}
|
||||
onClick={(pathname !== props.pathname && !props.disabled) ? () => history.push(props.pathname) : undefined}
|
||||
fa={props.fa}
|
||||
size={1.125}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Icon
|
||||
className={classNames('flex', 'flex-row', 'items-center', 'justify-center', 'top-nav__icon', {
|
||||
'top-nav__icon--selected': pathname === props.pathname,
|
||||
'top-nav__icon--disabled': props.disabled,
|
||||
})}
|
||||
onClick={
|
||||
pathname !== props.pathname && !props.disabled
|
||||
? () => history.push(props.pathname)
|
||||
: undefined
|
||||
}
|
||||
fa={props.fa}
|
||||
size={1.125}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.top-nav {
|
||||
&__icon {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
border-radius: .5rem;
|
||||
transition: background-color 100ms ease-in-out,
|
||||
color 100ms ease-in-out;
|
||||
border-radius: 0.5rem;
|
||||
transition: background-color 100ms ease-in-out, color 100ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
//background: rgba($primary-color, .15);
|
||||
@@ -30,18 +29,17 @@
|
||||
}
|
||||
|
||||
&__icon + &__icon {
|
||||
margin-left: .25rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
&__back-icon {
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
border-radius: 50%;
|
||||
transition: background-color 100ms ease-in-out,
|
||||
color 100ms ease-in-out;
|
||||
transition: background-color 100ms ease-in-out, color 100ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: rgba($primary-color, .15);
|
||||
background: rgba($primary-color, 0.15);
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
@@ -50,13 +48,12 @@
|
||||
.web3-button__unlock-menu {
|
||||
width: 16rem;
|
||||
position: fixed !important;
|
||||
margin-top: .5rem !important;
|
||||
margin-top: 0.5rem !important;
|
||||
top: 58px !important;
|
||||
right: 16px !important;
|
||||
left: auto !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +85,7 @@
|
||||
.top-nav {
|
||||
&__back-icon {
|
||||
i {
|
||||
font-size: .75rem !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,14 +102,14 @@
|
||||
.top-nav {
|
||||
&__back-icon {
|
||||
i {
|
||||
font-size: .75rem !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__title-group {
|
||||
padding-top: .25rem;
|
||||
padding-bottom: .25rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@@ -121,7 +118,7 @@
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: .75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +129,7 @@
|
||||
.top-nav {
|
||||
&__back-icon {
|
||||
i {
|
||||
font-size: .75rem !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,4 +164,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,242 +1,237 @@
|
||||
import React, {ReactElement, useCallback, useEffect, useState} from "react";
|
||||
import "./url-preview.scss";
|
||||
import Icon from "../Icon";
|
||||
import classNames from "classnames";
|
||||
import config from "../../util/config";
|
||||
import {shouldBlurImage} from "../../pages/SettingView";
|
||||
import SpinnerGif from "../../../static/icons/spinner.gif";
|
||||
import WebTorrentViewer from "../WebTorrentViewer";
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import './url-preview.scss';
|
||||
import Icon from '../Icon';
|
||||
import classNames from 'classnames';
|
||||
import config from '../../util/config';
|
||||
import { shouldBlurImage } from '../../pages/SettingView';
|
||||
import SpinnerGif from '../../../static/icons/spinner.gif';
|
||||
import WebTorrentViewer from '../WebTorrentViewer';
|
||||
|
||||
type Props = {
|
||||
url?: string;
|
||||
editable?: boolean;
|
||||
showAll?: boolean;
|
||||
onRemove?: () => void;
|
||||
className?: string;
|
||||
url?: string;
|
||||
editable?: boolean;
|
||||
showAll?: boolean;
|
||||
onRemove?: () => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type Preview = {
|
||||
link: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
mediaType: string;
|
||||
contentType: string;
|
||||
favicon: string;
|
||||
}
|
||||
link: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
mediaType: string;
|
||||
contentType: string;
|
||||
favicon: string;
|
||||
};
|
||||
|
||||
function parseURL(url = ''): URL | null {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function URLPreview(props: Props): ReactElement {
|
||||
const {url, editable} = props;
|
||||
const [imageSrc, setImageSrc] = useState('');
|
||||
const [preview, setPreview] = useState<Preview|null>(null);
|
||||
const [isBlur, setBlur] = useState(shouldBlurImage());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { url, editable } = props;
|
||||
const [imageSrc, setImageSrc] = useState('');
|
||||
const [preview, setPreview] = useState<Preview | null>(null);
|
||||
const [isBlur, setBlur] = useState(shouldBlurImage());
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const urlParams = parseURL(url);
|
||||
const isMagnet = urlParams?.protocol === 'magnet:';
|
||||
const urlParams = parseURL(url);
|
||||
const isMagnet = urlParams?.protocol === 'magnet:';
|
||||
|
||||
useEffect(() => {
|
||||
(async function onURLPreviewLoad() {
|
||||
setPreview(null);
|
||||
setImageSrc('');
|
||||
|
||||
useEffect(() => {
|
||||
(async function onURLPreviewLoad() {
|
||||
setPreview(null);
|
||||
setImageSrc('');
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
if (isMagnet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMagnet) {
|
||||
return;
|
||||
}
|
||||
if (await testImage(url)) {
|
||||
setImageSrc(url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await testImage(url)) {
|
||||
setImageSrc(url);
|
||||
return;
|
||||
}
|
||||
const resp = await fetch(`${config.indexerAPI}/preview?link=${encodeURI(url)}`);
|
||||
const json = await resp.json();
|
||||
|
||||
const resp = await fetch(`${config.indexerAPI}/preview?link=${encodeURI(url)}`);
|
||||
const json = await resp.json();
|
||||
if (!json.payload.error) {
|
||||
const {
|
||||
link,
|
||||
title = '',
|
||||
description = '',
|
||||
image = '',
|
||||
mediaType = '',
|
||||
contentType = '',
|
||||
favicon = '',
|
||||
} = json.payload;
|
||||
|
||||
if (!json.payload.error) {
|
||||
const {
|
||||
link,
|
||||
title = '',
|
||||
description = '',
|
||||
image = '',
|
||||
mediaType = '',
|
||||
contentType = '',
|
||||
favicon = '',
|
||||
} = json.payload;
|
||||
setPreview({
|
||||
link,
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
mediaType,
|
||||
contentType,
|
||||
favicon,
|
||||
});
|
||||
|
||||
setPreview({
|
||||
link,
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
mediaType,
|
||||
contentType,
|
||||
favicon,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
setImageSrc('');
|
||||
setPreview(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [url]);
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
setImageSrc('');
|
||||
setPreview(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [url]);
|
||||
const openImageLink = useCallback(
|
||||
(e: any) => {
|
||||
e.stopPropagation();
|
||||
window.open(imageSrc, '_blank');
|
||||
},
|
||||
[imageSrc]
|
||||
);
|
||||
|
||||
const openImageLink = useCallback((e: any) => {
|
||||
e.stopPropagation();
|
||||
window.open(imageSrc, '_blank');
|
||||
}, [imageSrc]);
|
||||
|
||||
const openLink = useCallback((e: any) => {
|
||||
if (!preview?.link) return;
|
||||
e.stopPropagation();
|
||||
window.open(preview?.link, '_blank');
|
||||
}, [preview?.link]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={classNames("url-preview", props.className)}>
|
||||
<div className="flex flex-row items-center">
|
||||
<Icon url={SpinnerGif} size={2.5} />
|
||||
<small className="text-gray-500 font-semibold text-xs">Loading...</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!imageSrc && !preview && !isMagnet) {
|
||||
return <></>;
|
||||
}
|
||||
const openLink = useCallback(
|
||||
(e: any) => {
|
||||
if (!preview?.link) return;
|
||||
e.stopPropagation();
|
||||
window.open(preview?.link, '_blank');
|
||||
},
|
||||
[preview?.link]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div
|
||||
className={classNames("url-preview", props.className, {
|
||||
'url-preview--wt': urlParams?.protocol === 'magnet:',
|
||||
})}
|
||||
>
|
||||
{ urlParams?.protocol === 'magnet:' && (
|
||||
<WebTorrentViewer
|
||||
url={urlParams.href}
|
||||
/>
|
||||
)}
|
||||
|
||||
{ imageSrc && (
|
||||
<div
|
||||
className={classNames("url-preview__img-container", {
|
||||
'url-preview__img-container--showAll': props.showAll,
|
||||
'blurred-image': !props.editable && isBlur,
|
||||
'unblurred-image': props.editable || !isBlur,
|
||||
})}
|
||||
>
|
||||
<img
|
||||
className={classNames("url-preview__img", {
|
||||
'cursor-pointer': !props.editable,
|
||||
})}
|
||||
src={imageSrc}
|
||||
onClick={!props.editable ? openImageLink : undefined}
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
|
||||
{ preview && (
|
||||
<div
|
||||
className={classNames("url-preview__link-container", {
|
||||
'cursor-pointer': !props.editable,
|
||||
})}
|
||||
onClick={!props.editable ? openLink : undefined}
|
||||
>
|
||||
{
|
||||
preview.image && (
|
||||
<div
|
||||
className={classNames("url-preview__link-image", {
|
||||
'blurred-image': !props.editable && isBlur,
|
||||
'unblurred-image': props.editable || !isBlur,
|
||||
})}
|
||||
>
|
||||
<img src={preview.image} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
(preview.title || preview.description)
|
||||
? (
|
||||
<div className="url-preview__link-content">
|
||||
<div className="url-preview__link-title">{preview.title}</div>
|
||||
<div className="url-preview__link-desc">{preview.description}</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<a className="px-4 py-2 text-light text-ellipsis overflow-hidden" href={url} target="_blank">{url}</a>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
) }
|
||||
|
||||
{ editable && (
|
||||
<Icon
|
||||
className="url-preview__close bg-black bg-opacity-80 text-white absolute top-4 left-4 w-8 h-8"
|
||||
fa="fas fa-times"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
props.onRemove && props.onRemove();
|
||||
}}
|
||||
/>
|
||||
) }
|
||||
|
||||
{ !editable && !isMagnet && !!(imageSrc || preview?.image) && (
|
||||
<Icon
|
||||
className="url-preview__close bg-black bg-opacity-80 text-white absolute top-4 left-4 w-8 h-8"
|
||||
fa={isBlur ? "fas fa-eye-slash" : "fas fa-eye"}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setBlur(!isBlur);
|
||||
}}
|
||||
/>
|
||||
) }
|
||||
<div className={classNames('url-preview', props.className)}>
|
||||
<div className="flex flex-row items-center">
|
||||
<Icon url={SpinnerGif} size={2.5} />
|
||||
<small className="text-gray-500 font-semibold text-xs">Loading...</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if (!imageSrc && !preview && !isMagnet) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('url-preview', props.className, {
|
||||
'url-preview--wt': urlParams?.protocol === 'magnet:',
|
||||
})}>
|
||||
{urlParams?.protocol === 'magnet:' && <WebTorrentViewer url={urlParams.href} />}
|
||||
|
||||
{imageSrc && (
|
||||
<div
|
||||
className={classNames('url-preview__img-container', {
|
||||
'url-preview__img-container--showAll': props.showAll,
|
||||
'blurred-image': !props.editable && isBlur,
|
||||
'unblurred-image': props.editable || !isBlur,
|
||||
})}>
|
||||
<img
|
||||
className={classNames('url-preview__img', {
|
||||
'cursor-pointer': !props.editable,
|
||||
})}
|
||||
src={imageSrc}
|
||||
onClick={!props.editable ? openImageLink : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{preview && (
|
||||
<div
|
||||
className={classNames('url-preview__link-container', {
|
||||
'cursor-pointer': !props.editable,
|
||||
})}
|
||||
onClick={!props.editable ? openLink : undefined}>
|
||||
{preview.image && (
|
||||
<div
|
||||
className={classNames('url-preview__link-image', {
|
||||
'blurred-image': !props.editable && isBlur,
|
||||
'unblurred-image': props.editable || !isBlur,
|
||||
})}>
|
||||
<img src={preview.image} />
|
||||
</div>
|
||||
)}
|
||||
{preview.title || preview.description ? (
|
||||
<div className="url-preview__link-content">
|
||||
<div className="url-preview__link-title">{preview.title}</div>
|
||||
<div className="url-preview__link-desc">{preview.description}</div>
|
||||
</div>
|
||||
) : (
|
||||
<a
|
||||
className="px-4 py-2 text-light text-ellipsis overflow-hidden"
|
||||
href={url}
|
||||
target="_blank">
|
||||
{url}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editable && (
|
||||
<Icon
|
||||
className="url-preview__close bg-black bg-opacity-80 text-white absolute top-4 left-4 w-8 h-8"
|
||||
fa="fas fa-times"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
props.onRemove && props.onRemove();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!editable && !isMagnet && !!(imageSrc || preview?.image) && (
|
||||
<Icon
|
||||
className="url-preview__close bg-black bg-opacity-80 text-white absolute top-4 left-4 w-8 h-8"
|
||||
fa={isBlur ? 'fas fa-eye-slash' : 'fas fa-eye'}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setBlur(!isBlur);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function testImage(url: string) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const timer = setTimeout(function () {
|
||||
// reset .src to invalid URL so it stops previous
|
||||
// loading, but doesn't trigger new load
|
||||
img.src = "//!!!!/test.jpg";
|
||||
resolve(false);
|
||||
}, 60000);
|
||||
return new Promise(function (resolve, reject) {
|
||||
const timer = setTimeout(function () {
|
||||
// reset .src to invalid URL so it stops previous
|
||||
// loading, but doesn't trigger new load
|
||||
img.src = '//!!!!/test.jpg';
|
||||
resolve(false);
|
||||
}, 60000);
|
||||
|
||||
const img = new Image();
|
||||
const img = new Image();
|
||||
|
||||
img.onerror = img.onabort = function () {
|
||||
clearTimeout(timer);
|
||||
resolve(false);
|
||||
};
|
||||
img.onerror = img.onabort = function () {
|
||||
clearTimeout(timer);
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
img.onload = function () {
|
||||
clearTimeout(timer);
|
||||
resolve(true);
|
||||
};
|
||||
img.onload = function () {
|
||||
clearTimeout(timer);
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.dark {
|
||||
.url-preview {
|
||||
@@ -73,14 +73,14 @@
|
||||
|
||||
&__link-content {
|
||||
@extend %col-nowrap;
|
||||
padding: .75rem 1rem;
|
||||
font-size: .9375rem;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.35rem;
|
||||
}
|
||||
|
||||
&__link-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: .25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-height: 2.7rem;
|
||||
@@ -92,4 +92,4 @@
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,12 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
.web3-button {
|
||||
background-color: rgba($primary-color, .1);
|
||||
background-color: rgba($primary-color, 0.1);
|
||||
color: $primary-color;
|
||||
font-family: Inter, sans-serif;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px rgba($primary-color, .5);
|
||||
box-shadow: 0 0 0 1px rgba($primary-color, 0.5);
|
||||
}
|
||||
|
||||
&__alt-action {
|
||||
@@ -22,7 +22,7 @@
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
border-radius: 50%;
|
||||
margin: 0 .375rem;
|
||||
margin: 0 0.375rem;
|
||||
|
||||
&:hover {
|
||||
//color: $primary-color;
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
&__item {
|
||||
width: 100%;
|
||||
padding: .5rem 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
&__selected-item {
|
||||
|
||||
@@ -1,65 +1,69 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {act} from "react-dom/test-utils";
|
||||
import {Provider} from "react-redux";
|
||||
import {ducks, store} from "../../util/testUtils";
|
||||
import Web3Button from "./index";
|
||||
import {setSelectedId} from "../../ducks/worker";
|
||||
import {ZkIdentity} from "@zk-kit/identity";
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Provider } from 'react-redux';
|
||||
import { ducks, store } from '../../util/testUtils';
|
||||
import Web3Button from './index';
|
||||
import { setSelectedId } from '../../ducks/worker';
|
||||
import { ZkIdentity } from '@zk-kit/identity';
|
||||
|
||||
describe('<Web3Button>', () => {
|
||||
const root = document.createElement('div');
|
||||
const root = document.createElement('div');
|
||||
|
||||
it ('should mount', async () => {
|
||||
act(() => {
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Web3Button />
|
||||
</Provider>,
|
||||
root,
|
||||
)
|
||||
});
|
||||
|
||||
expect(root.textContent).toBe('Add a user');
|
||||
it('should mount', async () => {
|
||||
act(() => {
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Web3Button />
|
||||
</Provider>,
|
||||
root
|
||||
);
|
||||
});
|
||||
|
||||
it ('should render gun identity', async () => {
|
||||
await store.dispatch(ducks.worker.setSelectedId({
|
||||
type: "gun",
|
||||
address: '0xgunuser',
|
||||
privateKey: '0xpriv',
|
||||
publicKey: '0xpub',
|
||||
nonce: 1,
|
||||
}));
|
||||
expect(root.textContent).toBe('0xgunu...user');
|
||||
});
|
||||
expect(root.textContent).toBe('Add a user');
|
||||
});
|
||||
|
||||
it ('should render gun identity', async () => {
|
||||
const id = new ZkIdentity();
|
||||
await store.dispatch(ducks.worker.setSelectedId({
|
||||
type: "interrep",
|
||||
nonce: 0,
|
||||
address: '0xinterrep',
|
||||
identityCommitment: id.genIdentityCommitment().toString(16),
|
||||
serializedIdentity: id.serializeIdentity(),
|
||||
provider: 'twitter',
|
||||
name: 'gold',
|
||||
identityPath: null,
|
||||
}));
|
||||
expect(root.textContent).toBe('Incognito');
|
||||
});
|
||||
|
||||
it ('should render zkpr identity', async () => {
|
||||
const id = new ZkIdentity();
|
||||
await store.dispatch(ducks.worker.setSelectedId({
|
||||
type: "zkpr_interrep",
|
||||
identityCommitment: id.genIdentityCommitment().toString(16),
|
||||
provider: 'twitter',
|
||||
name: 'gold',
|
||||
identityPath: null,
|
||||
}));
|
||||
expect(root.textContent).toBe('Connected to ZKPR');
|
||||
});
|
||||
it('should render gun identity', async () => {
|
||||
await store.dispatch(
|
||||
ducks.worker.setSelectedId({
|
||||
type: 'gun',
|
||||
address: '0xgunuser',
|
||||
privateKey: '0xpriv',
|
||||
publicKey: '0xpub',
|
||||
nonce: 1,
|
||||
})
|
||||
);
|
||||
expect(root.textContent).toBe('0xgunu...user');
|
||||
});
|
||||
|
||||
it('should render gun identity', async () => {
|
||||
const id = new ZkIdentity();
|
||||
await store.dispatch(
|
||||
ducks.worker.setSelectedId({
|
||||
type: 'interrep',
|
||||
nonce: 0,
|
||||
address: '0xinterrep',
|
||||
identityCommitment: id.genIdentityCommitment().toString(16),
|
||||
serializedIdentity: id.serializeIdentity(),
|
||||
provider: 'twitter',
|
||||
name: 'gold',
|
||||
identityPath: null,
|
||||
})
|
||||
);
|
||||
expect(root.textContent).toBe('Incognito');
|
||||
});
|
||||
|
||||
it('should render zkpr identity', async () => {
|
||||
const id = new ZkIdentity();
|
||||
await store.dispatch(
|
||||
ducks.worker.setSelectedId({
|
||||
type: 'zkpr_interrep',
|
||||
identityCommitment: id.genIdentityCommitment().toString(16),
|
||||
provider: 'twitter',
|
||||
name: 'gold',
|
||||
identityPath: null,
|
||||
})
|
||||
);
|
||||
expect(root.textContent).toBe('Connected to ZKPR');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,297 +1,306 @@
|
||||
import React, {ReactElement, useCallback, useEffect, useRef, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import {addMagnetURL, getInfoHashFromMagnet, getWebtorrentClient, removeMagnetURL} from "../../util/webtorrent";
|
||||
import Icon from "../Icon";
|
||||
import SpinnerGif from "../../../static/icons/spinner.gif";
|
||||
import {Torrent, TorrentFile} from "webtorrent";
|
||||
import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
addMagnetURL,
|
||||
getInfoHashFromMagnet,
|
||||
getWebtorrentClient,
|
||||
removeMagnetURL,
|
||||
} from '../../util/webtorrent';
|
||||
import Icon from '../Icon';
|
||||
import SpinnerGif from '../../../static/icons/spinner.gif';
|
||||
import { Torrent, TorrentFile } from 'webtorrent';
|
||||
import mime from 'mime-types';
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import WTIcon from "../../../static/icons/webtorrent-small.png";
|
||||
import "./wt-viewer.scss";
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import WTIcon from '../../../static/icons/webtorrent-small.png';
|
||||
import './wt-viewer.scss';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
url: string;
|
||||
}
|
||||
className?: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function WebTorrentViewer(props: Props): ReactElement {
|
||||
const viewer = useRef<HTMLDivElement>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [files, setFiles] = useState<{name: string; length: number; progress: number}[]>([]);
|
||||
const [selectedFile, selectFile] = useState<TorrentFile|null>(null);
|
||||
const [currentTorrent, setTorrent] = useState<Torrent|null>(null);
|
||||
const [isDownloading, setDownloading] = useState(false);
|
||||
const [isSeeding, setSeeding] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const viewer = useRef<HTMLDivElement>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [files, setFiles] = useState<{ name: string; length: number; progress: number }[]>([]);
|
||||
const [selectedFile, selectFile] = useState<TorrentFile | null>(null);
|
||||
const [currentTorrent, setTorrent] = useState<Torrent | null>(null);
|
||||
const [isDownloading, setDownloading] = useState(false);
|
||||
const [isSeeding, setSeeding] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const onFileClick = useCallback(async (fileIndex: number) => {
|
||||
const client = getWebtorrentClient();
|
||||
const torrent = client.get(getInfoHashFromMagnet(props.url)) || await addMagnetURL(props.url);
|
||||
setTorrent(torrent);
|
||||
const file = torrent.files[fileIndex];
|
||||
const onFileClick = useCallback(
|
||||
async (fileIndex: number) => {
|
||||
const client = getWebtorrentClient();
|
||||
const torrent =
|
||||
client.get(getInfoHashFromMagnet(props.url)) || (await addMagnetURL(props.url));
|
||||
setTorrent(torrent);
|
||||
const file = torrent.files[fileIndex];
|
||||
|
||||
const type = mime.lookup(file.name) || '';
|
||||
const isImage = /image/.test(type);
|
||||
const isVideo = /video/.test(type);
|
||||
const isAudio = /audio/.test(type);
|
||||
const type = mime.lookup(file.name) || '';
|
||||
const isImage = /image/.test(type);
|
||||
const isVideo = /video/.test(type);
|
||||
const isAudio = /audio/.test(type);
|
||||
|
||||
const onFileReady = () => {
|
||||
setLoading(true);
|
||||
|
||||
if (isImage) {
|
||||
if (file.progress === 1) {
|
||||
if (file.progress) {
|
||||
torrent.off('download', onFileReady);
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
selectFile(file);
|
||||
}
|
||||
} else {
|
||||
if (file.progress) {
|
||||
selectFile(file);
|
||||
torrent.off('download', onFileReady);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onDownload = () => {
|
||||
setFiles(torrent.files.map(({ name, length, progress }) => ({ name, length, progress })));
|
||||
setProgress(torrent.progress);
|
||||
if (torrent.progress === 1) {
|
||||
torrent.off('download', onDownload);
|
||||
setDownloading(false);
|
||||
setSeeding(true);
|
||||
} else {
|
||||
setDownloading(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (file.progress === 1) {
|
||||
selectFile(file);
|
||||
} else {
|
||||
torrent.on('download', onFileReady);
|
||||
torrent.on('download', onDownload);
|
||||
torrent.once('done', () => {
|
||||
onFileReady();
|
||||
onDownload();
|
||||
});
|
||||
}
|
||||
|
||||
}, [props.url]);
|
||||
|
||||
const onDownload = useCallback(async (fileIndex: number) => {
|
||||
const client = getWebtorrentClient();
|
||||
const torrent = client.get(getInfoHashFromMagnet(props.url)) || await addMagnetURL(props.url);
|
||||
setTorrent(torrent);
|
||||
const file = torrent.files[fileIndex];
|
||||
|
||||
const onDownload = () => {
|
||||
setFiles(torrent.files.map(({ name, length, progress }) => ({ name, length, progress })));
|
||||
setProgress(torrent.progress);
|
||||
setDownloading(torrent.progress !== 1);
|
||||
}
|
||||
|
||||
if (torrent.progress !== 1) {
|
||||
torrent.on('download', onDownload);
|
||||
torrent.once('done', () => {
|
||||
onDownload();
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
file.getBlobURL((err, url) => {
|
||||
if (err || !url) throw err
|
||||
const a = document.createElement('a')
|
||||
a.download = file.name;
|
||||
a.href = url;
|
||||
a.click();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}, [props.url]);
|
||||
|
||||
useEffect(() => {
|
||||
(async function onFileChanged () {
|
||||
if (!selectedFile || !viewer.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = viewer.current;
|
||||
const type = mime.lookup(selectedFile.name) || '';
|
||||
const isImage = /image/.test(type);
|
||||
const isVideo = /video/.test(type);
|
||||
const isAudio = /audio/.test(type);
|
||||
|
||||
if (!isImage && !isVideo && !isAudio) {
|
||||
return;
|
||||
}
|
||||
|
||||
current.innerHTML = '';
|
||||
|
||||
let el: any;
|
||||
|
||||
if (isImage) {
|
||||
el = document.createElement('img');
|
||||
}
|
||||
|
||||
if (isVideo) {
|
||||
el = document.createElement('video');
|
||||
}
|
||||
|
||||
if (isAudio) {
|
||||
el = document.createElement('audio');
|
||||
}
|
||||
|
||||
current.appendChild(el);
|
||||
selectedFile.renderTo(el);
|
||||
if (el.play) el.play();
|
||||
})();
|
||||
|
||||
}, [selectedFile, viewer.current, props.url]);
|
||||
|
||||
useEffect(() => {
|
||||
const onFileReady = () => {
|
||||
setLoading(true);
|
||||
|
||||
(async function onMount() {
|
||||
const client = getWebtorrentClient();
|
||||
const t = client.get(getInfoHashFromMagnet(props.url)) || await addMagnetURL(props.url);
|
||||
setTorrent(t);
|
||||
setFiles(t.files.map(({ name, length, progress }) => ({ name, length, progress })));
|
||||
await new Promise(r => t.destroy({ destroyStore: false }, r));
|
||||
if (isImage) {
|
||||
if (file.progress === 1) {
|
||||
if (file.progress) {
|
||||
torrent.off('download', onFileReady);
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
selectFile(file);
|
||||
}
|
||||
} else {
|
||||
if (file.progress) {
|
||||
selectFile(file);
|
||||
torrent.off('download', onFileReady);
|
||||
setLoading(false);
|
||||
})();
|
||||
}, [props.url]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const client = getWebtorrentClient();
|
||||
const t = client.get(getInfoHashFromMagnet(props.url));
|
||||
|
||||
if (t) {
|
||||
t.destroy({destroyStore: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [props.url]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('wt-viewer', props.className)}
|
||||
>
|
||||
{loading && (
|
||||
<div className="flex flex-row items-center">
|
||||
<Icon url={SpinnerGif} size={2.5} />
|
||||
<small className="text-gray-500 font-semibold text-xs">Loading...</small>
|
||||
</div>
|
||||
)}
|
||||
<div className="wt-viewer__viewer" ref={viewer} />
|
||||
{!!files.length && (
|
||||
<div className="wt-viewer__files">
|
||||
{files.map((file, i) => {
|
||||
return (
|
||||
<FileRow
|
||||
key={i}
|
||||
name={file.name}
|
||||
length={file.length}
|
||||
progress={file.progress}
|
||||
onFileClick={() => onFileClick(i)}
|
||||
onDownload={() => onDownload(i)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
currentTorrent && (
|
||||
<div
|
||||
className="flex flex-row items-center px-4 py-2 bg-gray-200"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<Icon
|
||||
className="text-gray-400 mr-2 cursor-pointer"
|
||||
fa={(isDownloading || isSeeding) ? 'fas fa-stop' : undefined}
|
||||
url={(isDownloading || isSeeding) ? undefined : WTIcon}
|
||||
size={(isDownloading || isSeeding) ? .75 : 1.25}
|
||||
onClick={async (e) => {
|
||||
const client = getWebtorrentClient();
|
||||
const torrent = client.get(getInfoHashFromMagnet(props.url));
|
||||
if (torrent) {
|
||||
await new Promise(r => torrent.destroy({ destroyStore: false }, r));
|
||||
setDownloading(false);
|
||||
setSeeding(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs text-gray-400">
|
||||
{
|
||||
isDownloading
|
||||
? `${prettyBytes(currentTorrent.downloadSpeed)}/s - Downloading ${currentTorrent.name}`
|
||||
: isSeeding
|
||||
? `${prettyBytes(currentTorrent.uploadSpeed)}/s - Sharing with ${currentTorrent.numPeers} peers`
|
||||
: 'WebTorrent'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const onDownload = () => {
|
||||
setFiles(torrent.files.map(({ name, length, progress }) => ({ name, length, progress })));
|
||||
setProgress(torrent.progress);
|
||||
if (torrent.progress === 1) {
|
||||
torrent.off('download', onDownload);
|
||||
setDownloading(false);
|
||||
setSeeding(true);
|
||||
} else {
|
||||
setDownloading(true);
|
||||
}
|
||||
};
|
||||
|
||||
if (file.progress === 1) {
|
||||
selectFile(file);
|
||||
} else {
|
||||
torrent.on('download', onFileReady);
|
||||
torrent.on('download', onDownload);
|
||||
torrent.once('done', () => {
|
||||
onFileReady();
|
||||
onDownload();
|
||||
});
|
||||
}
|
||||
},
|
||||
[props.url]
|
||||
);
|
||||
|
||||
const onDownload = useCallback(
|
||||
async (fileIndex: number) => {
|
||||
const client = getWebtorrentClient();
|
||||
const torrent =
|
||||
client.get(getInfoHashFromMagnet(props.url)) || (await addMagnetURL(props.url));
|
||||
setTorrent(torrent);
|
||||
const file = torrent.files[fileIndex];
|
||||
|
||||
const onDownload = () => {
|
||||
setFiles(torrent.files.map(({ name, length, progress }) => ({ name, length, progress })));
|
||||
setProgress(torrent.progress);
|
||||
setDownloading(torrent.progress !== 1);
|
||||
};
|
||||
|
||||
if (torrent.progress !== 1) {
|
||||
torrent.on('download', onDownload);
|
||||
torrent.once('done', () => {
|
||||
onDownload();
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
file.getBlobURL((err, url) => {
|
||||
if (err || !url) throw err;
|
||||
const a = document.createElement('a');
|
||||
a.download = file.name;
|
||||
a.href = url;
|
||||
a.click();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
[props.url]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async function onFileChanged() {
|
||||
if (!selectedFile || !viewer.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = viewer.current;
|
||||
const type = mime.lookup(selectedFile.name) || '';
|
||||
const isImage = /image/.test(type);
|
||||
const isVideo = /video/.test(type);
|
||||
const isAudio = /audio/.test(type);
|
||||
|
||||
if (!isImage && !isVideo && !isAudio) {
|
||||
return;
|
||||
}
|
||||
|
||||
current.innerHTML = '';
|
||||
|
||||
let el: any;
|
||||
|
||||
if (isImage) {
|
||||
el = document.createElement('img');
|
||||
}
|
||||
|
||||
if (isVideo) {
|
||||
el = document.createElement('video');
|
||||
}
|
||||
|
||||
if (isAudio) {
|
||||
el = document.createElement('audio');
|
||||
}
|
||||
|
||||
current.appendChild(el);
|
||||
selectedFile.renderTo(el);
|
||||
if (el.play) el.play();
|
||||
})();
|
||||
}, [selectedFile, viewer.current, props.url]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
(async function onMount() {
|
||||
const client = getWebtorrentClient();
|
||||
const t = client.get(getInfoHashFromMagnet(props.url)) || (await addMagnetURL(props.url));
|
||||
setTorrent(t);
|
||||
setFiles(t.files.map(({ name, length, progress }) => ({ name, length, progress })));
|
||||
await new Promise(r => t.destroy({ destroyStore: false }, r));
|
||||
setLoading(false);
|
||||
})();
|
||||
}, [props.url]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const client = getWebtorrentClient();
|
||||
const t = client.get(getInfoHashFromMagnet(props.url));
|
||||
|
||||
if (t) {
|
||||
t.destroy({ destroyStore: false });
|
||||
}
|
||||
};
|
||||
}, [props.url]);
|
||||
|
||||
return (
|
||||
<div className={classNames('wt-viewer', props.className)}>
|
||||
{loading && (
|
||||
<div className="flex flex-row items-center">
|
||||
<Icon url={SpinnerGif} size={2.5} />
|
||||
<small className="text-gray-500 font-semibold text-xs">Loading...</small>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<div className="wt-viewer__viewer" ref={viewer} />
|
||||
{!!files.length && (
|
||||
<div className="wt-viewer__files">
|
||||
{files.map((file, i) => {
|
||||
return (
|
||||
<FileRow
|
||||
key={i}
|
||||
name={file.name}
|
||||
length={file.length}
|
||||
progress={file.progress}
|
||||
onFileClick={() => onFileClick(i)}
|
||||
onDownload={() => onDownload(i)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{currentTorrent && (
|
||||
<div
|
||||
className="flex flex-row items-center px-4 py-2 bg-gray-200"
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<Icon
|
||||
className="text-gray-400 mr-2 cursor-pointer"
|
||||
fa={isDownloading || isSeeding ? 'fas fa-stop' : undefined}
|
||||
url={isDownloading || isSeeding ? undefined : WTIcon}
|
||||
size={isDownloading || isSeeding ? 0.75 : 1.25}
|
||||
onClick={async e => {
|
||||
const client = getWebtorrentClient();
|
||||
const torrent = client.get(getInfoHashFromMagnet(props.url));
|
||||
if (torrent) {
|
||||
await new Promise(r => torrent.destroy({ destroyStore: false }, r));
|
||||
setDownloading(false);
|
||||
setSeeding(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs text-gray-400">
|
||||
{isDownloading
|
||||
? `${prettyBytes(currentTorrent.downloadSpeed)}/s - Downloading ${
|
||||
currentTorrent.name
|
||||
}`
|
||||
: isSeeding
|
||||
? `${prettyBytes(currentTorrent.uploadSpeed)}/s - Sharing with ${
|
||||
currentTorrent.numPeers
|
||||
} peers`
|
||||
: 'WebTorrent'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FileRow(props: {
|
||||
name: string;
|
||||
length: number;
|
||||
onFileClick: () => void;
|
||||
onDownload: () => void;
|
||||
progress: number;
|
||||
name: string;
|
||||
length: number;
|
||||
onFileClick: () => void;
|
||||
onDownload: () => void;
|
||||
progress: number;
|
||||
}): ReactElement {
|
||||
const type = mime.lookup(props.name) || '';
|
||||
const isImage = /image/.test(type);
|
||||
const isVideo = /video/.test(type);
|
||||
const isAudio = /audio/.test(type);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const type = mime.lookup(props.name) || '';
|
||||
const isImage = /image/.test(type);
|
||||
const isVideo = /video/.test(type);
|
||||
const isAudio = /audio/.test(type);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
const onDownload = async () => {
|
||||
setDownloading(true);
|
||||
await props.onDownload();
|
||||
setDownloading(false);
|
||||
};
|
||||
const onDownload = async () => {
|
||||
setDownloading(true);
|
||||
await props.onDownload();
|
||||
setDownloading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="wt-viewer__file"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
props.onFileClick();
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className="wt-viewer__file__icon"
|
||||
fa={classNames({
|
||||
'fas fa-file-image': isImage,
|
||||
'fas fa-file-video': isVideo,
|
||||
'fas fa-file-audio': isAudio,
|
||||
'fas fa-file': !isImage && !isVideo && !isAudio,
|
||||
})}
|
||||
// url={props.loading ? SpinnerGif : undefined}
|
||||
size={1.5}
|
||||
/>
|
||||
<div className="wt-viewer__file__name text-light mx-4">
|
||||
<div>{props.name}</div>
|
||||
<small>{`${(props.progress * 100).toFixed(0)}% - ${prettyBytes(props.length * props.progress)} / ${prettyBytes(props.length)}`}</small>
|
||||
</div>
|
||||
<Icon
|
||||
className="wt-viewer__file__download-btn"
|
||||
fa={downloading ? undefined : "fas fa-download"}
|
||||
url={downloading ? SpinnerGif : undefined}
|
||||
size={downloading ? 2 : 1.25}
|
||||
onClick={e => {
|
||||
if (downloading) return;
|
||||
e.stopPropagation();
|
||||
onDownload();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="wt-viewer__file"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
props.onFileClick();
|
||||
}}>
|
||||
<Icon
|
||||
className="wt-viewer__file__icon"
|
||||
fa={classNames({
|
||||
'fas fa-file-image': isImage,
|
||||
'fas fa-file-video': isVideo,
|
||||
'fas fa-file-audio': isAudio,
|
||||
'fas fa-file': !isImage && !isVideo && !isAudio,
|
||||
})}
|
||||
// url={props.loading ? SpinnerGif : undefined}
|
||||
size={1.5}
|
||||
/>
|
||||
<div className="wt-viewer__file__name text-light mx-4">
|
||||
<div>{props.name}</div>
|
||||
<small>{`${(props.progress * 100).toFixed(0)}% - ${prettyBytes(
|
||||
props.length * props.progress
|
||||
)} / ${prettyBytes(props.length)}`}</small>
|
||||
</div>
|
||||
<Icon
|
||||
className="wt-viewer__file__download-btn"
|
||||
fa={downloading ? undefined : 'fas fa-download'}
|
||||
url={downloading ? SpinnerGif : undefined}
|
||||
size={downloading ? 2 : 1.25}
|
||||
onClick={e => {
|
||||
if (downloading) return;
|
||||
e.stopPropagation();
|
||||
onDownload();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable.scss";
|
||||
@import '../../util/variable.scss';
|
||||
|
||||
.wt-viewer {
|
||||
&__files {
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
&__file {
|
||||
@extend %row-nowrap;
|
||||
padding: .5rem 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
&__viewer {
|
||||
@extend %row-nowrap;
|
||||
align-items: center;;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: black;
|
||||
|
||||
@@ -67,4 +67,4 @@
|
||||
margin: 1rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
src/custom.d.ts
vendored
20
src/custom.d.ts
vendored
@@ -1,16 +1,16 @@
|
||||
declare module "*.svg" {
|
||||
const content: string;
|
||||
export default content;
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module "*.png" {
|
||||
const content: string;
|
||||
export default content;
|
||||
declare module '*.png' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module "*.gif" {
|
||||
const content: string;
|
||||
export default content;
|
||||
declare module '*.gif' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare const self: ServiceWorkerGlobalScope;
|
||||
declare const self: ServiceWorkerGlobalScope;
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
import {useSelector} from "react-redux";
|
||||
import {AppRootState} from "../store/configureAppStore";
|
||||
import deepEqual from "fast-deep-equal";
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppRootState } from '../store/configureAppStore';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
const THEME_LS_KEY = 'theme';
|
||||
|
||||
enum ActionTypes {
|
||||
SET_THEME = 'app/setTheme',
|
||||
SET_THEME = 'app/setTheme',
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
type: ActionTypes;
|
||||
payload: payload;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
}
|
||||
type: ActionTypes;
|
||||
payload: payload;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
theme: string;
|
||||
theme: string;
|
||||
};
|
||||
|
||||
const getTheme = () => {
|
||||
let theme = localStorage.getItem(THEME_LS_KEY);
|
||||
if (theme === 'dark' || theme === 'light') return theme;
|
||||
if (window.matchMedia("(prefers-color-scheme: dark)")) return 'dark';
|
||||
return 'light';
|
||||
}
|
||||
let theme = localStorage.getItem(THEME_LS_KEY);
|
||||
if (theme === 'dark' || theme === 'light') return theme;
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)')) return 'dark';
|
||||
return 'light';
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
theme: getTheme(),
|
||||
theme: getTheme(),
|
||||
};
|
||||
|
||||
export const setTheme = (theme: 'dark' | 'light') => {
|
||||
localStorage.setItem(THEME_LS_KEY, theme);
|
||||
return {
|
||||
type: ActionTypes.SET_THEME,
|
||||
payload: theme,
|
||||
};
|
||||
}
|
||||
localStorage.setItem(THEME_LS_KEY, theme);
|
||||
return {
|
||||
type: ActionTypes.SET_THEME,
|
||||
payload: theme,
|
||||
};
|
||||
};
|
||||
|
||||
export default function app(state = initialState, action: Action<any>): State {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_THEME:
|
||||
return {
|
||||
...state,
|
||||
theme: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_THEME:
|
||||
return {
|
||||
...state,
|
||||
theme: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const useSetting = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return {
|
||||
theme: state.app.theme,
|
||||
};
|
||||
}, deepEqual);
|
||||
}
|
||||
return useSelector((state: AppRootState) => {
|
||||
return {
|
||||
theme: state.app.theme,
|
||||
};
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
@@ -1,285 +1,288 @@
|
||||
import {Chat, ChatMessage, ZKChatClient} from "../util/zkchat";
|
||||
import {Dispatch} from "redux";
|
||||
import store, {AppRootState} from "../store/configureAppStore";
|
||||
import config from "../util/config";
|
||||
import {useSelector} from "react-redux";
|
||||
import deepEqual from "fast-deep-equal";
|
||||
import sse from "../util/sse";
|
||||
import { Chat, ChatMessage, ZKChatClient } from '../util/zkchat';
|
||||
import { Dispatch } from 'redux';
|
||||
import store, { AppRootState } from '../store/configureAppStore';
|
||||
import config from '../util/config';
|
||||
import { useSelector } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import sse from '../util/sse';
|
||||
const EVENTS = ZKChatClient.EVENTS;
|
||||
|
||||
export const zkchat = new ZKChatClient({
|
||||
api: `${config.indexerAPI}/v1/zkchat`,
|
||||
api: `${config.indexerAPI}/v1/zkchat`,
|
||||
});
|
||||
|
||||
sse.on('NEW_CHAT_MESSAGE', async (payload: any) => {
|
||||
const message = await zkchat.inflateMessage(payload);
|
||||
zkchat.prependMessage(message);
|
||||
const message = await zkchat.inflateMessage(payload);
|
||||
zkchat.prependMessage(message);
|
||||
});
|
||||
|
||||
const onNewMessage = (message: ChatMessage) => {
|
||||
const {
|
||||
receiver,
|
||||
sender,
|
||||
} = message;
|
||||
const chatId = zkchat.deriveChatId({
|
||||
type: 'DIRECT',
|
||||
receiver: '',
|
||||
receiverECDH: receiver.ecdh!,
|
||||
senderECDH: sender.ecdh!,
|
||||
});
|
||||
store.dispatch(setMessage(message));
|
||||
store.dispatch(setMessagesForChat(chatId, zkchat.activeChats[chatId].messages));
|
||||
const { receiver, sender } = message;
|
||||
const chatId = zkchat.deriveChatId({
|
||||
type: 'DIRECT',
|
||||
receiver: '',
|
||||
receiverECDH: receiver.ecdh!,
|
||||
senderECDH: sender.ecdh!,
|
||||
});
|
||||
store.dispatch(setMessage(message));
|
||||
store.dispatch(setMessagesForChat(chatId, zkchat.activeChats[chatId].messages));
|
||||
};
|
||||
|
||||
zkchat.on(EVENTS.MESSAGE_APPENDED, onNewMessage);
|
||||
zkchat.on(EVENTS.MESSAGE_PREPENDED, onNewMessage);
|
||||
zkchat.on(EVENTS.CHAT_CREATED, (chat: Chat) => {
|
||||
store.dispatch(addChat(chat));
|
||||
store.dispatch(addChat(chat));
|
||||
});
|
||||
|
||||
enum ActionTypes {
|
||||
SET_CHATS = 'chats/setChats',
|
||||
SET_MESSAGES_FOR_CHAT = 'chats/setMessagesForChat',
|
||||
ADD_CHAT = 'chats/addChat',
|
||||
SET_CHAT_NICKNAME = 'chats/setChatNickname',
|
||||
SET_MESSAGE = 'chats/SET_MESSAGE',
|
||||
SET_CHATS = 'chats/setChats',
|
||||
SET_MESSAGES_FOR_CHAT = 'chats/setMessagesForChat',
|
||||
ADD_CHAT = 'chats/addChat',
|
||||
SET_CHAT_NICKNAME = 'chats/setChatNickname',
|
||||
SET_MESSAGE = 'chats/SET_MESSAGE',
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
type: ActionTypes;
|
||||
payload?: payload;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
}
|
||||
type: ActionTypes;
|
||||
payload?: payload;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
chats: {
|
||||
order: string[];
|
||||
map: {
|
||||
[chatId: string]: InflatedChat;
|
||||
};
|
||||
chats: {
|
||||
order: string[];
|
||||
map: {
|
||||
[chatId: string]: InflatedChat;
|
||||
};
|
||||
messages: {
|
||||
[messageId: string]: ChatMessage;
|
||||
};
|
||||
}
|
||||
};
|
||||
messages: {
|
||||
[messageId: string]: ChatMessage;
|
||||
};
|
||||
};
|
||||
|
||||
export type InflatedChat = Chat & {
|
||||
messages: string[];
|
||||
nickname?: string;
|
||||
}
|
||||
messages: string[];
|
||||
nickname?: string;
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
chats: {
|
||||
order: [],
|
||||
map: {},
|
||||
},
|
||||
messages: {},
|
||||
chats: {
|
||||
order: [],
|
||||
map: {},
|
||||
},
|
||||
messages: {},
|
||||
};
|
||||
|
||||
export const setChats = (chats: {
|
||||
[chatId: string]: Chat;
|
||||
[chatId: string]: Chat;
|
||||
}): Action<{
|
||||
[chatId: string]: Chat;
|
||||
[chatId: string]: Chat;
|
||||
}> => ({
|
||||
type: ActionTypes.SET_CHATS,
|
||||
payload: chats,
|
||||
type: ActionTypes.SET_CHATS,
|
||||
payload: chats,
|
||||
});
|
||||
|
||||
const addChat = (chat: Chat): Action<Chat> => ({
|
||||
type: ActionTypes.ADD_CHAT,
|
||||
payload: chat,
|
||||
type: ActionTypes.ADD_CHAT,
|
||||
payload: chat,
|
||||
});
|
||||
|
||||
const setNickname = (chat: Chat, nickname: string): Action<{
|
||||
chat: Chat,
|
||||
nickname: string,
|
||||
const setNickname = (
|
||||
chat: Chat,
|
||||
nickname: string
|
||||
): Action<{
|
||||
chat: Chat;
|
||||
nickname: string;
|
||||
}> => ({
|
||||
type: ActionTypes.SET_CHAT_NICKNAME,
|
||||
payload: {chat, nickname},
|
||||
type: ActionTypes.SET_CHAT_NICKNAME,
|
||||
payload: { chat, nickname },
|
||||
});
|
||||
|
||||
const setMessage = (msg: ChatMessage): Action<ChatMessage> => ({
|
||||
type: ActionTypes.SET_MESSAGE,
|
||||
payload: msg,
|
||||
type: ActionTypes.SET_MESSAGE,
|
||||
payload: msg,
|
||||
});
|
||||
|
||||
const setMessagesForChat = (chatId: string, messages: string[]): Action<{ chatId: string; messages: string[] }> => ({
|
||||
type: ActionTypes.SET_MESSAGES_FOR_CHAT,
|
||||
payload: { chatId, messages },
|
||||
const setMessagesForChat = (
|
||||
chatId: string,
|
||||
messages: string[]
|
||||
): Action<{ chatId: string; messages: string[] }> => ({
|
||||
type: ActionTypes.SET_MESSAGES_FOR_CHAT,
|
||||
payload: { chatId, messages },
|
||||
});
|
||||
|
||||
export const fetchChats = (address: string) => async (dispatch: Dispatch, getState: () => AppRootState) => {
|
||||
export const fetchChats =
|
||||
(address: string) => async (dispatch: Dispatch, getState: () => AppRootState) => {
|
||||
await zkchat.fetchActiveChats(address);
|
||||
dispatch(setChats(zkchat.activeChats));
|
||||
}
|
||||
};
|
||||
|
||||
export default function chats(state = initialState, action: Action<any>): State {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_CHATS:
|
||||
return handleSetChats(state, action);
|
||||
case ActionTypes.ADD_CHAT:
|
||||
return handeAddChat(state, action);
|
||||
case ActionTypes.SET_MESSAGES_FOR_CHAT:
|
||||
return handleSetMessagesForChats(state, action);
|
||||
case ActionTypes.SET_CHAT_NICKNAME:
|
||||
return handeSetNickname(state, action);
|
||||
case ActionTypes.SET_MESSAGE:
|
||||
return {
|
||||
...state,
|
||||
messages: {
|
||||
...state.messages,
|
||||
[action.payload.messageId]: action.payload,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_CHATS:
|
||||
return handleSetChats(state, action);
|
||||
case ActionTypes.ADD_CHAT:
|
||||
return handeAddChat(state, action);
|
||||
case ActionTypes.SET_MESSAGES_FOR_CHAT:
|
||||
return handleSetMessagesForChats(state, action);
|
||||
case ActionTypes.SET_CHAT_NICKNAME:
|
||||
return handeSetNickname(state, action);
|
||||
case ActionTypes.SET_MESSAGE:
|
||||
return {
|
||||
...state,
|
||||
messages: {
|
||||
...state.messages,
|
||||
[action.payload.messageId]: action.payload,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function handeSetNickname(state: State, action: Action<{ chat: Chat, nickname: string }>) {
|
||||
const chatId = zkchat.deriveChatId(action.payload!.chat);
|
||||
const chats = state.chats;
|
||||
const { map } = chats;
|
||||
const chat = map[chatId];
|
||||
function handeSetNickname(state: State, action: Action<{ chat: Chat; nickname: string }>) {
|
||||
const chatId = zkchat.deriveChatId(action.payload!.chat);
|
||||
const chats = state.chats;
|
||||
const { map } = chats;
|
||||
const chat = map[chatId];
|
||||
|
||||
if (!chat || chat.nickname === action.payload!.nickname) {
|
||||
return state;
|
||||
}
|
||||
if (!chat || chat.nickname === action.payload!.nickname) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
chats: {
|
||||
...chats,
|
||||
map: {
|
||||
...map,
|
||||
[chatId]: {
|
||||
...chat,
|
||||
nickname: action.payload!.nickname,
|
||||
},
|
||||
},
|
||||
return {
|
||||
...state,
|
||||
chats: {
|
||||
...chats,
|
||||
map: {
|
||||
...map,
|
||||
[chatId]: {
|
||||
...chat,
|
||||
nickname: action.payload!.nickname,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function handeAddChat(state: State, action: Action<Chat>) {
|
||||
const chatId = zkchat.deriveChatId(action.payload!);
|
||||
const chats = state.chats;
|
||||
const chatId = zkchat.deriveChatId(action.payload!);
|
||||
const chats = state.chats;
|
||||
|
||||
const { order, map } = chats;
|
||||
const { order, map } = chats;
|
||||
|
||||
if (map[chatId]) {
|
||||
return state;
|
||||
}
|
||||
if (map[chatId]) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const newOrder = [...order, chatId];
|
||||
const newMap = {
|
||||
...map,
|
||||
[chatId]: {
|
||||
...action.payload!,
|
||||
messages: [],
|
||||
},
|
||||
};
|
||||
const newOrder = [...order, chatId];
|
||||
const newMap = {
|
||||
...map,
|
||||
[chatId]: {
|
||||
...action.payload!,
|
||||
messages: [],
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
chats: {
|
||||
order: newOrder,
|
||||
map: newMap,
|
||||
},
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
chats: {
|
||||
order: newOrder,
|
||||
map: newMap,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function handleSetChats(state: State, action: Action<{
|
||||
function handleSetChats(
|
||||
state: State,
|
||||
action: Action<{
|
||||
[chatId: string]: Chat & {
|
||||
messages: string[];
|
||||
};
|
||||
}>) {
|
||||
const chats = action.payload!;
|
||||
const order = Object.keys(chats);
|
||||
const map = {
|
||||
...chats,
|
||||
messages: string[];
|
||||
};
|
||||
}>
|
||||
) {
|
||||
const chats = action.payload!;
|
||||
const order = Object.keys(chats);
|
||||
const map = {
|
||||
...chats,
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
chats: {
|
||||
order,
|
||||
map,
|
||||
},
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
chats: {
|
||||
order,
|
||||
map,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function handleSetMessagesForChats(state: State, action: Action<{
|
||||
function handleSetMessagesForChats(
|
||||
state: State,
|
||||
action: Action<{
|
||||
chatId: string;
|
||||
messages: string[]
|
||||
}>) {
|
||||
const { chatId, messages } = action.payload!;
|
||||
const chat = state.chats.map[chatId];
|
||||
messages: string[];
|
||||
}>
|
||||
) {
|
||||
const { chatId, messages } = action.payload!;
|
||||
const chat = state.chats.map[chatId];
|
||||
|
||||
if (!chat) return state;
|
||||
if (!chat) return state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
chats: {
|
||||
...state.chats,
|
||||
map: {
|
||||
...state.chats.map,
|
||||
[chatId]: {
|
||||
...chat,
|
||||
messages: messages,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
chats: {
|
||||
...state.chats,
|
||||
map: {
|
||||
...state.chats.map,
|
||||
[chatId]: {
|
||||
...chat,
|
||||
messages: messages,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const useChatIds = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.chats.chats.order;
|
||||
}, deepEqual);
|
||||
}
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.chats.chats.order;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useChatId = (chatId: string) => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
const chat = state.chats.chats.map[chatId];
|
||||
return useSelector((state: AppRootState) => {
|
||||
const chat = state.chats.chats.map[chatId];
|
||||
|
||||
if (!chat) return;
|
||||
if (!chat) return;
|
||||
|
||||
const {
|
||||
messages,
|
||||
nickname,
|
||||
...rest
|
||||
} = chat;
|
||||
const { messages, nickname, ...rest } = chat;
|
||||
|
||||
return rest;
|
||||
}, deepEqual);
|
||||
}
|
||||
return rest;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useMessagesByChatId = (chatId: string) => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.chats.chats.map[chatId]?.messages || [];
|
||||
}, deepEqual);
|
||||
}
|
||||
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.chats.chats.map[chatId]?.messages || [];
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useLastNMessages = (chatId: string, n = 1): ChatMessage[] => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
const {
|
||||
chats: {
|
||||
chats: {
|
||||
map,
|
||||
},
|
||||
messages,
|
||||
}
|
||||
} = state;
|
||||
const ids = state.chats.chats.map[chatId]?.messages || [];
|
||||
return ids.slice(0, n).map(messageId => messages[messageId]);
|
||||
}, deepEqual);
|
||||
}
|
||||
return useSelector((state: AppRootState) => {
|
||||
const {
|
||||
chats: {
|
||||
chats: { map },
|
||||
messages,
|
||||
},
|
||||
} = state;
|
||||
const ids = state.chats.chats.map[chatId]?.messages || [];
|
||||
return ids.slice(0, n).map(messageId => messages[messageId]);
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useChatMessage = (messageId: string) => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.chats.messages[messageId];
|
||||
}, deepEqual);
|
||||
}
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.chats.messages[messageId];
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
@@ -1,188 +1,209 @@
|
||||
import {ducks, fetchReset, fetchStub, store} from "../util/testUtils";
|
||||
import {createEditorStateWithText} from "@draft-js-plugins/editor";
|
||||
import {ZkIdentity} from "@zk-kit/identity";
|
||||
import { ducks, fetchReset, fetchStub, store } from '../util/testUtils';
|
||||
import { createEditorStateWithText } from '@draft-js-plugins/editor';
|
||||
import { ZkIdentity } from '@zk-kit/identity';
|
||||
|
||||
const {
|
||||
setMirror,
|
||||
submitConnection,
|
||||
submitModeration,
|
||||
submitPost,
|
||||
submitProfile,
|
||||
submitRepost,
|
||||
setDraft,
|
||||
setMirror,
|
||||
submitConnection,
|
||||
submitModeration,
|
||||
submitPost,
|
||||
submitProfile,
|
||||
submitRepost,
|
||||
setDraft,
|
||||
} = ducks.drafts;
|
||||
|
||||
const {
|
||||
setSelectedId,
|
||||
} = ducks.worker;
|
||||
const { setSelectedId } = ducks.worker;
|
||||
|
||||
describe('Drafts Duck', () => {
|
||||
it('should initialize state', () => {
|
||||
expect(store.getState().drafts).toStrictEqual({ map: {}, mirror: false, submitting: false });
|
||||
});
|
||||
it('should initialize state', () => {
|
||||
expect(store.getState().drafts).toStrictEqual({ map: {}, mirror: false, submitting: false });
|
||||
});
|
||||
|
||||
it('should submit post', async () => {
|
||||
store.dispatch(setDraft({
|
||||
reference: '',
|
||||
editorState: createEditorStateWithText('hello world!'),
|
||||
}));
|
||||
store.dispatch(setMirror(true));
|
||||
// @ts-ignore
|
||||
const post: any = await store.dispatch(submitPost(''));
|
||||
expect(post.payload).toStrictEqual({
|
||||
"attachment": "",
|
||||
"content": "hello world!",
|
||||
"reference": "",
|
||||
"title": "",
|
||||
"topic": "ok",
|
||||
});
|
||||
expect(fetchStub.args[0]).toStrictEqual(["http://127.0.0.1:3000/twitter/update", {"body": "{\"status\":\"hello world!\"}", "credentials": "include", "headers": {"Content-Type": "application/json"}, "method": "POST"}]);
|
||||
fetchReset();
|
||||
it('should submit post', async () => {
|
||||
store.dispatch(
|
||||
setDraft({
|
||||
reference: '',
|
||||
editorState: createEditorStateWithText('hello world!'),
|
||||
})
|
||||
);
|
||||
store.dispatch(setMirror(true));
|
||||
// @ts-ignore
|
||||
const post: any = await store.dispatch(submitPost(''));
|
||||
expect(post.payload).toStrictEqual({
|
||||
attachment: '',
|
||||
content: 'hello world!',
|
||||
reference: '',
|
||||
title: '',
|
||||
topic: 'ok',
|
||||
});
|
||||
expect(fetchStub.args[0]).toStrictEqual([
|
||||
'http://127.0.0.1:3000/twitter/update',
|
||||
{
|
||||
body: '{"status":"hello world!"}',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
},
|
||||
]);
|
||||
fetchReset();
|
||||
});
|
||||
|
||||
it('should submit semaphore post', async () => {
|
||||
const zkIdentity = new ZkIdentity();
|
||||
const identityCommitment = '0x' + zkIdentity.genIdentityCommitment().toString(16);
|
||||
store.dispatch(setSelectedId({
|
||||
type: 'interrep',
|
||||
address: '0xmyaddress',
|
||||
nonce: 0,
|
||||
provider: 'autism',
|
||||
name: 'diamond',
|
||||
identityPath: {
|
||||
path_elements: ['0x0', '0x1'],
|
||||
path_index: [0, 1],
|
||||
root: '0xroothash',
|
||||
it('should submit semaphore post', async () => {
|
||||
const zkIdentity = new ZkIdentity();
|
||||
const identityCommitment = '0x' + zkIdentity.genIdentityCommitment().toString(16);
|
||||
store.dispatch(
|
||||
setSelectedId({
|
||||
type: 'interrep',
|
||||
address: '0xmyaddress',
|
||||
nonce: 0,
|
||||
provider: 'autism',
|
||||
name: 'diamond',
|
||||
identityPath: {
|
||||
path_elements: ['0x0', '0x1'],
|
||||
path_index: [0, 1],
|
||||
root: '0xroothash',
|
||||
},
|
||||
identityCommitment,
|
||||
serializedIdentity: zkIdentity.serializeIdentity(),
|
||||
})
|
||||
);
|
||||
store.dispatch(
|
||||
setDraft({
|
||||
reference: '0xparenthash',
|
||||
editorState: createEditorStateWithText('reply world!'),
|
||||
})
|
||||
);
|
||||
store.dispatch(setMirror(false));
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
payload: {
|
||||
data: {
|
||||
siblings: ['0x00', '0x01'],
|
||||
pathIndices: [0, 1],
|
||||
root: '0x00',
|
||||
},
|
||||
identityCommitment,
|
||||
serializedIdentity: zkIdentity.serializeIdentity(),
|
||||
}))
|
||||
store.dispatch(setDraft({
|
||||
reference: '0xparenthash',
|
||||
editorState: createEditorStateWithText('reply world!'),
|
||||
}));
|
||||
store.dispatch(setMirror(false));
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
payload: {
|
||||
data: {
|
||||
siblings: ['0x00', '0x01'],
|
||||
pathIndices: [0, 1],
|
||||
root: '0x00',
|
||||
},
|
||||
name: '',
|
||||
provider: '',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
// @ts-ignore
|
||||
const post: any = await store.dispatch(submitPost('0xparenthash'));
|
||||
expect(post.payload).toStrictEqual({
|
||||
"attachment": "",
|
||||
"content": "reply world!",
|
||||
"reference": "0xparenthash",
|
||||
"title": "",
|
||||
"topic": "",
|
||||
});
|
||||
expect(fetchStub.args[0][0]).toBe(`http://127.0.0.1:3000/interrep/${identityCommitment}`);
|
||||
fetchReset();
|
||||
name: '',
|
||||
provider: '',
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
const post: any = await store.dispatch(submitPost('0xparenthash'));
|
||||
expect(post.payload).toStrictEqual({
|
||||
attachment: '',
|
||||
content: 'reply world!',
|
||||
reference: '0xparenthash',
|
||||
title: '',
|
||||
topic: '',
|
||||
});
|
||||
expect(fetchStub.args[0][0]).toBe(`http://127.0.0.1:3000/interrep/${identityCommitment}`);
|
||||
fetchReset();
|
||||
});
|
||||
|
||||
it('should submit zkpr semaphore post', async () => {
|
||||
const zkIdentity = new ZkIdentity();
|
||||
const identityCommitment = '0x' + zkIdentity.genIdentityCommitment().toString(16);
|
||||
store.dispatch(setSelectedId({
|
||||
type: 'zkpr_interrep',
|
||||
provider: 'autism',
|
||||
name: 'diamond',
|
||||
identityPath: {
|
||||
path_elements: ['0x0', '0x1'],
|
||||
path_index: [0, 1],
|
||||
root: '0xroothash',
|
||||
it('should submit zkpr semaphore post', async () => {
|
||||
const zkIdentity = new ZkIdentity();
|
||||
const identityCommitment = '0x' + zkIdentity.genIdentityCommitment().toString(16);
|
||||
store.dispatch(
|
||||
setSelectedId({
|
||||
type: 'zkpr_interrep',
|
||||
provider: 'autism',
|
||||
name: 'diamond',
|
||||
identityPath: {
|
||||
path_elements: ['0x0', '0x1'],
|
||||
path_index: [0, 1],
|
||||
root: '0xroothash',
|
||||
},
|
||||
identityCommitment,
|
||||
})
|
||||
);
|
||||
store.dispatch(
|
||||
setDraft({
|
||||
reference: '0xparenthash1',
|
||||
editorState: createEditorStateWithText('reply world!!'),
|
||||
})
|
||||
);
|
||||
store.dispatch(setMirror(false));
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
payload: {
|
||||
data: {
|
||||
siblings: ['0x00', '0x01'],
|
||||
pathIndices: [0, 1],
|
||||
root: '0x00',
|
||||
},
|
||||
identityCommitment,
|
||||
}))
|
||||
store.dispatch(setDraft({
|
||||
reference: '0xparenthash1',
|
||||
editorState: createEditorStateWithText('reply world!!'),
|
||||
}));
|
||||
store.dispatch(setMirror(false));
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
payload: {
|
||||
data: {
|
||||
siblings: ['0x00', '0x01'],
|
||||
pathIndices: [0, 1],
|
||||
root: '0x00',
|
||||
},
|
||||
name: '',
|
||||
provider: '',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
// @ts-ignore
|
||||
const post: any = await store.dispatch(submitPost('0xparenthash1'));
|
||||
expect(post.payload).toStrictEqual({
|
||||
"attachment": "",
|
||||
"content": "reply world!!",
|
||||
"reference": "0xparenthash1",
|
||||
"title": "",
|
||||
"topic": "",
|
||||
});
|
||||
expect(fetchStub.args[0][0]).toBe(`http://127.0.0.1:3000/interrep/${identityCommitment}`);
|
||||
fetchReset();
|
||||
store.dispatch(setSelectedId({
|
||||
type: 'gun',
|
||||
address: '0xmyaddress',
|
||||
nonce: 0,
|
||||
publicKey: '0xpubkey',
|
||||
privateKey: '0xprivkey',
|
||||
}));
|
||||
name: '',
|
||||
provider: '',
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
const post: any = await store.dispatch(submitPost('0xparenthash1'));
|
||||
expect(post.payload).toStrictEqual({
|
||||
attachment: '',
|
||||
content: 'reply world!!',
|
||||
reference: '0xparenthash1',
|
||||
title: '',
|
||||
topic: '',
|
||||
});
|
||||
expect(fetchStub.args[0][0]).toBe(`http://127.0.0.1:3000/interrep/${identityCommitment}`);
|
||||
fetchReset();
|
||||
store.dispatch(
|
||||
setSelectedId({
|
||||
type: 'gun',
|
||||
address: '0xmyaddress',
|
||||
nonce: 0,
|
||||
publicKey: '0xpubkey',
|
||||
privateKey: '0xprivkey',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should submit repost', async () => {
|
||||
// @ts-ignore
|
||||
const post: any = await store.dispatch(submitRepost('0xreposthash'));
|
||||
expect(post.subtype).toBe('REPOST');
|
||||
expect(post.payload).toStrictEqual({
|
||||
"attachment": "",
|
||||
"content": "",
|
||||
"reference": "0xreposthash",
|
||||
"title": "",
|
||||
"topic": "",
|
||||
});
|
||||
it('should submit repost', async () => {
|
||||
// @ts-ignore
|
||||
const post: any = await store.dispatch(submitRepost('0xreposthash'));
|
||||
expect(post.subtype).toBe('REPOST');
|
||||
expect(post.payload).toStrictEqual({
|
||||
attachment: '',
|
||||
content: '',
|
||||
reference: '0xreposthash',
|
||||
title: '',
|
||||
topic: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit moderation', async () => {
|
||||
// @ts-ignore
|
||||
const mod: any = await store.dispatch(submitModeration('0xmodhash', 'LIKE'));
|
||||
expect(mod.subtype).toBe('LIKE');
|
||||
expect(mod.payload).toStrictEqual({
|
||||
"reference": "0xmodhash",
|
||||
});
|
||||
it('should submit moderation', async () => {
|
||||
// @ts-ignore
|
||||
const mod: any = await store.dispatch(submitModeration('0xmodhash', 'LIKE'));
|
||||
expect(mod.subtype).toBe('LIKE');
|
||||
expect(mod.payload).toStrictEqual({
|
||||
reference: '0xmodhash',
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit connection', async () => {
|
||||
// @ts-ignore
|
||||
const conn: any = await store.dispatch(submitConnection('0xotheruser', 'BLOCK'));
|
||||
expect(conn.subtype).toBe('BLOCK');
|
||||
expect(conn.payload).toStrictEqual({
|
||||
"name": "0xotheruser",
|
||||
});
|
||||
it('should submit connection', async () => {
|
||||
// @ts-ignore
|
||||
const conn: any = await store.dispatch(submitConnection('0xotheruser', 'BLOCK'));
|
||||
expect(conn.subtype).toBe('BLOCK');
|
||||
expect(conn.payload).toStrictEqual({
|
||||
name: '0xotheruser',
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit profile', async () => {
|
||||
// @ts-ignore
|
||||
const pf: any = await store.dispatch(submitProfile('PROFILE_IMAGE', 'image'));
|
||||
expect(pf.subtype).toBe('PROFILE_IMAGE');
|
||||
expect(pf.payload).toStrictEqual({
|
||||
key: "",
|
||||
value: "image",
|
||||
});
|
||||
it('should submit profile', async () => {
|
||||
// @ts-ignore
|
||||
const pf: any = await store.dispatch(submitProfile('PROFILE_IMAGE', 'image'));
|
||||
expect(pf.subtype).toBe('PROFILE_IMAGE');
|
||||
expect(pf.payload).toStrictEqual({
|
||||
key: '',
|
||||
value: 'image',
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
1291
src/ducks/drafts.ts
1291
src/ducks/drafts.ts
File diff suppressed because it is too large
Load Diff
@@ -1,64 +1,67 @@
|
||||
import {useSelector} from "react-redux";
|
||||
import {AppRootState} from "../store/configureAppStore";
|
||||
import deepEqual from "fast-deep-equal";
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppRootState } from '../store/configureAppStore';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
enum ActionTypes {
|
||||
UNMODERATE = 'mods/unmoderate',
|
||||
UNMODERATE = 'mods/unmoderate',
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
type: ActionTypes;
|
||||
payload?: payload;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
type ModSetting = {
|
||||
unmoderated: boolean;
|
||||
}
|
||||
|
||||
type State = {
|
||||
posts: {
|
||||
[messageId: string]: ModSetting;
|
||||
};
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
posts: {},
|
||||
type: ActionTypes;
|
||||
payload?: payload;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
export const unmoderate = (messageId: string, unmoderated: boolean): Action<{
|
||||
messageId: string;
|
||||
unmoderated: boolean;
|
||||
type ModSetting = {
|
||||
unmoderated: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
posts: {
|
||||
[messageId: string]: ModSetting;
|
||||
};
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
posts: {},
|
||||
};
|
||||
|
||||
export const unmoderate = (
|
||||
messageId: string,
|
||||
unmoderated: boolean
|
||||
): Action<{
|
||||
messageId: string;
|
||||
unmoderated: boolean;
|
||||
}> => ({
|
||||
type: ActionTypes.UNMODERATE,
|
||||
payload: {
|
||||
messageId,
|
||||
unmoderated,
|
||||
},
|
||||
type: ActionTypes.UNMODERATE,
|
||||
payload: {
|
||||
messageId,
|
||||
unmoderated,
|
||||
},
|
||||
});
|
||||
|
||||
export default function mods(state = initialState, action: Action<any>): State {
|
||||
switch (action.type) {
|
||||
case ActionTypes.UNMODERATE:
|
||||
return {
|
||||
...state,
|
||||
posts: {
|
||||
...state.posts,
|
||||
[action.payload.messageId]: {
|
||||
...state.posts[action.payload.messageId],
|
||||
unmoderated: action.payload.unmoderated,
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
switch (action.type) {
|
||||
case ActionTypes.UNMODERATE:
|
||||
return {
|
||||
...state,
|
||||
posts: {
|
||||
...state.posts,
|
||||
[action.payload.messageId]: {
|
||||
...state.posts[action.payload.messageId],
|
||||
unmoderated: action.payload.unmoderated,
|
||||
},
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const usePostModeration = (messageId?: string | null): ModSetting | null => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
if (!messageId) return null;
|
||||
return state.mods.posts[messageId] || null;
|
||||
}, deepEqual);
|
||||
}
|
||||
return useSelector((state: AppRootState) => {
|
||||
if (!messageId) return null;
|
||||
return state.mods.posts[messageId] || null;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
@@ -1,251 +1,360 @@
|
||||
import {ducks, fetchStub, gunStub, store} from '../util/testUtils';
|
||||
import {MessageType, Post, PostMessageSubType} from "../util/message";
|
||||
import { ducks, fetchStub, gunStub, store } from '../util/testUtils';
|
||||
import { MessageType, Post, PostMessageSubType } from '../util/message';
|
||||
|
||||
const {
|
||||
posts: {
|
||||
fetchPosts,
|
||||
fetchMeta,
|
||||
fetchPost,
|
||||
fetchReplies,
|
||||
fetchTagFeed,
|
||||
fetchHomeFeed,
|
||||
fetchLikedBy,
|
||||
fetchRepliedBy,
|
||||
setPost,
|
||||
setLiked,
|
||||
setReposted,
|
||||
setBlockedPost,
|
||||
incrementRepost,
|
||||
incrementReply,
|
||||
incrementLike,
|
||||
decrementRepost,
|
||||
decrementLike,
|
||||
unsetPost,
|
||||
}
|
||||
posts: {
|
||||
fetchPosts,
|
||||
fetchMeta,
|
||||
fetchPost,
|
||||
fetchReplies,
|
||||
fetchTagFeed,
|
||||
fetchHomeFeed,
|
||||
fetchLikedBy,
|
||||
fetchRepliedBy,
|
||||
setPost,
|
||||
setLiked,
|
||||
setReposted,
|
||||
setBlockedPost,
|
||||
incrementRepost,
|
||||
incrementReply,
|
||||
incrementLike,
|
||||
decrementRepost,
|
||||
decrementLike,
|
||||
unsetPost,
|
||||
},
|
||||
} = ducks;
|
||||
|
||||
describe('Posts Duck', () => {
|
||||
it('should return initial state', async () => {
|
||||
expect(store.getState().posts).toStrictEqual({ map: {}, meta: {}});
|
||||
it('should return initial state', async () => {
|
||||
expect(store.getState().posts).toStrictEqual({ map: {}, meta: {} });
|
||||
});
|
||||
|
||||
it('should fetch meta', async () => {
|
||||
const messageId = '0xmeta/0000000000000000000000000000000000000000000000000000000000000000';
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: {
|
||||
messageId: messageId,
|
||||
meta: {
|
||||
likeCounts: 11,
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchMeta(messageId));
|
||||
expect(fetchStub.args[0][0]).toBe(
|
||||
'http://127.0.0.1:3000/v1/post/0000000000000000000000000000000000000000000000000000000000000000'
|
||||
);
|
||||
expect(store.getState().posts.meta[messageId]).toStrictEqual({ likeCounts: 11 });
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch post', async () => {
|
||||
const messageId = 'e6789d8ea65b57efd365ada389924246e4bd7be9c109a7fe294646831f67db8b';
|
||||
gunStub.get
|
||||
.withArgs('message/e6789d8ea65b57efd365ada389924246e4bd7be9c109a7fe294646831f67db8b')
|
||||
// @ts-ignore
|
||||
.returns(
|
||||
Promise.resolve({
|
||||
type: 'POST',
|
||||
subtype: 'REPLY',
|
||||
createdAt: 1,
|
||||
payload: {
|
||||
'#': 'payload',
|
||||
},
|
||||
})
|
||||
)
|
||||
// @ts-ignore
|
||||
.withArgs('payload')
|
||||
.returns(Promise.resolve({ content: 'fetch post' }));
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: {
|
||||
name: '',
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchPost(messageId));
|
||||
expect(store.getState().posts.map[messageId].hash()).toBe(messageId);
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch posts', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{
|
||||
messageId: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.Default,
|
||||
payload: { content: 'test 1' },
|
||||
createdAt: 1,
|
||||
meta: { likeCount: 12 },
|
||||
},
|
||||
{
|
||||
messageId: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.Default,
|
||||
payload: { content: 'test 2' },
|
||||
createdAt: 1,
|
||||
meta: { likeCount: 13 },
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchPosts());
|
||||
const map = store.getState().posts.map;
|
||||
expect(fetchStub.args[0][0]).toBe('http://127.0.0.1:3000/v1/posts?limit=10&offset=0');
|
||||
expect(
|
||||
map['0657166868848f6b37debc5833112be36266dbfc8ec4f45c235372306fdfe965'].payload.content
|
||||
).toBe('test 1');
|
||||
expect(
|
||||
map['5775c1181556da4a0f1a1f378006ec71a131f3cd5f759bc852df97748b1d9935'].payload.content
|
||||
).toBe('test 2');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch liked by', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{
|
||||
messageId: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.Reply,
|
||||
payload: { content: 'like 1' },
|
||||
createdAt: 1,
|
||||
meta: { likeCount: 17 },
|
||||
},
|
||||
{
|
||||
messageId: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.MirrorPost,
|
||||
payload: { content: 'like 2' },
|
||||
createdAt: 1,
|
||||
meta: { likeCount: 17 },
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchLikedBy('0xuser'));
|
||||
const map = store.getState().posts.map;
|
||||
|
||||
expect(fetchStub.args[0][0]).toBe('http://127.0.0.1:3000/v1/0xuser/likes?limit=10&offset=0');
|
||||
expect(
|
||||
map['67b5f4334815476783316977c9608633219705a3eceb00c39a88ce173705f67f'].payload.content
|
||||
).toBe('like 1');
|
||||
expect(
|
||||
map['20d0bcb1b075de4e49d0adb7efd10becbf547ce2c46a2ddf01d48c83964fdfc5'].payload.content
|
||||
).toBe('like 2');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch replied by', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{
|
||||
messageId: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.Reply,
|
||||
payload: { content: 'reply 1' },
|
||||
createdAt: 1,
|
||||
meta: { likeCount: 17 },
|
||||
},
|
||||
{
|
||||
messageId: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.MirrorReply,
|
||||
payload: { content: 'reply 2' },
|
||||
createdAt: 1,
|
||||
meta: { likeCount: 17 },
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchRepliedBy('0xuser'));
|
||||
const map = store.getState().posts.map;
|
||||
|
||||
expect(fetchStub.args[0][0]).toBe('http://127.0.0.1:3000/v1/0xuser/replies?limit=10&offset=0');
|
||||
expect(
|
||||
map['86e976c49fcdb572e95bdb58ecb4c38b6a4e6e6d132fda8cb910c8a627a8de87'].payload.content
|
||||
).toBe('reply 1');
|
||||
expect(
|
||||
map['6c190cc667449d4867391c2bf6c4c17839143b036e92ed04dd07379d01b9bf7e'].payload.content
|
||||
).toBe('reply 2');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch homefed', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{
|
||||
messageId: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.Default,
|
||||
payload: { content: 'homefeed 1' },
|
||||
createdAt: 1,
|
||||
meta: { likeCount: 17 },
|
||||
},
|
||||
{
|
||||
messageId: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.Default,
|
||||
payload: { content: 'homefeed 2' },
|
||||
createdAt: 1,
|
||||
meta: { likeCount: 17 },
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchHomeFeed());
|
||||
const map = store.getState().posts.map;
|
||||
|
||||
expect(fetchStub.args[0][0]).toBe('http://127.0.0.1:3000/v1/homefeed?limit=10&offset=0');
|
||||
expect(
|
||||
map['04475753c11b8a9a1e9b8fd63d822e416c963b611ef7e09acf74583d4ca32f79'].payload.content
|
||||
).toBe('homefeed 1');
|
||||
expect(
|
||||
map['b5ccff8a5d242605599e182a29c08932d99bda45b8d0b149c8ddee19bb2ba6bf'].payload.content
|
||||
).toBe('homefeed 2');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch tagfeed', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{
|
||||
messageId: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.Default,
|
||||
payload: { content: '#tagfeed 1' },
|
||||
createdAt: 1,
|
||||
meta: { likeCount: 17 },
|
||||
},
|
||||
{
|
||||
messageId: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.Default,
|
||||
payload: { content: '#tagfeed 2' },
|
||||
createdAt: 1,
|
||||
meta: { likeCount: 17 },
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchTagFeed('#tagfeed'));
|
||||
const map = store.getState().posts.map;
|
||||
|
||||
expect(fetchStub.args[0][0]).toBe('http://127.0.0.1:3000/v1/tags/%23tagfeed?limit=10&offset=0');
|
||||
expect(
|
||||
map['0a8a52534ffaf357dd35061f127fa3c2628e29f4e0a7939e0c503a467130128a'].payload.content
|
||||
).toBe('#tagfeed 1');
|
||||
expect(
|
||||
map['b30fa8424d3db0889ddb39d968f5a8b7df4b4b847b0ed1e5df1f74f834cba463'].payload.content
|
||||
).toBe('#tagfeed 2');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch replies', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{
|
||||
messageId: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.Default,
|
||||
payload: { content: 'replies 3' },
|
||||
createdAt: 1,
|
||||
meta: { likeCount: 17 },
|
||||
},
|
||||
{
|
||||
messageId: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
type: MessageType.Post,
|
||||
subtype: PostMessageSubType.Default,
|
||||
payload: { content: 'replies 4' },
|
||||
createdAt: 1,
|
||||
meta: { likeCount: 17 },
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchReplies('0xparent'));
|
||||
const map = store.getState().posts.map;
|
||||
|
||||
expect(fetchStub.args[0][0]).toBe(
|
||||
'http://127.0.0.1:3000/v1/replies?limit=10&offset=0&parent=0xparent'
|
||||
);
|
||||
expect(
|
||||
map['4a524cdf0239a6fc465ada54fa1d0b568c186ca03054607b7463f16c14c56f2d'].payload.content
|
||||
).toBe('replies 3');
|
||||
expect(
|
||||
map['997c84ed80660c89403b530b2e3fc8a8af061de6609415375ccbc4adb34f12e3'].payload.content
|
||||
).toBe('replies 4');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should handle meta', async () => {
|
||||
await store.dispatch(incrementLike('0xtestmeta'));
|
||||
await store.dispatch(incrementReply('0xtestmeta'));
|
||||
await store.dispatch(incrementRepost('0xtestmeta'));
|
||||
await store.dispatch(setLiked('0xtestmeta', '0xlikedtestmeta'));
|
||||
await store.dispatch(setReposted('0xtestmeta', '0xrepostedtestmeta'));
|
||||
await store.dispatch(setBlockedPost('0xtestmeta', '0xblockedtestmeta'));
|
||||
expect(store.getState().posts.meta['0xtestmeta']).toStrictEqual({
|
||||
likeCount: 1,
|
||||
replyCount: 1,
|
||||
repostCount: 1,
|
||||
liked: '0xlikedtestmeta',
|
||||
reposted: '0xrepostedtestmeta',
|
||||
blocked: '0xblockedtestmeta',
|
||||
});
|
||||
|
||||
it('should fetch meta', async () => {
|
||||
const messageId = '0xmeta/0000000000000000000000000000000000000000000000000000000000000000';
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: {
|
||||
messageId: messageId,
|
||||
meta: {
|
||||
likeCounts: 11,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}))
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchMeta(messageId));
|
||||
expect(fetchStub.args[0][0])
|
||||
.toBe('http://127.0.0.1:3000/v1/post/0000000000000000000000000000000000000000000000000000000000000000');
|
||||
expect(store.getState().posts.meta[messageId])
|
||||
.toStrictEqual({likeCounts: 11});
|
||||
fetchStub.reset();
|
||||
await store.dispatch(decrementLike('0xtestmeta'));
|
||||
await store.dispatch(decrementRepost('0xtestmeta'));
|
||||
await store.dispatch(setLiked('0xtestmeta', null));
|
||||
await store.dispatch(setReposted('0xtestmeta', null));
|
||||
await store.dispatch(setBlockedPost('0xtestmeta', null));
|
||||
expect(store.getState().posts.meta['0xtestmeta']).toStrictEqual({
|
||||
likeCount: 0,
|
||||
replyCount: 1,
|
||||
repostCount: 0,
|
||||
liked: null,
|
||||
reposted: null,
|
||||
blocked: null,
|
||||
});
|
||||
|
||||
it('should fetch post', async () => {
|
||||
const messageId = 'e6789d8ea65b57efd365ada389924246e4bd7be9c109a7fe294646831f67db8b';
|
||||
gunStub.get.withArgs('message/e6789d8ea65b57efd365ada389924246e4bd7be9c109a7fe294646831f67db8b')
|
||||
// @ts-ignore
|
||||
.returns(Promise.resolve({
|
||||
type: 'POST',
|
||||
'subtype': 'REPLY',
|
||||
createdAt: 1,
|
||||
payload: {
|
||||
'#': 'payload',
|
||||
},
|
||||
}))
|
||||
// @ts-ignore
|
||||
.withArgs('payload').returns(Promise.resolve({ content: 'fetch post' }));
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: {
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
}));
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchPost(messageId));
|
||||
expect(store.getState().posts.map[messageId].hash())
|
||||
.toBe(messageId);
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch posts', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{ messageId: '0000000000000000000000000000000000000000000000000000000000000000', type: MessageType.Post, subtype: PostMessageSubType.Default, payload: { content: 'test 1' }, createdAt: 1, meta: { likeCount: 12 } },
|
||||
{ messageId: '0000000000000000000000000000000000000000000000000000000000000000', type: MessageType.Post, subtype: PostMessageSubType.Default, payload: { content: 'test 2' }, createdAt: 1, meta: { likeCount: 13 } },
|
||||
],
|
||||
})
|
||||
}));
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchPosts());
|
||||
const map = store.getState().posts.map;
|
||||
expect(fetchStub.args[0][0])
|
||||
.toBe('http://127.0.0.1:3000/v1/posts?limit=10&offset=0');
|
||||
expect(map['0657166868848f6b37debc5833112be36266dbfc8ec4f45c235372306fdfe965'].payload.content)
|
||||
.toBe('test 1');
|
||||
expect(map['5775c1181556da4a0f1a1f378006ec71a131f3cd5f759bc852df97748b1d9935'].payload.content)
|
||||
.toBe('test 2');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch liked by', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{ messageId: '0000000000000000000000000000000000000000000000000000000000000000', type: MessageType.Post, subtype: PostMessageSubType.Reply, payload: { content: 'like 1' }, createdAt: 1, meta: { likeCount: 17 } },
|
||||
{ messageId: '0000000000000000000000000000000000000000000000000000000000000000', type: MessageType.Post, subtype: PostMessageSubType.MirrorPost, payload: { content: 'like 2' }, createdAt: 1, meta: { likeCount: 17 } },
|
||||
],
|
||||
})
|
||||
}));
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchLikedBy('0xuser'));
|
||||
const map = store.getState().posts.map;
|
||||
|
||||
expect(fetchStub.args[0][0])
|
||||
.toBe('http://127.0.0.1:3000/v1/0xuser/likes?limit=10&offset=0');
|
||||
expect(map['67b5f4334815476783316977c9608633219705a3eceb00c39a88ce173705f67f'].payload.content)
|
||||
.toBe('like 1');
|
||||
expect(map['20d0bcb1b075de4e49d0adb7efd10becbf547ce2c46a2ddf01d48c83964fdfc5'].payload.content)
|
||||
.toBe('like 2');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch replied by', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{ messageId: '0000000000000000000000000000000000000000000000000000000000000000', type: MessageType.Post, subtype: PostMessageSubType.Reply, payload: { content: 'reply 1' }, createdAt: 1, meta: { likeCount: 17 } },
|
||||
{ messageId: '0000000000000000000000000000000000000000000000000000000000000000', type: MessageType.Post, subtype: PostMessageSubType.MirrorReply, payload: { content: 'reply 2' }, createdAt: 1, meta: { likeCount: 17 } },
|
||||
],
|
||||
})
|
||||
}));
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchRepliedBy('0xuser'));
|
||||
const map = store.getState().posts.map;
|
||||
|
||||
expect(fetchStub.args[0][0])
|
||||
.toBe('http://127.0.0.1:3000/v1/0xuser/replies?limit=10&offset=0');
|
||||
expect(map['86e976c49fcdb572e95bdb58ecb4c38b6a4e6e6d132fda8cb910c8a627a8de87'].payload.content)
|
||||
.toBe('reply 1');
|
||||
expect(map['6c190cc667449d4867391c2bf6c4c17839143b036e92ed04dd07379d01b9bf7e'].payload.content)
|
||||
.toBe('reply 2');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch homefed', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{ messageId: '0000000000000000000000000000000000000000000000000000000000000000', type: MessageType.Post, subtype: PostMessageSubType.Default, payload: { content: 'homefeed 1' }, createdAt: 1, meta: { likeCount: 17 } },
|
||||
{ messageId: '0000000000000000000000000000000000000000000000000000000000000000', type: MessageType.Post, subtype: PostMessageSubType.Default, payload: { content: 'homefeed 2' }, createdAt: 1, meta: { likeCount: 17 } },
|
||||
],
|
||||
})
|
||||
}));
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchHomeFeed());
|
||||
const map = store.getState().posts.map;
|
||||
|
||||
expect(fetchStub.args[0][0])
|
||||
.toBe('http://127.0.0.1:3000/v1/homefeed?limit=10&offset=0');
|
||||
expect(map['04475753c11b8a9a1e9b8fd63d822e416c963b611ef7e09acf74583d4ca32f79'].payload.content)
|
||||
.toBe('homefeed 1');
|
||||
expect(map['b5ccff8a5d242605599e182a29c08932d99bda45b8d0b149c8ddee19bb2ba6bf'].payload.content)
|
||||
.toBe('homefeed 2');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch tagfeed', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{ messageId: '0000000000000000000000000000000000000000000000000000000000000000', type: MessageType.Post, subtype: PostMessageSubType.Default, payload: { content: '#tagfeed 1' }, createdAt: 1, meta: { likeCount: 17 } },
|
||||
{ messageId: '0000000000000000000000000000000000000000000000000000000000000000', type: MessageType.Post, subtype: PostMessageSubType.Default, payload: { content: '#tagfeed 2' }, createdAt: 1, meta: { likeCount: 17 } },
|
||||
],
|
||||
})
|
||||
}));
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchTagFeed('#tagfeed'));
|
||||
const map = store.getState().posts.map;
|
||||
|
||||
expect(fetchStub.args[0][0])
|
||||
.toBe('http://127.0.0.1:3000/v1/tags/%23tagfeed?limit=10&offset=0');
|
||||
expect(map['0a8a52534ffaf357dd35061f127fa3c2628e29f4e0a7939e0c503a467130128a'].payload.content)
|
||||
.toBe('#tagfeed 1');
|
||||
expect(map['b30fa8424d3db0889ddb39d968f5a8b7df4b4b847b0ed1e5df1f74f834cba463'].payload.content)
|
||||
.toBe('#tagfeed 2');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch replies', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{ messageId: '0000000000000000000000000000000000000000000000000000000000000000', type: MessageType.Post, subtype: PostMessageSubType.Default, payload: { content: 'replies 3' }, createdAt: 1, meta: { likeCount: 17 } },
|
||||
{ messageId: '0000000000000000000000000000000000000000000000000000000000000000', type: MessageType.Post, subtype: PostMessageSubType.Default, payload: { content: 'replies 4' }, createdAt: 1, meta: { likeCount: 17 } },
|
||||
],
|
||||
})
|
||||
}));
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchReplies('0xparent'));
|
||||
const map = store.getState().posts.map;
|
||||
|
||||
expect(fetchStub.args[0][0])
|
||||
.toBe('http://127.0.0.1:3000/v1/replies?limit=10&offset=0&parent=0xparent');
|
||||
expect(map['4a524cdf0239a6fc465ada54fa1d0b568c186ca03054607b7463f16c14c56f2d'].payload.content)
|
||||
.toBe('replies 3');
|
||||
expect(map['997c84ed80660c89403b530b2e3fc8a8af061de6609415375ccbc4adb34f12e3'].payload.content)
|
||||
.toBe('replies 4');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should handle meta', async () => {
|
||||
await store.dispatch(incrementLike('0xtestmeta'));
|
||||
await store.dispatch(incrementReply('0xtestmeta'));
|
||||
await store.dispatch(incrementRepost('0xtestmeta'));
|
||||
await store.dispatch(setLiked('0xtestmeta', '0xlikedtestmeta'));
|
||||
await store.dispatch(setReposted('0xtestmeta', '0xrepostedtestmeta'));
|
||||
await store.dispatch(setBlockedPost('0xtestmeta', '0xblockedtestmeta'));
|
||||
expect(store.getState().posts.meta['0xtestmeta'])
|
||||
.toStrictEqual({
|
||||
likeCount: 1,
|
||||
replyCount: 1,
|
||||
repostCount: 1,
|
||||
liked: '0xlikedtestmeta',
|
||||
reposted: '0xrepostedtestmeta',
|
||||
blocked: '0xblockedtestmeta'
|
||||
});
|
||||
await store.dispatch(decrementLike('0xtestmeta'));
|
||||
await store.dispatch(decrementRepost('0xtestmeta'));
|
||||
await store.dispatch(setLiked('0xtestmeta', null));
|
||||
await store.dispatch(setReposted('0xtestmeta', null));
|
||||
await store.dispatch(setBlockedPost('0xtestmeta', null));
|
||||
expect(store.getState().posts.meta['0xtestmeta'])
|
||||
.toStrictEqual({
|
||||
likeCount: 0,
|
||||
replyCount: 1,
|
||||
repostCount: 0,
|
||||
liked: null,
|
||||
reposted: null,
|
||||
blocked: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
1107
src/ducks/posts.ts
1107
src/ducks/posts.ts
File diff suppressed because it is too large
Load Diff
@@ -1,126 +1,125 @@
|
||||
import {store, ducks, fetchStub} from "../util/testUtils";
|
||||
import { store, ducks, fetchStub } from '../util/testUtils';
|
||||
|
||||
const {
|
||||
users: {
|
||||
fetchAddressByName,
|
||||
fetchUsers,
|
||||
getUser,
|
||||
resetUser,
|
||||
searchUsers,
|
||||
setFollowed,
|
||||
},
|
||||
users: { fetchAddressByName, fetchUsers, getUser, resetUser, searchUsers, setFollowed },
|
||||
} = ducks;
|
||||
|
||||
describe('Users Duck', () => {
|
||||
it('should initialize', async () => {
|
||||
expect(store.getState().users).toStrictEqual({
|
||||
map: {},
|
||||
});
|
||||
});
|
||||
it('should initialize', async () => {
|
||||
expect(store.getState().users).toStrictEqual({
|
||||
map: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch address by name', async () => {
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchAddressByName('yagamilight.eth'));
|
||||
expect(store.getState().users.map['0xd44a82dD160217d46D754a03C8f841edF06EBE3c'])
|
||||
.toStrictEqual({
|
||||
ens: 'yagamilight.eth',
|
||||
username: '0xd44a82dD160217d46D754a03C8f841edF06EBE3c',
|
||||
address: '0xd44a82dD160217d46D754a03C8f841edF06EBE3c',
|
||||
});
|
||||
});
|
||||
it('should fetch address by name', async () => {
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchAddressByName('yagamilight.eth'));
|
||||
expect(store.getState().users.map['0xd44a82dD160217d46D754a03C8f841edF06EBE3c']).toStrictEqual({
|
||||
ens: 'yagamilight.eth',
|
||||
username: '0xd44a82dD160217d46D754a03C8f841edF06EBE3c',
|
||||
address: '0xd44a82dD160217d46D754a03C8f841edF06EBE3c',
|
||||
});
|
||||
});
|
||||
|
||||
it('should get user', async () => {
|
||||
const user = {
|
||||
address: '0xd44a82dD160217d46D754a03C8f841edF06EBE3c',
|
||||
ens: 'yagamilight.eth',
|
||||
username: '0xd44a82dD160217d46D754a03C8f841edF06EBE3c',
|
||||
name: 'yagami',
|
||||
pubkey: 'pubkey',
|
||||
bio: 'my bio',
|
||||
profileImage: 'http://profile.image',
|
||||
coverImage: 'http://cover.image',
|
||||
twitterVerification: 'http://twitter/123',
|
||||
website: '',
|
||||
joinedAt: 1,
|
||||
joinedTx: '0xjoinedtxhash',
|
||||
type: 'arbitrum',
|
||||
meta: {
|
||||
followerCount: 2,
|
||||
followingCount: 2,
|
||||
blockedCount: 2,
|
||||
blockingCount: 2,
|
||||
postingCount: 2,
|
||||
followed: 2,
|
||||
blocked: 2,
|
||||
},
|
||||
};
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({ payload: user }),
|
||||
}));
|
||||
// @ts-ignore
|
||||
await store.dispatch(getUser('yagamilight.eth'));
|
||||
expect(store.getState().users.map['0xd44a82dD160217d46D754a03C8f841edF06EBE3c'])
|
||||
.toStrictEqual(user);
|
||||
fetchStub.reset();
|
||||
});
|
||||
it('should get user', async () => {
|
||||
const user = {
|
||||
address: '0xd44a82dD160217d46D754a03C8f841edF06EBE3c',
|
||||
ens: 'yagamilight.eth',
|
||||
username: '0xd44a82dD160217d46D754a03C8f841edF06EBE3c',
|
||||
name: 'yagami',
|
||||
pubkey: 'pubkey',
|
||||
bio: 'my bio',
|
||||
profileImage: 'http://profile.image',
|
||||
coverImage: 'http://cover.image',
|
||||
twitterVerification: 'http://twitter/123',
|
||||
website: '',
|
||||
joinedAt: 1,
|
||||
joinedTx: '0xjoinedtxhash',
|
||||
type: 'arbitrum',
|
||||
meta: {
|
||||
followerCount: 2,
|
||||
followingCount: 2,
|
||||
blockedCount: 2,
|
||||
blockingCount: 2,
|
||||
postingCount: 2,
|
||||
followed: 2,
|
||||
blocked: 2,
|
||||
},
|
||||
};
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({ payload: user }),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
await store.dispatch(getUser('yagamilight.eth'));
|
||||
expect(store.getState().users.map['0xd44a82dD160217d46D754a03C8f841edF06EBE3c']).toStrictEqual(
|
||||
user
|
||||
);
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should fetch users', async () => {
|
||||
const user1 = {
|
||||
address: '0x001',
|
||||
ens: '0x001.eth',
|
||||
username: '0x001',
|
||||
};
|
||||
const user2 = {
|
||||
address: '0x002',
|
||||
ens: '0x002.eth',
|
||||
username: '0x002',
|
||||
};
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({ payload: [user1, user2] }),
|
||||
}));
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchUsers());
|
||||
expect(store.getState().users.map['0x001'].ens).toBe('0x001.eth');
|
||||
expect(store.getState().users.map['0x002'].ens).toBe('0x002.eth');
|
||||
fetchStub.reset();
|
||||
});
|
||||
it('should fetch users', async () => {
|
||||
const user1 = {
|
||||
address: '0x001',
|
||||
ens: '0x001.eth',
|
||||
username: '0x001',
|
||||
};
|
||||
const user2 = {
|
||||
address: '0x002',
|
||||
ens: '0x002.eth',
|
||||
username: '0x002',
|
||||
};
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({ payload: [user1, user2] }),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
await store.dispatch(fetchUsers());
|
||||
expect(store.getState().users.map['0x001'].ens).toBe('0x001.eth');
|
||||
expect(store.getState().users.map['0x002'].ens).toBe('0x002.eth');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should search users', async () => {
|
||||
const user3 = {
|
||||
address: '0x003',
|
||||
ens: '0x003.eth',
|
||||
username: '0x003',
|
||||
};
|
||||
const user4 = {
|
||||
address: '0x004',
|
||||
ens: '0x004.eth',
|
||||
username: '0x004',
|
||||
};
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({ payload: [user3, user4] }),
|
||||
}));
|
||||
// @ts-ignore
|
||||
const users: any = await store.dispatch(searchUsers('heyo'));
|
||||
it('should search users', async () => {
|
||||
const user3 = {
|
||||
address: '0x003',
|
||||
ens: '0x003.eth',
|
||||
username: '0x003',
|
||||
};
|
||||
const user4 = {
|
||||
address: '0x004',
|
||||
ens: '0x004.eth',
|
||||
username: '0x004',
|
||||
};
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({ payload: [user3, user4] }),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
const users: any = await store.dispatch(searchUsers('heyo'));
|
||||
|
||||
expect(fetchStub.args[0][0]).toBe('http://127.0.0.1:3000/v1/users/search/heyo?limit=5');
|
||||
expect(users[0].ens).toBe('0x003.eth');
|
||||
expect(users[1].ens).toBe('0x004.eth');
|
||||
expect(fetchStub.args[0][0]).toBe('http://127.0.0.1:3000/v1/users/search/heyo?limit=5');
|
||||
expect(users[0].ens).toBe('0x003.eth');
|
||||
expect(users[1].ens).toBe('0x004.eth');
|
||||
|
||||
fetchStub.reset();
|
||||
});
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
it('should set followed', async () => {
|
||||
// @ts-ignore
|
||||
store.dispatch(setFollowed('0x002', '0xfollowhash'));
|
||||
expect(store.getState().users.map['0x002'].meta.followed).toBe('0xfollowhash');
|
||||
});
|
||||
it('should set followed', async () => {
|
||||
// @ts-ignore
|
||||
store.dispatch(setFollowed('0x002', '0xfollowhash'));
|
||||
expect(store.getState().users.map['0x002'].meta.followed).toBe('0xfollowhash');
|
||||
});
|
||||
|
||||
it('should reset', async () => {
|
||||
// @ts-ignore
|
||||
store.dispatch(resetUser());
|
||||
expect(store.getState().users.map).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
it('should reset', async () => {
|
||||
// @ts-ignore
|
||||
store.dispatch(resetUser());
|
||||
expect(store.getState().users.map).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,475 +1,506 @@
|
||||
import {Dispatch} from "redux";
|
||||
import {AppRootState} from "../store/configureAppStore";
|
||||
import {useSelector} from "react-redux";
|
||||
import deepEqual from "fast-deep-equal";
|
||||
import config from "../util/config";
|
||||
import {defaultENS, fetchNameByAddress, fetchAddressByName as _fetchAddressByName} from "../util/web3";
|
||||
import {ThunkDispatch} from "redux-thunk";
|
||||
import {setJoinedTx} from "./web3";
|
||||
import {getContextNameFromState} from "./posts";
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppRootState } from '../store/configureAppStore';
|
||||
import { useSelector } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import config from '../util/config';
|
||||
import {
|
||||
defaultENS,
|
||||
fetchNameByAddress,
|
||||
fetchAddressByName as _fetchAddressByName,
|
||||
} from '../util/web3';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { setJoinedTx } from './web3';
|
||||
import { getContextNameFromState } from './posts';
|
||||
|
||||
enum ActionTypes {
|
||||
SET_USER = 'users/setUser',
|
||||
RESET_USERS = 'users/resetUsers',
|
||||
SET_FOLLOWED = 'users/setFollowed',
|
||||
SET_BLOCKED = 'users/setBlocked',
|
||||
SET_USER_ADDRESS = 'users/setUserAddress',
|
||||
SET_ECDH = 'users/setECDH',
|
||||
SET_ID_COMMITMENT = 'users/setIdCommitment',
|
||||
SET_ACCEPTANCE_SENT = 'users/setAcceptanceSent',
|
||||
SET_USER = 'users/setUser',
|
||||
RESET_USERS = 'users/resetUsers',
|
||||
SET_FOLLOWED = 'users/setFollowed',
|
||||
SET_BLOCKED = 'users/setBlocked',
|
||||
SET_USER_ADDRESS = 'users/setUserAddress',
|
||||
SET_ECDH = 'users/setECDH',
|
||||
SET_ID_COMMITMENT = 'users/setIdCommitment',
|
||||
SET_ACCEPTANCE_SENT = 'users/setAcceptanceSent',
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
type: ActionTypes;
|
||||
payload?: payload;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
}
|
||||
type: ActionTypes;
|
||||
payload?: payload;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
username: string;
|
||||
ens?: string;
|
||||
name: string;
|
||||
pubkey: string;
|
||||
address: string;
|
||||
coverImage: string;
|
||||
profileImage: string;
|
||||
twitterVerification: string;
|
||||
bio: string;
|
||||
website: string;
|
||||
group: boolean;
|
||||
ecdh: string;
|
||||
idcommitment: string;
|
||||
joinedAt: number;
|
||||
joinedTx: string;
|
||||
type: 'ens' | 'arbitrum' | '';
|
||||
meta: {
|
||||
blockedCount: number;
|
||||
blockingCount: number;
|
||||
followerCount: number;
|
||||
followingCount: number;
|
||||
postingCount: number;
|
||||
followed: string | null;
|
||||
blocked: string | null;
|
||||
inviteSent: string | null;
|
||||
acceptanceReceived: string | null;
|
||||
inviteReceived: string | null;
|
||||
acceptanceSent: string | null;
|
||||
};
|
||||
}
|
||||
username: string;
|
||||
ens?: string;
|
||||
name: string;
|
||||
pubkey: string;
|
||||
address: string;
|
||||
coverImage: string;
|
||||
profileImage: string;
|
||||
twitterVerification: string;
|
||||
bio: string;
|
||||
website: string;
|
||||
group: boolean;
|
||||
ecdh: string;
|
||||
idcommitment: string;
|
||||
joinedAt: number;
|
||||
joinedTx: string;
|
||||
type: 'ens' | 'arbitrum' | '';
|
||||
meta: {
|
||||
blockedCount: number;
|
||||
blockingCount: number;
|
||||
followerCount: number;
|
||||
followingCount: number;
|
||||
postingCount: number;
|
||||
followed: string | null;
|
||||
blocked: string | null;
|
||||
inviteSent: string | null;
|
||||
acceptanceReceived: string | null;
|
||||
inviteReceived: string | null;
|
||||
acceptanceSent: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
type State = {
|
||||
map: {
|
||||
[name: string]: User;
|
||||
};
|
||||
}
|
||||
map: {
|
||||
[name: string]: User;
|
||||
};
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
map: {},
|
||||
map: {},
|
||||
};
|
||||
|
||||
let fetchPromises: any = {};
|
||||
let cachedUser: any = {};
|
||||
|
||||
export const fetchAddressByName = (ens: string) => async (dispatch: Dispatch) => {
|
||||
const address = await _fetchAddressByName(ens);
|
||||
dispatch({
|
||||
type: ActionTypes.SET_USER_ADDRESS,
|
||||
payload: {
|
||||
ens: ens,
|
||||
address: address === '0x0000000000000000000000000000000000000000' ? '' : address,
|
||||
},
|
||||
});
|
||||
return address;
|
||||
}
|
||||
const address = await _fetchAddressByName(ens);
|
||||
dispatch({
|
||||
type: ActionTypes.SET_USER_ADDRESS,
|
||||
payload: {
|
||||
ens: ens,
|
||||
address: address === '0x0000000000000000000000000000000000000000' ? '' : address,
|
||||
},
|
||||
});
|
||||
return address;
|
||||
};
|
||||
|
||||
export const watchUser = (username: string) => async (dispatch: ThunkDispatch<any, any, any>) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
_getUser();
|
||||
return new Promise(async (resolve, reject) => {
|
||||
_getUser();
|
||||
|
||||
async function _getUser() {
|
||||
const user: any = await dispatch(getUser(username));
|
||||
async function _getUser() {
|
||||
const user: any = await dispatch(getUser(username));
|
||||
|
||||
if (!user?.joinedTx) {
|
||||
setTimeout(_getUser, 5000);
|
||||
return;
|
||||
}
|
||||
if (!user?.joinedTx) {
|
||||
setTimeout(_getUser, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
resolve(user);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getUser = (address: string) => async (dispatch: Dispatch, getState: () => AppRootState): Promise<User> => {
|
||||
export const getUser =
|
||||
(address: string) =>
|
||||
async (dispatch: Dispatch, getState: () => AppRootState): Promise<User> => {
|
||||
const contextualName = getContextNameFromState(getState());
|
||||
const key = contextualName + address;
|
||||
|
||||
if (fetchPromises[key]) {
|
||||
return fetchPromises[key];
|
||||
return fetchPromises[key];
|
||||
}
|
||||
|
||||
const fetchPromise = new Promise<User>(async (resolve, reject) => {
|
||||
let payload;
|
||||
let payload;
|
||||
|
||||
if (cachedUser[key]) {
|
||||
payload = cachedUser[key];
|
||||
} else {
|
||||
const resp = await fetch(`${config.indexerAPI}/v1/users/${address}`, {
|
||||
method: 'GET',
|
||||
// @ts-ignore
|
||||
headers: {
|
||||
'x-contextual-name': contextualName,
|
||||
},
|
||||
});
|
||||
const json = await resp.json();
|
||||
// @ts-ignore
|
||||
payload = dispatch(processUserPayload({...json.payload}));
|
||||
if (payload?.joinedTx) {
|
||||
cachedUser[key] = payload;
|
||||
}
|
||||
|
||||
delete fetchPromises[key];
|
||||
if (cachedUser[key]) {
|
||||
payload = cachedUser[key];
|
||||
} else {
|
||||
const resp = await fetch(`${config.indexerAPI}/v1/users/${address}`, {
|
||||
method: 'GET',
|
||||
// @ts-ignore
|
||||
headers: {
|
||||
'x-contextual-name': contextualName,
|
||||
},
|
||||
});
|
||||
const json = await resp.json();
|
||||
// @ts-ignore
|
||||
payload = dispatch(processUserPayload({ ...json.payload }));
|
||||
if (payload?.joinedTx) {
|
||||
cachedUser[key] = payload;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.SET_USER,
|
||||
payload: payload,
|
||||
});
|
||||
delete fetchPromises[key];
|
||||
}
|
||||
|
||||
resolve(payload);
|
||||
dispatch({
|
||||
type: ActionTypes.SET_USER,
|
||||
payload: payload,
|
||||
});
|
||||
|
||||
resolve(payload);
|
||||
});
|
||||
|
||||
fetchPromises[key] = fetchPromise;
|
||||
|
||||
return fetchPromise;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchUsers = () => async (dispatch: Dispatch, getState: () => AppRootState): Promise<string[]> => {
|
||||
export const fetchUsers =
|
||||
() =>
|
||||
async (dispatch: Dispatch, getState: () => AppRootState): Promise<string[]> => {
|
||||
const contextualName = getContextNameFromState(getState());
|
||||
const resp = await fetch(`${config.indexerAPI}/v1/users?limit=5`, {
|
||||
method: 'GET',
|
||||
// @ts-ignore
|
||||
headers: {
|
||||
'x-contextual-name': contextualName,
|
||||
},
|
||||
method: 'GET',
|
||||
// @ts-ignore
|
||||
headers: {
|
||||
'x-contextual-name': contextualName,
|
||||
},
|
||||
});
|
||||
|
||||
const json = await resp.json();
|
||||
const list: string[] = [];
|
||||
|
||||
for (const user of json.payload) {
|
||||
// @ts-ignore
|
||||
const payload = dispatch(processUserPayload(user));
|
||||
const key = contextualName + user.address;
|
||||
if (payload?.joinedTx) {
|
||||
cachedUser[key] = payload;
|
||||
}
|
||||
list.push(user.address);
|
||||
// @ts-ignore
|
||||
const payload = dispatch(processUserPayload(user));
|
||||
const key = contextualName + user.address;
|
||||
if (payload?.joinedTx) {
|
||||
cachedUser[key] = payload;
|
||||
}
|
||||
list.push(user.address);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
};
|
||||
|
||||
export const searchUsers = (query: string) => async (dispatch: Dispatch, getState: () => AppRootState): Promise<string[]> => {
|
||||
export const searchUsers =
|
||||
(query: string) =>
|
||||
async (dispatch: Dispatch, getState: () => AppRootState): Promise<string[]> => {
|
||||
const contextualName = getContextNameFromState(getState());
|
||||
const resp = await fetch(`${config.indexerAPI}/v1/users/search/${encodeURIComponent(query)}?limit=5`, {
|
||||
const resp = await fetch(
|
||||
`${config.indexerAPI}/v1/users/search/${encodeURIComponent(query)}?limit=5`,
|
||||
{
|
||||
method: 'GET',
|
||||
// @ts-ignore
|
||||
headers: {
|
||||
'x-contextual-name': contextualName,
|
||||
'x-contextual-name': contextualName,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const json = await resp.json();
|
||||
const list: string[] = [];
|
||||
|
||||
for (const user of json.payload) {
|
||||
// @ts-ignore
|
||||
const payload = dispatch(processUserPayload({...user}));
|
||||
const key = contextualName + user.address;
|
||||
if (payload?.joinedTx) {
|
||||
cachedUser[key] = payload;
|
||||
}
|
||||
list.push(user.address);
|
||||
// @ts-ignore
|
||||
const payload = dispatch(processUserPayload({ ...user }));
|
||||
const key = contextualName + user.address;
|
||||
if (payload?.joinedTx) {
|
||||
cachedUser[key] = payload;
|
||||
}
|
||||
list.push(user.address);
|
||||
}
|
||||
|
||||
return json.payload;
|
||||
}
|
||||
};
|
||||
|
||||
export const setAcceptanceSent = (address: string, acceptanceSent: string | null): Action<{ address: string; acceptanceSent: string | null }> => ({
|
||||
type: ActionTypes.SET_ACCEPTANCE_SENT,
|
||||
payload: {address, acceptanceSent},
|
||||
export const setAcceptanceSent = (
|
||||
address: string,
|
||||
acceptanceSent: string | null
|
||||
): Action<{ address: string; acceptanceSent: string | null }> => ({
|
||||
type: ActionTypes.SET_ACCEPTANCE_SENT,
|
||||
payload: { address, acceptanceSent },
|
||||
});
|
||||
|
||||
export const setFollowed = (address: string, followed: string | null): Action<{ address: string; followed: string | null }> => ({
|
||||
type: ActionTypes.SET_FOLLOWED,
|
||||
payload: {address, followed},
|
||||
export const setFollowed = (
|
||||
address: string,
|
||||
followed: string | null
|
||||
): Action<{ address: string; followed: string | null }> => ({
|
||||
type: ActionTypes.SET_FOLLOWED,
|
||||
payload: { address, followed },
|
||||
});
|
||||
|
||||
export const setBlocked = (address: string, blocked: string | null): Action<{ address: string; blocked: string | null }> => ({
|
||||
type: ActionTypes.SET_BLOCKED,
|
||||
payload: {address, blocked},
|
||||
export const setBlocked = (
|
||||
address: string,
|
||||
blocked: string | null
|
||||
): Action<{ address: string; blocked: string | null }> => ({
|
||||
type: ActionTypes.SET_BLOCKED,
|
||||
payload: { address, blocked },
|
||||
});
|
||||
|
||||
export const setEcdh = (address: string, ecdh: string): Action<{ address: string; ecdh: string }> => ({
|
||||
type: ActionTypes.SET_ECDH,
|
||||
payload: {address, ecdh},
|
||||
export const setEcdh = (
|
||||
address: string,
|
||||
ecdh: string
|
||||
): Action<{ address: string; ecdh: string }> => ({
|
||||
type: ActionTypes.SET_ECDH,
|
||||
payload: { address, ecdh },
|
||||
});
|
||||
|
||||
export const setIdCommitment = (address: string, idcommitment: string): Action<{ address: string; idcommitment: string }> => ({
|
||||
type: ActionTypes.SET_ID_COMMITMENT,
|
||||
payload: {address, idcommitment},
|
||||
export const setIdCommitment = (
|
||||
address: string,
|
||||
idcommitment: string
|
||||
): Action<{ address: string; idcommitment: string }> => ({
|
||||
type: ActionTypes.SET_ID_COMMITMENT,
|
||||
payload: { address, idcommitment },
|
||||
});
|
||||
|
||||
export const resetUser = () => {
|
||||
fetchPromises = {};
|
||||
cachedUser = {};
|
||||
return {
|
||||
type: ActionTypes.RESET_USERS,
|
||||
}
|
||||
fetchPromises = {};
|
||||
cachedUser = {};
|
||||
return {
|
||||
type: ActionTypes.RESET_USERS,
|
||||
};
|
||||
};
|
||||
|
||||
const processUserPayload = (user: any) => (dispatch: Dispatch) => {
|
||||
const payload: User = {
|
||||
address: user.username,
|
||||
ens: user.ens,
|
||||
username: user.username,
|
||||
name: user.name || '',
|
||||
pubkey: user.pubkey || '',
|
||||
bio: user.bio || '',
|
||||
profileImage: user.profileImage || '',
|
||||
coverImage: user.coverImage || '',
|
||||
group: !!user.group,
|
||||
twitterVerification: user.twitterVerification || '',
|
||||
website: user.website || '',
|
||||
ecdh: user.ecdh || '',
|
||||
idcommitment: user.idcommitment || '',
|
||||
joinedAt: user.joinedAt || '',
|
||||
joinedTx: user.joinedTx || '',
|
||||
type: user.type || '',
|
||||
meta: {
|
||||
followerCount: user.meta?.followerCount || 0,
|
||||
followingCount: user.meta?.followingCount || 0,
|
||||
blockedCount: user.meta?.blockedCount || 0,
|
||||
blockingCount: user.meta?.blockingCount || 0,
|
||||
postingCount: user.meta?.postingCount || 0,
|
||||
followed: user.meta?.followed || null,
|
||||
blocked: user.meta?.blocked || null,
|
||||
inviteSent: user.meta?.inviteSent || null,
|
||||
acceptanceReceived: user.meta?.acceptanceReceived || null,
|
||||
inviteReceived: user.meta?.inviteReceived || null,
|
||||
acceptanceSent: user.meta?.acceptanceSent || null,
|
||||
},
|
||||
};
|
||||
const payload: User = {
|
||||
address: user.username,
|
||||
ens: user.ens,
|
||||
username: user.username,
|
||||
name: user.name || '',
|
||||
pubkey: user.pubkey || '',
|
||||
bio: user.bio || '',
|
||||
profileImage: user.profileImage || '',
|
||||
coverImage: user.coverImage || '',
|
||||
group: !!user.group,
|
||||
twitterVerification: user.twitterVerification || '',
|
||||
website: user.website || '',
|
||||
ecdh: user.ecdh || '',
|
||||
idcommitment: user.idcommitment || '',
|
||||
joinedAt: user.joinedAt || '',
|
||||
joinedTx: user.joinedTx || '',
|
||||
type: user.type || '',
|
||||
meta: {
|
||||
followerCount: user.meta?.followerCount || 0,
|
||||
followingCount: user.meta?.followingCount || 0,
|
||||
blockedCount: user.meta?.blockedCount || 0,
|
||||
blockingCount: user.meta?.blockingCount || 0,
|
||||
postingCount: user.meta?.postingCount || 0,
|
||||
followed: user.meta?.followed || null,
|
||||
blocked: user.meta?.blocked || null,
|
||||
inviteSent: user.meta?.inviteSent || null,
|
||||
acceptanceReceived: user.meta?.acceptanceReceived || null,
|
||||
inviteReceived: user.meta?.inviteReceived || null,
|
||||
acceptanceSent: user.meta?.acceptanceSent || null,
|
||||
},
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.SET_USER,
|
||||
payload: payload,
|
||||
});
|
||||
dispatch({
|
||||
type: ActionTypes.SET_USER,
|
||||
payload: payload,
|
||||
});
|
||||
|
||||
return payload;
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
export const setUser = (user: User) => ({
|
||||
type: ActionTypes.SET_USER,
|
||||
payload: user,
|
||||
type: ActionTypes.SET_USER,
|
||||
payload: user,
|
||||
});
|
||||
|
||||
export const useConnectedTwitter = (address = '') => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
const user = state.users.map[address];
|
||||
return useSelector((state: AppRootState) => {
|
||||
const user = state.users.map[address];
|
||||
|
||||
if (!user?.twitterVerification) return null;
|
||||
if (!user?.twitterVerification) return null;
|
||||
|
||||
const [twitterHandle] = user.twitterVerification.replace('https://twitter.com/', '').split('/');
|
||||
return twitterHandle;
|
||||
}, deepEqual);
|
||||
}
|
||||
const [twitterHandle] = user.twitterVerification.replace('https://twitter.com/', '').split('/');
|
||||
return twitterHandle;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useUser = (address = ''): User | null => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
if (!address) return null;
|
||||
return useSelector((state: AppRootState) => {
|
||||
if (!address) return null;
|
||||
|
||||
const user = state.users.map[address];
|
||||
const user = state.users.map[address];
|
||||
|
||||
const val: User = user;
|
||||
const val: User = user;
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
username: address,
|
||||
name: '',
|
||||
pubkey: '',
|
||||
address: address,
|
||||
coverImage: '',
|
||||
profileImage: '',
|
||||
twitterVerification: '',
|
||||
bio: '',
|
||||
website: '',
|
||||
ecdh: '',
|
||||
idcommitment: '',
|
||||
joinedAt: 0,
|
||||
joinedTx: '',
|
||||
type: '',
|
||||
group: false,
|
||||
meta: {
|
||||
followerCount: 0,
|
||||
followingCount: 0,
|
||||
blockedCount: 0,
|
||||
blockingCount: 0,
|
||||
postingCount: 0,
|
||||
followed: null,
|
||||
blocked: null,
|
||||
inviteSent: null,
|
||||
acceptanceReceived: null,
|
||||
inviteReceived: null,
|
||||
acceptanceSent: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
if (!user) {
|
||||
return {
|
||||
username: address,
|
||||
name: '',
|
||||
pubkey: '',
|
||||
address: address,
|
||||
coverImage: '',
|
||||
profileImage: '',
|
||||
twitterVerification: '',
|
||||
bio: '',
|
||||
website: '',
|
||||
ecdh: '',
|
||||
idcommitment: '',
|
||||
joinedAt: 0,
|
||||
joinedTx: '',
|
||||
type: '',
|
||||
group: false,
|
||||
meta: {
|
||||
followerCount: 0,
|
||||
followingCount: 0,
|
||||
blockedCount: 0,
|
||||
blockingCount: 0,
|
||||
postingCount: 0,
|
||||
followed: null,
|
||||
blocked: null,
|
||||
inviteSent: null,
|
||||
acceptanceReceived: null,
|
||||
inviteReceived: null,
|
||||
acceptanceSent: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return user;
|
||||
}, deepEqual);
|
||||
}
|
||||
return user;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export default function users(state = initialState, action: Action<any>): State {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_USER:
|
||||
return reduceSetUser(state, action);
|
||||
case ActionTypes.RESET_USERS:
|
||||
return {
|
||||
map: {},
|
||||
};
|
||||
case ActionTypes.SET_ACCEPTANCE_SENT:
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[action.payload.address]: {
|
||||
...state.map[action.payload.address],
|
||||
meta: {
|
||||
...state.map[action.payload.address]?.meta,
|
||||
acceptanceSent: action.payload.acceptanceSent,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
case ActionTypes.SET_FOLLOWED:
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[action.payload.address]: {
|
||||
...state.map[action.payload.address],
|
||||
meta: {
|
||||
...state.map[action.payload.address]?.meta,
|
||||
followed: action.payload.followed,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
case ActionTypes.SET_BLOCKED:
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[action.payload.address]: {
|
||||
...state.map[action.payload.address],
|
||||
meta: {
|
||||
...state.map[action.payload.address]?.meta,
|
||||
blocked: action.payload.blocked,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
case ActionTypes.SET_ECDH:
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[action.payload.address]: {
|
||||
...state.map[action.payload.address],
|
||||
ecdh: action.payload.ecdh,
|
||||
},
|
||||
},
|
||||
};
|
||||
case ActionTypes.SET_ID_COMMITMENT:
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[action.payload.address]: {
|
||||
...state.map[action.payload.address],
|
||||
idcommitment: action.payload.idcommitment,
|
||||
},
|
||||
},
|
||||
};
|
||||
case ActionTypes.SET_USER_ADDRESS:
|
||||
return reduceSetUserAddress(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function reduceSetUserAddress(state: State, action: Action<{ ens: string; address: string }>): State {
|
||||
if (!action.payload) return state;
|
||||
|
||||
const user = state.map[action.payload.address];
|
||||
|
||||
return {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_USER:
|
||||
return reduceSetUser(state, action);
|
||||
case ActionTypes.RESET_USERS:
|
||||
return {
|
||||
map: {},
|
||||
};
|
||||
case ActionTypes.SET_ACCEPTANCE_SENT:
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[action.payload.address]: {
|
||||
...user,
|
||||
ens: action.payload.ens,
|
||||
username: action.payload.address,
|
||||
address: action.payload.address,
|
||||
...state.map,
|
||||
[action.payload.address]: {
|
||||
...state.map[action.payload.address],
|
||||
meta: {
|
||||
...state.map[action.payload.address]?.meta,
|
||||
acceptanceSent: action.payload.acceptanceSent,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
case ActionTypes.SET_FOLLOWED:
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[action.payload.address]: {
|
||||
...state.map[action.payload.address],
|
||||
meta: {
|
||||
...state.map[action.payload.address]?.meta,
|
||||
followed: action.payload.followed,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
case ActionTypes.SET_BLOCKED:
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[action.payload.address]: {
|
||||
...state.map[action.payload.address],
|
||||
meta: {
|
||||
...state.map[action.payload.address]?.meta,
|
||||
blocked: action.payload.blocked,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
case ActionTypes.SET_ECDH:
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[action.payload.address]: {
|
||||
...state.map[action.payload.address],
|
||||
ecdh: action.payload.ecdh,
|
||||
},
|
||||
},
|
||||
};
|
||||
case ActionTypes.SET_ID_COMMITMENT:
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[action.payload.address]: {
|
||||
...state.map[action.payload.address],
|
||||
idcommitment: action.payload.idcommitment,
|
||||
},
|
||||
},
|
||||
};
|
||||
case ActionTypes.SET_USER_ADDRESS:
|
||||
return reduceSetUserAddress(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function reduceSetUserAddress(
|
||||
state: State,
|
||||
action: Action<{ ens: string; address: string }>
|
||||
): State {
|
||||
if (!action.payload) return state;
|
||||
|
||||
const user = state.map[action.payload.address];
|
||||
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[action.payload.address]: {
|
||||
...user,
|
||||
ens: action.payload.ens,
|
||||
username: action.payload.address,
|
||||
address: action.payload.address,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function reduceSetUser(state: State, action: Action<User>): State {
|
||||
if (!action.payload) return state;
|
||||
if (!action.payload) return state;
|
||||
|
||||
const user = state.map[action.payload.username];
|
||||
const user = state.map[action.payload.username];
|
||||
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[action.payload.username]: {
|
||||
...user,
|
||||
username: action.payload.username,
|
||||
address: action.payload.address,
|
||||
name: action.payload.name,
|
||||
ens: action.payload.ens,
|
||||
pubkey: action.payload.pubkey,
|
||||
bio: action.payload.bio,
|
||||
profileImage: action.payload.profileImage,
|
||||
twitterVerification: action.payload.twitterVerification,
|
||||
coverImage: action.payload.coverImage,
|
||||
website: action.payload.website,
|
||||
ecdh: action.payload.ecdh,
|
||||
idcommitment: action.payload.idcommitment,
|
||||
joinedAt: action.payload.joinedAt,
|
||||
joinedTx: action.payload.joinedTx,
|
||||
type: action.payload.type,
|
||||
group: action.payload.group,
|
||||
meta: {
|
||||
followerCount: action.payload.meta?.followerCount || 0,
|
||||
followingCount: action.payload.meta?.followingCount || 0,
|
||||
blockedCount: action.payload.meta?.blockedCount || 0,
|
||||
blockingCount: action.payload.meta?.blockingCount || 0,
|
||||
postingCount: action.payload.meta?.postingCount || 0,
|
||||
followed: action.payload.meta?.followed || null,
|
||||
blocked: action.payload.meta?.blocked || null,
|
||||
inviteSent: action.payload.meta?.inviteSent || null,
|
||||
acceptanceReceived: action.payload.meta?.acceptanceReceived || null,
|
||||
inviteReceived: action.payload.meta?.inviteReceived || null,
|
||||
acceptanceSent: action.payload.meta?.acceptanceSent || null,
|
||||
},
|
||||
},
|
||||
return {
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
[action.payload.username]: {
|
||||
...user,
|
||||
username: action.payload.username,
|
||||
address: action.payload.address,
|
||||
name: action.payload.name,
|
||||
ens: action.payload.ens,
|
||||
pubkey: action.payload.pubkey,
|
||||
bio: action.payload.bio,
|
||||
profileImage: action.payload.profileImage,
|
||||
twitterVerification: action.payload.twitterVerification,
|
||||
coverImage: action.payload.coverImage,
|
||||
website: action.payload.website,
|
||||
ecdh: action.payload.ecdh,
|
||||
idcommitment: action.payload.idcommitment,
|
||||
joinedAt: action.payload.joinedAt,
|
||||
joinedTx: action.payload.joinedTx,
|
||||
type: action.payload.type,
|
||||
group: action.payload.group,
|
||||
meta: {
|
||||
followerCount: action.payload.meta?.followerCount || 0,
|
||||
followingCount: action.payload.meta?.followingCount || 0,
|
||||
blockedCount: action.payload.meta?.blockedCount || 0,
|
||||
blockingCount: action.payload.meta?.blockingCount || 0,
|
||||
postingCount: action.payload.meta?.postingCount || 0,
|
||||
followed: action.payload.meta?.followed || null,
|
||||
blocked: action.payload.meta?.blocked || null,
|
||||
inviteSent: action.payload.meta?.inviteSent || null,
|
||||
acceptanceReceived: action.payload.meta?.acceptanceReceived || null,
|
||||
inviteReceived: action.payload.meta?.inviteReceived || null,
|
||||
acceptanceSent: action.payload.meta?.acceptanceSent || null,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,132 +1,132 @@
|
||||
import sinon from 'sinon';
|
||||
import {store, ducks, web3Stub, fetchStub, postWorkMessageStub} from "../util/testUtils";
|
||||
import { store, ducks, web3Stub, fetchStub, postWorkMessageStub } from '../util/testUtils';
|
||||
|
||||
const {
|
||||
web3: {
|
||||
connectWeb3,
|
||||
setWeb3,
|
||||
loginGun,
|
||||
genSemaphore,
|
||||
updateIdentity,
|
||||
web3Modal,
|
||||
},
|
||||
web3: { connectWeb3, setWeb3, loginGun, genSemaphore, updateIdentity, web3Modal },
|
||||
} = ducks;
|
||||
|
||||
describe('Web3 Duck', () => {
|
||||
it('should set web3', async () => {
|
||||
sinon.stub(web3Modal, 'clearCachedProvider');
|
||||
sinon.stub(web3Modal, 'connect').returns(Promise.resolve(null));
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: {
|
||||
joinedTx: 'hello',
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
it('should set web3', async () => {
|
||||
sinon.stub(web3Modal, 'clearCachedProvider');
|
||||
sinon.stub(web3Modal, 'connect').returns(Promise.resolve(null));
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({ payload: {
|
||||
joinedTx: 'hello',
|
||||
}}),
|
||||
}));
|
||||
try {
|
||||
// @ts-ignore
|
||||
await store.dispatch(connectWeb3());
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Provider not set or invalid');
|
||||
}
|
||||
|
||||
try {
|
||||
// @ts-ignore
|
||||
await store.dispatch(connectWeb3());
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Provider not set or invalid');
|
||||
}
|
||||
// @ts-ignore
|
||||
await store.dispatch(setWeb3(web3Stub, '0x0000000000000000000000000000000000000000'));
|
||||
|
||||
// @ts-ignore
|
||||
await store.dispatch(setWeb3(web3Stub, '0x0000000000000000000000000000000000000000'));
|
||||
expect(store.getState().web3.account).toBe('0x0000000000000000000000000000000000000000');
|
||||
expect(store.getState().web3.networkType).toBe('main');
|
||||
|
||||
expect(store.getState().web3.account).toBe('0x0000000000000000000000000000000000000000');
|
||||
expect(store.getState().web3.networkType).toBe('main');
|
||||
const onAccountChange = web3Stub.currentProvider.on.args[0][1];
|
||||
const onNetworkChange = web3Stub.currentProvider.on.args[1][1];
|
||||
|
||||
const onAccountChange = web3Stub.currentProvider.on.args[0][1];
|
||||
const onNetworkChange = web3Stub.currentProvider.on.args[1][1];
|
||||
web3Stub.eth.getAccounts.returns(['0x0000000000000000000000000000000000000001']);
|
||||
web3Stub.eth.net.getNetworkType.returns('test');
|
||||
|
||||
web3Stub.eth.getAccounts.returns(['0x0000000000000000000000000000000000000001']);
|
||||
web3Stub.eth.net.getNetworkType.returns('test');
|
||||
await onAccountChange(['0x0000000000000000000000000000000000000001']);
|
||||
await onNetworkChange();
|
||||
|
||||
await onAccountChange(['0x0000000000000000000000000000000000000001']);
|
||||
await onNetworkChange();
|
||||
expect(store.getState().web3.account).toBe('0x0000000000000000000000000000000000000001');
|
||||
expect(store.getState().web3.networkType).toBe('test');
|
||||
|
||||
expect(store.getState().web3.account).toBe('0x0000000000000000000000000000000000000001');
|
||||
expect(store.getState().web3.networkType).toBe('test');
|
||||
fetchStub.reset();
|
||||
});
|
||||
|
||||
fetchStub.reset();
|
||||
it('should login gun', async () => {
|
||||
// @ts-ignore
|
||||
const result = await store.dispatch(loginGun());
|
||||
const pub =
|
||||
'Ex4tafFuBZQXO610uL0v76bNnYokD7m-WBKFSYdOw6k.m0KmwZKFnqe-5iFOj_VIR50_BHtQDFceK7cF-Fi_-As';
|
||||
const priv = '47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU';
|
||||
|
||||
expect(postWorkMessageStub.args[0][0]).toStrictEqual({
|
||||
type: 'serviceWorker/identity/setIdentity',
|
||||
payload: {
|
||||
type: 'gun',
|
||||
address: '0x0000000000000000000000000000000000000001',
|
||||
publicKey: pub,
|
||||
privateKey: priv,
|
||||
nonce: 0,
|
||||
},
|
||||
});
|
||||
expect(result).toStrictEqual({ priv, pub });
|
||||
postWorkMessageStub.reset();
|
||||
});
|
||||
|
||||
it('should login gun', async () => {
|
||||
// @ts-ignore
|
||||
const result = await store.dispatch(loginGun());
|
||||
const pub = 'Ex4tafFuBZQXO610uL0v76bNnYokD7m-WBKFSYdOw6k.m0KmwZKFnqe-5iFOj_VIR50_BHtQDFceK7cF-Fi_-As';
|
||||
const priv = '47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU';
|
||||
|
||||
expect(postWorkMessageStub.args[0][0])
|
||||
.toStrictEqual({
|
||||
type: 'serviceWorker/identity/setIdentity',
|
||||
payload: {
|
||||
type: 'gun',
|
||||
address: '0x0000000000000000000000000000000000000001',
|
||||
publicKey: pub,
|
||||
privateKey: priv,
|
||||
nonce: 0,
|
||||
},
|
||||
});
|
||||
expect(result).toStrictEqual({ priv, pub });
|
||||
postWorkMessageStub.reset();
|
||||
it('should gen semaphore', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: {
|
||||
data: {
|
||||
siblings: [],
|
||||
pathIndices: [],
|
||||
root: '3',
|
||||
},
|
||||
name: '',
|
||||
provider: '',
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
const result = await store.dispatch(genSemaphore('Twitter'));
|
||||
expect(postWorkMessageStub.args[0][0]).toStrictEqual({
|
||||
type: 'serviceWorker/identity/setIdentity',
|
||||
payload: {
|
||||
type: 'interrep',
|
||||
address: '0x0000000000000000000000000000000000000001',
|
||||
nonce: 0,
|
||||
provider: 'Twitter',
|
||||
name: '',
|
||||
identityPath: {
|
||||
path_elements: [],
|
||||
path_index: [],
|
||||
root: BigInt(3),
|
||||
},
|
||||
identityCommitment:
|
||||
'13918226946525796188065125653551272759560533101256756969470448137974823773959',
|
||||
serializedIdentity:
|
||||
'{"identityNullifier":"aa3fda7fc5f0d2d2b8322bcc0367a1de25929526568dd90dc3b1891138425b00","identityTrapdoor":"bc63a4fcddf77544f4cedc29c394a72c1a1d134e3693f74d99ec4dffb6bf802a","secret":["aa3fda7fc5f0d2d2b8322bcc0367a1de25929526568dd90dc3b1891138425b00","bc63a4fcddf77544f4cedc29c394a72c1a1d134e3693f74d99ec4dffb6bf802a"]}',
|
||||
},
|
||||
});
|
||||
expect(result).toBeTruthy();
|
||||
fetchStub.reset();
|
||||
postWorkMessageStub.reset();
|
||||
});
|
||||
|
||||
it('should gen semaphore', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: {
|
||||
data: {
|
||||
siblings: [],
|
||||
pathIndices: [],
|
||||
root: '3',
|
||||
},
|
||||
name: '',
|
||||
provider: '',
|
||||
},
|
||||
})
|
||||
}))
|
||||
// @ts-ignore
|
||||
const result = await store.dispatch(genSemaphore('Twitter'));
|
||||
expect(postWorkMessageStub.args[0][0])
|
||||
.toStrictEqual({
|
||||
type: 'serviceWorker/identity/setIdentity',
|
||||
payload: {
|
||||
type: 'interrep',
|
||||
address: '0x0000000000000000000000000000000000000001',
|
||||
nonce: 0,
|
||||
provider: 'Twitter',
|
||||
name: '',
|
||||
identityPath: {
|
||||
path_elements: [],
|
||||
path_index: [],
|
||||
root: BigInt(3),
|
||||
},
|
||||
identityCommitment: '13918226946525796188065125653551272759560533101256756969470448137974823773959',
|
||||
serializedIdentity: '{"identityNullifier":"aa3fda7fc5f0d2d2b8322bcc0367a1de25929526568dd90dc3b1891138425b00","identityTrapdoor":"bc63a4fcddf77544f4cedc29c394a72c1a1d134e3693f74d99ec4dffb6bf802a","secret":["aa3fda7fc5f0d2d2b8322bcc0367a1de25929526568dd90dc3b1891138425b00","bc63a4fcddf77544f4cedc29c394a72c1a1d134e3693f74d99ec4dffb6bf802a"]}'
|
||||
}
|
||||
});
|
||||
expect(result).toBeTruthy();
|
||||
fetchStub.reset();
|
||||
postWorkMessageStub.reset();
|
||||
});
|
||||
|
||||
it('should update identity', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
payload: {
|
||||
transactionHash: '0xhash',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
// @ts-ignore
|
||||
await store.dispatch(updateIdentity('0xpubkey'));
|
||||
expect(fetchStub.args[0][0])
|
||||
.toBe('http://127.0.0.1:3000/v1/users');
|
||||
fetchStub.reset();
|
||||
});
|
||||
});
|
||||
it('should update identity', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
payload: {
|
||||
transactionHash: '0xhash',
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
await store.dispatch(updateIdentity('0xpubkey'));
|
||||
expect(fetchStub.args[0][0]).toBe('http://127.0.0.1:3000/v1/users');
|
||||
fetchStub.reset();
|
||||
});
|
||||
});
|
||||
|
||||
1081
src/ducks/web3.ts
1081
src/ducks/web3.ts
File diff suppressed because it is too large
Load Diff
@@ -1,48 +1,45 @@
|
||||
import {store, ducks, postWorkMessageStub} from "../util/testUtils";
|
||||
import {Identity} from "../serviceWorkers/identity";
|
||||
import {ServiceWorkerActionType} from "../serviceWorkers/util";
|
||||
import { store, ducks, postWorkMessageStub } from '../util/testUtils';
|
||||
import { Identity } from '../serviceWorkers/identity';
|
||||
import { ServiceWorkerActionType } from '../serviceWorkers/util';
|
||||
|
||||
const {
|
||||
worker: {
|
||||
syncWorker,
|
||||
}
|
||||
worker: { syncWorker },
|
||||
} = ducks;
|
||||
|
||||
describe('Worker Duck', () => {
|
||||
it('should sync worker', async () => {
|
||||
const identities: Identity[] = [
|
||||
{
|
||||
type: 'gun',
|
||||
address: '0xgunaddress',
|
||||
nonce: 0,
|
||||
publicKey: '0xpub',
|
||||
privateKey: '0xpriv',
|
||||
},
|
||||
{
|
||||
type: 'zkpr_interrep',
|
||||
provider: 'Twitter',
|
||||
name: 'diamond',
|
||||
identityPath: {
|
||||
path_elements: ['0x1', '0x2'],
|
||||
path_index: [0, 1],
|
||||
root: '0x123',
|
||||
},
|
||||
identityCommitment: '0x12345',
|
||||
}
|
||||
];
|
||||
it('should sync worker', async () => {
|
||||
const identities: Identity[] = [
|
||||
{
|
||||
type: 'gun',
|
||||
address: '0xgunaddress',
|
||||
nonce: 0,
|
||||
publicKey: '0xpub',
|
||||
privateKey: '0xpriv',
|
||||
},
|
||||
{
|
||||
type: 'zkpr_interrep',
|
||||
provider: 'Twitter',
|
||||
name: 'diamond',
|
||||
identityPath: {
|
||||
path_elements: ['0x1', '0x2'],
|
||||
path_index: [0, 1],
|
||||
root: '0x123',
|
||||
},
|
||||
identityCommitment: '0x12345',
|
||||
},
|
||||
];
|
||||
|
||||
postWorkMessageStub
|
||||
.withArgs({ type: ServiceWorkerActionType.GET_IDENTITIES })
|
||||
.returns(Promise.resolve(identities));
|
||||
postWorkMessageStub
|
||||
.withArgs({ type: ServiceWorkerActionType.GET_IDENTITIES })
|
||||
.returns(Promise.resolve(identities));
|
||||
|
||||
postWorkMessageStub
|
||||
.withArgs({ type: ServiceWorkerActionType.GET_IDENTITY_STATUS })
|
||||
.returns(Promise.resolve({ unlocked: true, currentIdentity: identities[0]}));
|
||||
postWorkMessageStub
|
||||
.withArgs({ type: ServiceWorkerActionType.GET_IDENTITY_STATUS })
|
||||
.returns(Promise.resolve({ unlocked: true, currentIdentity: identities[0] }));
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
const id = await store.dispatch(syncWorker());
|
||||
expect(id).toStrictEqual(identities[0]);
|
||||
postWorkMessageStub.reset();
|
||||
});
|
||||
});
|
||||
// @ts-ignore
|
||||
const id = await store.dispatch(syncWorker());
|
||||
expect(id).toStrictEqual(identities[0]);
|
||||
postWorkMessageStub.reset();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,176 +1,180 @@
|
||||
import {Identity} from "../serviceWorkers/identity";
|
||||
import {Dispatch} from "redux";
|
||||
import {postWorkerMessage} from "../util/sw";
|
||||
import {getIdentities, getIdentityStatus} from "../serviceWorkers/util";
|
||||
import {useSelector} from "react-redux";
|
||||
import {AppRootState} from "../store/configureAppStore";
|
||||
import deepEqual from "fast-deep-equal";
|
||||
import { Identity } from '../serviceWorkers/identity';
|
||||
import { Dispatch } from 'redux';
|
||||
import { postWorkerMessage } from '../util/sw';
|
||||
import { getIdentities, getIdentityStatus } from '../serviceWorkers/util';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppRootState } from '../store/configureAppStore';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
export enum ActionType {
|
||||
SET_SELECTED_ID = 'worker/setSelectedId',
|
||||
SET_IDENTITIES = 'worker/setIdentities',
|
||||
SET_UNLOCKED = 'worker/setUnlocked',
|
||||
SET_POSTING_GROUP = 'worker/setPostingGroup',
|
||||
SET_SELECTED_ID = 'worker/setSelectedId',
|
||||
SET_IDENTITIES = 'worker/setIdentities',
|
||||
SET_UNLOCKED = 'worker/setUnlocked',
|
||||
SET_POSTING_GROUP = 'worker/setPostingGroup',
|
||||
}
|
||||
|
||||
export type Action<payload> = {
|
||||
type: ActionType;
|
||||
payload?: payload;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
}
|
||||
type: ActionType;
|
||||
payload?: payload;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
export type State = {
|
||||
unlocked: boolean;
|
||||
selected: Identity | null;
|
||||
identities: Identity[];
|
||||
postingGroup: string;
|
||||
}
|
||||
unlocked: boolean;
|
||||
selected: Identity | null;
|
||||
identities: Identity[];
|
||||
postingGroup: string;
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
unlocked: false,
|
||||
selected: null,
|
||||
identities: [],
|
||||
postingGroup: '',
|
||||
unlocked: false,
|
||||
selected: null,
|
||||
identities: [],
|
||||
postingGroup: '',
|
||||
};
|
||||
|
||||
export const syncWorker = () => async (dispatch: Dispatch) => {
|
||||
const identities = await postWorkerMessage<Identity[]>(getIdentities());
|
||||
const {unlocked, currentIdentity} = await postWorkerMessage<{
|
||||
unlocked: boolean;
|
||||
currentIdentity: Identity | null;
|
||||
}>(getIdentityStatus());
|
||||
const identities = await postWorkerMessage<Identity[]>(getIdentities());
|
||||
const { unlocked, currentIdentity } = await postWorkerMessage<{
|
||||
unlocked: boolean;
|
||||
currentIdentity: Identity | null;
|
||||
}>(getIdentityStatus());
|
||||
|
||||
dispatch(setIdentities(identities));
|
||||
dispatch(setUnlocked(unlocked));
|
||||
dispatch(setIdentities(identities));
|
||||
dispatch(setUnlocked(unlocked));
|
||||
|
||||
if (currentIdentity) {
|
||||
if (currentIdentity.type === 'gun' && !currentIdentity.privateKey) return;
|
||||
if (currentIdentity.type === 'interrep' && !currentIdentity.serializedIdentity) return;
|
||||
dispatch(setSelectedId(currentIdentity));
|
||||
return currentIdentity;
|
||||
}
|
||||
}
|
||||
if (currentIdentity) {
|
||||
if (currentIdentity.type === 'gun' && !currentIdentity.privateKey) return;
|
||||
if (currentIdentity.type === 'interrep' && !currentIdentity.serializedIdentity) return;
|
||||
dispatch(setSelectedId(currentIdentity));
|
||||
return currentIdentity;
|
||||
}
|
||||
};
|
||||
|
||||
export const setIdentities = (identities: Identity[]): Action<Identity[]> => ({
|
||||
type: ActionType.SET_IDENTITIES,
|
||||
payload: identities,
|
||||
type: ActionType.SET_IDENTITIES,
|
||||
payload: identities,
|
||||
});
|
||||
|
||||
export const setUnlocked = (unlocked: boolean): Action<boolean> => ({
|
||||
type: ActionType.SET_UNLOCKED,
|
||||
payload: unlocked,
|
||||
type: ActionType.SET_UNLOCKED,
|
||||
payload: unlocked,
|
||||
});
|
||||
|
||||
export const setSelectedId = (id: Identity | null): Action<Identity | null> => ({
|
||||
type: ActionType.SET_SELECTED_ID,
|
||||
payload: id,
|
||||
type: ActionType.SET_SELECTED_ID,
|
||||
payload: id,
|
||||
});
|
||||
|
||||
export const setPostingGroup = (id: string): Action<string> => ({
|
||||
type: ActionType.SET_POSTING_GROUP,
|
||||
payload: id,
|
||||
type: ActionType.SET_POSTING_GROUP,
|
||||
payload: id,
|
||||
});
|
||||
|
||||
export default function worker(state = initialState, action: Action<any>): State {
|
||||
switch (action.type) {
|
||||
case ActionType.SET_IDENTITIES:
|
||||
return {
|
||||
...state,
|
||||
identities: action.payload,
|
||||
};
|
||||
case ActionType.SET_SELECTED_ID:
|
||||
return {
|
||||
...state,
|
||||
selected: action.payload,
|
||||
postingGroup: '',
|
||||
};
|
||||
case ActionType.SET_UNLOCKED:
|
||||
return {
|
||||
...state,
|
||||
unlocked: action.payload,
|
||||
};
|
||||
case ActionType.SET_POSTING_GROUP:
|
||||
return {
|
||||
...state,
|
||||
postingGroup: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
switch (action.type) {
|
||||
case ActionType.SET_IDENTITIES:
|
||||
return {
|
||||
...state,
|
||||
identities: action.payload,
|
||||
};
|
||||
case ActionType.SET_SELECTED_ID:
|
||||
return {
|
||||
...state,
|
||||
selected: action.payload,
|
||||
postingGroup: '',
|
||||
};
|
||||
case ActionType.SET_UNLOCKED:
|
||||
return {
|
||||
...state,
|
||||
unlocked: action.payload,
|
||||
};
|
||||
case ActionType.SET_POSTING_GROUP:
|
||||
return {
|
||||
...state,
|
||||
postingGroup: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const useIdentities = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.worker.identities;
|
||||
}, deepEqual);
|
||||
}
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.worker.identities;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useWorkerUnlocked = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.worker.unlocked;
|
||||
}, deepEqual);
|
||||
}
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.worker.unlocked;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const usePostingGroup = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.worker.postingGroup;
|
||||
}, deepEqual);
|
||||
}
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.worker.postingGroup;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useSelectedLocalId = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
const {
|
||||
worker: { selected },
|
||||
} = state;
|
||||
return useSelector((state: AppRootState) => {
|
||||
const {
|
||||
worker: { selected },
|
||||
} = state;
|
||||
|
||||
if (selected) {
|
||||
return selected;
|
||||
}
|
||||
if (selected) {
|
||||
return selected;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, deepEqual);
|
||||
}
|
||||
return null;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const getZKGroupFromIdentity = (id: Identity) => {
|
||||
if (id?.type !== 'interrep' && id?.type !== 'zkpr_interrep' && id?.type !== 'taz') {
|
||||
return null;
|
||||
}
|
||||
if (id?.type !== 'interrep' && id?.type !== 'zkpr_interrep' && id?.type !== 'taz') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (id?.type === 'taz') {
|
||||
return 'semaphore_taz_members';
|
||||
}
|
||||
if (id?.type === 'taz') {
|
||||
return 'semaphore_taz_members';
|
||||
}
|
||||
|
||||
return `interrep_${id.provider.toLowerCase()}_${id.name.toLowerCase()}`;
|
||||
}
|
||||
return `interrep_${id.provider.toLowerCase()}_${id.name.toLowerCase()}`;
|
||||
};
|
||||
|
||||
export const useSelectedZKGroup = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
const {
|
||||
worker: { selected },
|
||||
} = state;
|
||||
return useSelector((state: AppRootState) => {
|
||||
const {
|
||||
worker: { selected },
|
||||
} = state;
|
||||
|
||||
if (selected?.type === 'taz') return 'semaphore_taz_members';
|
||||
if (selected?.type === 'taz') return 'semaphore_taz_members';
|
||||
|
||||
if (selected?.type !== 'interrep' && selected?.type !== 'zkpr_interrep') {
|
||||
return null;
|
||||
}
|
||||
if (selected?.type !== 'interrep' && selected?.type !== 'zkpr_interrep') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!selected.provider || !selected.name) return '';
|
||||
if (!selected.provider || !selected.name) return '';
|
||||
|
||||
return `interrep_${selected.provider.toLowerCase()}_${selected.name.toLowerCase()}`;
|
||||
}, deepEqual);
|
||||
}
|
||||
return `interrep_${selected.provider.toLowerCase()}_${selected.name.toLowerCase()}`;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useHasIdConnected = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
const { worker: { selected } } = state;
|
||||
const { web3: { account } } = state;
|
||||
return useSelector((state: AppRootState) => {
|
||||
const {
|
||||
worker: { selected },
|
||||
} = state;
|
||||
const {
|
||||
web3: { account },
|
||||
} = state;
|
||||
|
||||
if (selected?.address === account) {
|
||||
return true;
|
||||
}
|
||||
if (selected?.address === account) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, deepEqual);
|
||||
}
|
||||
return false;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
@@ -1,51 +1,49 @@
|
||||
import sinon from "sinon";
|
||||
import {store, ducks, fetchStub, zkprStub, postWorkMessageStub} from "../util/testUtils";
|
||||
import sinon from 'sinon';
|
||||
import { store, ducks, fetchStub, zkprStub, postWorkMessageStub } from '../util/testUtils';
|
||||
const {
|
||||
zkpr: {
|
||||
connectZKPR,
|
||||
createZKPRIdentity,
|
||||
maybeSetZKPRIdentity,
|
||||
},
|
||||
zkpr: { connectZKPR, createZKPRIdentity, maybeSetZKPRIdentity },
|
||||
} = ducks;
|
||||
|
||||
describe('ZKPR duck', () => {
|
||||
it('should connect to zkpr', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: {
|
||||
name: 'Twitter',
|
||||
provider: 'diamond',
|
||||
data: {
|
||||
siblings: ['0x1', '0x2'],
|
||||
pathIndices: [0, 2],
|
||||
root: ['0x3'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}))
|
||||
// @ts-ignore
|
||||
const id = await store.dispatch(connectZKPR());
|
||||
expect(id).toStrictEqual({
|
||||
type: 'zkpr_interrep',
|
||||
provider: 'diamond',
|
||||
it('should connect to zkpr', async () => {
|
||||
// @ts-ignore
|
||||
fetchStub.returns(
|
||||
Promise.resolve({
|
||||
json: async () => ({
|
||||
payload: {
|
||||
name: 'Twitter',
|
||||
identityPath: { path_elements: [ '1', '2' ], path_index: [ 0, 2 ], root: '3' },
|
||||
identityCommitment: '291'
|
||||
});
|
||||
|
||||
const onLogout = zkprStub.on.args[0][1];
|
||||
const onIdentityChanged = zkprStub.on.args[1][1];
|
||||
|
||||
expect(store.getState().zkpr.zkpr).toBeTruthy();
|
||||
|
||||
onIdentityChanged('45678');
|
||||
expect(store.getState().zkpr.idCommitment).toBe('284280');
|
||||
|
||||
onLogout();
|
||||
expect(store.getState().zkpr.zkpr).toBeFalsy();
|
||||
|
||||
fetchStub.reset();
|
||||
postWorkMessageStub.reset();
|
||||
provider: 'diamond',
|
||||
data: {
|
||||
siblings: ['0x1', '0x2'],
|
||||
pathIndices: [0, 2],
|
||||
root: ['0x3'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
const id = await store.dispatch(connectZKPR());
|
||||
expect(id).toStrictEqual({
|
||||
type: 'zkpr_interrep',
|
||||
provider: 'diamond',
|
||||
name: 'Twitter',
|
||||
identityPath: { path_elements: ['1', '2'], path_index: [0, 2], root: '3' },
|
||||
identityCommitment: '291',
|
||||
});
|
||||
|
||||
const onLogout = zkprStub.on.args[0][1];
|
||||
const onIdentityChanged = zkprStub.on.args[1][1];
|
||||
|
||||
expect(store.getState().zkpr.zkpr).toBeTruthy();
|
||||
|
||||
onIdentityChanged('45678');
|
||||
expect(store.getState().zkpr.idCommitment).toBe('284280');
|
||||
|
||||
onLogout();
|
||||
expect(store.getState().zkpr.zkpr).toBeFalsy();
|
||||
|
||||
fetchStub.reset();
|
||||
postWorkMessageStub.reset();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,284 +1,301 @@
|
||||
import {ThunkDispatch} from "redux-thunk";
|
||||
import {useSelector} from "react-redux";
|
||||
import deepEqual from "fast-deep-equal";
|
||||
import {AppRootState} from "../store/configureAppStore";
|
||||
import {checkPath} from "../util/interrep";
|
||||
import {Identity} from "../serviceWorkers/identity";
|
||||
import {postWorkerMessage} from "../util/sw";
|
||||
import {selectIdentity, setIdentity} from "../serviceWorkers/util";
|
||||
import {Dispatch} from "redux";
|
||||
import {SemaphoreFullProof, SemaphoreSolidityProof, MerkleProof, RLNFullProof} from "@zk-kit/protocols";
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { useSelector } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { AppRootState } from '../store/configureAppStore';
|
||||
import { checkPath } from '../util/interrep';
|
||||
import { Identity } from '../serviceWorkers/identity';
|
||||
import { postWorkerMessage } from '../util/sw';
|
||||
import { selectIdentity, setIdentity } from '../serviceWorkers/util';
|
||||
import { Dispatch } from 'redux';
|
||||
import {
|
||||
SemaphoreFullProof,
|
||||
SemaphoreSolidityProof,
|
||||
MerkleProof,
|
||||
RLNFullProof,
|
||||
} from '@zk-kit/protocols';
|
||||
|
||||
enum ActionTypes {
|
||||
SET_LOADING = 'zkpr/setLoading',
|
||||
SET_UNLOCKING = 'zkpr/setUnlocking',
|
||||
SET_ID_COMMIMENT = 'zkpr/setIdCommitment',
|
||||
SET_ZKPR = 'zkpr/setZKPR',
|
||||
SET_LOADING = 'zkpr/setLoading',
|
||||
SET_UNLOCKING = 'zkpr/setUnlocking',
|
||||
SET_ID_COMMIMENT = 'zkpr/setIdCommitment',
|
||||
SET_ZKPR = 'zkpr/setZKPR',
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
type: ActionTypes;
|
||||
payload?: payload;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
type State = {
|
||||
loading: boolean;
|
||||
unlocking: boolean;
|
||||
zkpr: ZKPR | null;
|
||||
idCommitment: string;
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
zkpr: null,
|
||||
idCommitment: '',
|
||||
loading: false,
|
||||
unlocking: false,
|
||||
type: ActionTypes;
|
||||
payload?: payload;
|
||||
meta?: any;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
export const connectZKPR = () => async (
|
||||
dispatch: ThunkDispatch<any, any, any>,
|
||||
getState: () => AppRootState,
|
||||
) => {
|
||||
type State = {
|
||||
loading: boolean;
|
||||
unlocking: boolean;
|
||||
zkpr: ZKPR | null;
|
||||
idCommitment: string;
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
zkpr: null,
|
||||
idCommitment: '',
|
||||
loading: false,
|
||||
unlocking: false,
|
||||
};
|
||||
|
||||
export const connectZKPR =
|
||||
() => async (dispatch: ThunkDispatch<any, any, any>, getState: () => AppRootState) => {
|
||||
dispatch(setLoading(true));
|
||||
|
||||
try {
|
||||
let id: Identity | null = null;
|
||||
let id: Identity | null = null;
|
||||
|
||||
// @ts-ignore
|
||||
if (typeof window.zkpr !== 'undefined') {
|
||||
// @ts-ignore
|
||||
if (typeof window.zkpr !== 'undefined') {
|
||||
// @ts-ignore
|
||||
const zkpr: any = window.zkpr;
|
||||
const client = await zkpr.connect();
|
||||
const zkprClient = new ZKPR(client);
|
||||
const zkpr: any = window.zkpr;
|
||||
const client = await zkpr.connect();
|
||||
const zkprClient = new ZKPR(client);
|
||||
|
||||
zkprClient.on('logout', async data => {
|
||||
const {
|
||||
worker: { selected, identities },
|
||||
} = getState();
|
||||
zkprClient.on('logout', async data => {
|
||||
const {
|
||||
worker: { selected, identities },
|
||||
} = getState();
|
||||
|
||||
dispatch(disconnectZKPR());
|
||||
dispatch(disconnectZKPR());
|
||||
|
||||
const [defaultId] = identities;
|
||||
if (defaultId) {
|
||||
postWorkerMessage(selectIdentity(defaultId.type === 'gun' ? defaultId.publicKey : defaultId.identityCommitment));
|
||||
} else {
|
||||
postWorkerMessage(setIdentity(null));
|
||||
}
|
||||
});
|
||||
const [defaultId] = identities;
|
||||
if (defaultId) {
|
||||
postWorkerMessage(
|
||||
selectIdentity(
|
||||
defaultId.type === 'gun' ? defaultId.publicKey : defaultId.identityCommitment
|
||||
)
|
||||
);
|
||||
} else {
|
||||
postWorkerMessage(setIdentity(null));
|
||||
}
|
||||
});
|
||||
|
||||
zkprClient.on('identityChanged', async data => {
|
||||
const idCommitment = data && BigInt('0x' + data).toString();
|
||||
const { worker: { identities } } = getState();
|
||||
zkprClient.on('identityChanged', async data => {
|
||||
const idCommitment = data && BigInt('0x' + data).toString();
|
||||
const {
|
||||
worker: { identities },
|
||||
} = getState();
|
||||
|
||||
dispatch(setIdCommitment(''));
|
||||
dispatch(setIdCommitment(''));
|
||||
|
||||
if (idCommitment) {
|
||||
dispatch(setIdCommitment(idCommitment));
|
||||
const id: any = await maybeSetZKPRIdentity(idCommitment);
|
||||
if (!id) {
|
||||
const [defaultId] = identities;
|
||||
if (defaultId) {
|
||||
postWorkerMessage(selectIdentity(defaultId.type === 'gun' ? defaultId.publicKey : defaultId.identityCommitment));
|
||||
} else {
|
||||
postWorkerMessage(setIdentity(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem('ZKPR_CACHED', '1');
|
||||
|
||||
const idCommitmentHex = await zkprClient.getActiveIdentity();
|
||||
const idCommitment = idCommitmentHex && BigInt('0x' + idCommitmentHex).toString();
|
||||
|
||||
if (idCommitment) {
|
||||
dispatch(setIdCommitment(idCommitment));
|
||||
id = await maybeSetZKPRIdentity(idCommitment);
|
||||
if (idCommitment) {
|
||||
dispatch(setIdCommitment(idCommitment));
|
||||
const id: any = await maybeSetZKPRIdentity(idCommitment);
|
||||
if (!id) {
|
||||
const [defaultId] = identities;
|
||||
if (defaultId) {
|
||||
postWorkerMessage(
|
||||
selectIdentity(
|
||||
defaultId.type === 'gun' ? defaultId.publicKey : defaultId.identityCommitment
|
||||
)
|
||||
);
|
||||
} else {
|
||||
postWorkerMessage(setIdentity(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(setZKPR(zkprClient));
|
||||
localStorage.setItem('ZKPR_CACHED', '1');
|
||||
|
||||
const idCommitmentHex = await zkprClient.getActiveIdentity();
|
||||
const idCommitment = idCommitmentHex && BigInt('0x' + idCommitmentHex).toString();
|
||||
|
||||
if (idCommitment) {
|
||||
dispatch(setIdCommitment(idCommitment));
|
||||
id = await maybeSetZKPRIdentity(idCommitment);
|
||||
}
|
||||
|
||||
dispatch(setLoading(false));
|
||||
dispatch(setZKPR(zkprClient));
|
||||
}
|
||||
|
||||
return id;
|
||||
dispatch(setLoading(false));
|
||||
|
||||
return id;
|
||||
} catch (e) {
|
||||
dispatch(setLoading(false));
|
||||
throw e;
|
||||
dispatch(setLoading(false));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createZKPRIdentity = () => async (
|
||||
dispatch: ThunkDispatch<any, any, any>,
|
||||
getState: () => AppRootState,
|
||||
) => {
|
||||
const { zkpr: { zkpr } } = getState();
|
||||
export const createZKPRIdentity =
|
||||
() => async (dispatch: ThunkDispatch<any, any, any>, getState: () => AppRootState) => {
|
||||
const {
|
||||
zkpr: { zkpr },
|
||||
} = getState();
|
||||
if (zkpr) {
|
||||
return zkpr.createIdentity();
|
||||
return zkpr.createIdentity();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function maybeSetZKPRIdentity(idCommitment: string) {
|
||||
let id: Identity | null = null;
|
||||
const data = await checkPath(idCommitment);
|
||||
let id: Identity | null = null;
|
||||
const data = await checkPath(idCommitment);
|
||||
|
||||
if (data) {
|
||||
id = {
|
||||
type: 'zkpr_interrep',
|
||||
provider: data.provider,
|
||||
name: data.name,
|
||||
identityPath: {
|
||||
path_elements: data.path.path_elements.map(d => d.toString()),
|
||||
path_index: data.path.path_index,
|
||||
root: data.path.root.toString(),
|
||||
},
|
||||
identityCommitment: idCommitment,
|
||||
}
|
||||
if (data) {
|
||||
id = {
|
||||
type: 'zkpr_interrep',
|
||||
provider: data.provider,
|
||||
name: data.name,
|
||||
identityPath: {
|
||||
path_elements: data.path.path_elements.map(d => d.toString()),
|
||||
path_index: data.path.path_index,
|
||||
root: data.path.root.toString(),
|
||||
},
|
||||
identityCommitment: idCommitment,
|
||||
};
|
||||
|
||||
await postWorkerMessage(setIdentity(id));
|
||||
}
|
||||
await postWorkerMessage(setIdentity(id));
|
||||
}
|
||||
|
||||
return id;
|
||||
return id;
|
||||
}
|
||||
|
||||
export const disconnectZKPR = () => (dispatch: Dispatch) => {
|
||||
localStorage.setItem('ZKPR_CACHED', '');
|
||||
dispatch(setZKPR(null));
|
||||
dispatch(setIdCommitment(''));
|
||||
}
|
||||
localStorage.setItem('ZKPR_CACHED', '');
|
||||
dispatch(setZKPR(null));
|
||||
dispatch(setIdCommitment(''));
|
||||
};
|
||||
|
||||
export const setUnlocking = (unlocking: boolean) => ({
|
||||
type: ActionTypes.SET_UNLOCKING,
|
||||
payload: unlocking,
|
||||
type: ActionTypes.SET_UNLOCKING,
|
||||
payload: unlocking,
|
||||
});
|
||||
|
||||
export const setIdCommitment = (idCommitment: string) => ({
|
||||
type: ActionTypes.SET_ID_COMMIMENT,
|
||||
payload: idCommitment,
|
||||
type: ActionTypes.SET_ID_COMMIMENT,
|
||||
payload: idCommitment,
|
||||
});
|
||||
|
||||
export const setLoading = (loading: boolean) => ({
|
||||
type: ActionTypes.SET_LOADING,
|
||||
payload: loading,
|
||||
type: ActionTypes.SET_LOADING,
|
||||
payload: loading,
|
||||
});
|
||||
|
||||
export const setZKPR = (zkpr: ZKPR | null) => ({
|
||||
type: ActionTypes.SET_ZKPR,
|
||||
payload: zkpr,
|
||||
type: ActionTypes.SET_ZKPR,
|
||||
payload: zkpr,
|
||||
});
|
||||
|
||||
export default function zkpr(state = initialState, action: Action<any>): State {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_UNLOCKING:
|
||||
return {
|
||||
...state,
|
||||
unlocking: action.payload,
|
||||
};
|
||||
case ActionTypes.SET_LOADING:
|
||||
return {
|
||||
...state,
|
||||
loading: action.payload,
|
||||
};
|
||||
case ActionTypes.SET_ZKPR:
|
||||
return {
|
||||
...state,
|
||||
zkpr: action.payload,
|
||||
};
|
||||
case ActionTypes.SET_ID_COMMIMENT:
|
||||
return {
|
||||
...state,
|
||||
idCommitment: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_UNLOCKING:
|
||||
return {
|
||||
...state,
|
||||
unlocking: action.payload,
|
||||
};
|
||||
case ActionTypes.SET_LOADING:
|
||||
return {
|
||||
...state,
|
||||
loading: action.payload,
|
||||
};
|
||||
case ActionTypes.SET_ZKPR:
|
||||
return {
|
||||
...state,
|
||||
zkpr: action.payload,
|
||||
};
|
||||
case ActionTypes.SET_ID_COMMIMENT:
|
||||
return {
|
||||
...state,
|
||||
idCommitment: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const useZKPRLoading = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.zkpr.loading;
|
||||
}, deepEqual);
|
||||
}
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.zkpr.loading;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useZKPR = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.zkpr.zkpr;
|
||||
}, deepEqual);
|
||||
}
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.zkpr.zkpr;
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export const useIdCommitment = () => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return BigInt(state.zkpr.idCommitment).toString(16);
|
||||
}, deepEqual);
|
||||
}
|
||||
return useSelector((state: AppRootState) => {
|
||||
return BigInt(state.zkpr.idCommitment).toString(16);
|
||||
}, deepEqual);
|
||||
};
|
||||
|
||||
export class ZKPR {
|
||||
private client: any;
|
||||
private client: any;
|
||||
|
||||
constructor(client: any) {
|
||||
this.client = client;
|
||||
}
|
||||
constructor(client: any) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
on(eventName: string, cb: (data: unknown) => void) {
|
||||
return this.client.on(eventName, cb);
|
||||
}
|
||||
on(eventName: string, cb: (data: unknown) => void) {
|
||||
return this.client.on(eventName, cb);
|
||||
}
|
||||
|
||||
async getActiveIdentity(): Promise<string | null> {
|
||||
return this.client.getActiveIdentity();
|
||||
}
|
||||
async getActiveIdentity(): Promise<string | null> {
|
||||
return this.client.getActiveIdentity();
|
||||
}
|
||||
|
||||
async createIdentity(): Promise<void> {
|
||||
return this.client.createIdentity();
|
||||
}
|
||||
async createIdentity(): Promise<void> {
|
||||
return this.client.createIdentity();
|
||||
}
|
||||
|
||||
async semaphoreProof(
|
||||
externalNullifier: string,
|
||||
signal: string,
|
||||
circuitFilePath: string,
|
||||
zkeyFilePath: string,
|
||||
merkleProofArtifactsOrStorageAddress: string | {
|
||||
leaves: string[];
|
||||
depth: number;
|
||||
leavesPerNode: number;
|
||||
async semaphoreProof(
|
||||
externalNullifier: string,
|
||||
signal: string,
|
||||
circuitFilePath: string,
|
||||
zkeyFilePath: string,
|
||||
merkleProofArtifactsOrStorageAddress:
|
||||
| string
|
||||
| {
|
||||
leaves: string[];
|
||||
depth: number;
|
||||
leavesPerNode: number;
|
||||
},
|
||||
merkleProof?: MerkleProof,
|
||||
): Promise<{
|
||||
fullProof: SemaphoreFullProof
|
||||
solidityProof: SemaphoreSolidityProof
|
||||
}> {
|
||||
return this.client.semaphoreProof(
|
||||
externalNullifier,
|
||||
signal,
|
||||
circuitFilePath,
|
||||
zkeyFilePath,
|
||||
merkleProofArtifactsOrStorageAddress,
|
||||
merkleProof,
|
||||
);
|
||||
}
|
||||
merkleProof?: MerkleProof
|
||||
): Promise<{
|
||||
fullProof: SemaphoreFullProof;
|
||||
solidityProof: SemaphoreSolidityProof;
|
||||
}> {
|
||||
return this.client.semaphoreProof(
|
||||
externalNullifier,
|
||||
signal,
|
||||
circuitFilePath,
|
||||
zkeyFilePath,
|
||||
merkleProofArtifactsOrStorageAddress,
|
||||
merkleProof
|
||||
);
|
||||
}
|
||||
|
||||
async rlnProof(
|
||||
externalNullifier: string,
|
||||
signal: string,
|
||||
circuitFilePath: string,
|
||||
zkeyFilePath: string,
|
||||
merkleProofArtifactsOrStorageAddress: string | {
|
||||
leaves: string[];
|
||||
depth: number;
|
||||
leavesPerNode: number;
|
||||
async rlnProof(
|
||||
externalNullifier: string,
|
||||
signal: string,
|
||||
circuitFilePath: string,
|
||||
zkeyFilePath: string,
|
||||
merkleProofArtifactsOrStorageAddress:
|
||||
| string
|
||||
| {
|
||||
leaves: string[];
|
||||
depth: number;
|
||||
leavesPerNode: number;
|
||||
},
|
||||
rlnIdentifier: string,
|
||||
merkleProof?: MerkleProof,
|
||||
): Promise<RLNFullProof> {
|
||||
return this.client.rlnProof(
|
||||
externalNullifier,
|
||||
signal,
|
||||
circuitFilePath,
|
||||
zkeyFilePath,
|
||||
merkleProofArtifactsOrStorageAddress,
|
||||
rlnIdentifier,
|
||||
merkleProof,
|
||||
);
|
||||
}
|
||||
}
|
||||
rlnIdentifier: string,
|
||||
merkleProof?: MerkleProof
|
||||
): Promise<RLNFullProof> {
|
||||
return this.client.rlnProof(
|
||||
externalNullifier,
|
||||
signal,
|
||||
circuitFilePath,
|
||||
zkeyFilePath,
|
||||
merkleProofArtifactsOrStorageAddress,
|
||||
rlnIdentifier,
|
||||
merkleProof
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
@import "../../util/variable";
|
||||
@import '../../util/variable';
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
|
||||
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
#root, body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
#root,
|
||||
body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
cursor: default;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@@ -62,7 +63,7 @@ ul {
|
||||
|
||||
li {
|
||||
margin-left: 2rem;
|
||||
padding-left: .5rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -79,4 +80,4 @@ a {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import React from "react";
|
||||
import App from "./index";
|
||||
import ReactDOM from "react-dom";
|
||||
import {Provider} from "react-redux";
|
||||
import {BrowserRouter} from "react-router-dom";
|
||||
import sinon from "sinon";
|
||||
import {store} from "../../util/testUtils";
|
||||
import React from 'react';
|
||||
import App from './index';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import sinon from 'sinon';
|
||||
import { store } from '../../util/testUtils';
|
||||
|
||||
// @ts-ignore
|
||||
navigator.serviceWorker = {
|
||||
addEventListener: sinon.stub(),
|
||||
addEventListener: sinon.stub(),
|
||||
};
|
||||
|
||||
test('<App> - should mount', async () => {
|
||||
const root = document.getElementById('root');
|
||||
const root = document.getElementById('root');
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
root,
|
||||
);
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
root
|
||||
);
|
||||
|
||||
expect(root!.innerHTML).toBeTruthy();
|
||||
expect(root!.innerHTML).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -1,221 +1,226 @@
|
||||
import React, {ReactElement, useCallback, useContext, useEffect, useState} from "react";
|
||||
import {Redirect, Route, RouteProps, Switch, useHistory, useLocation} from "react-router";
|
||||
import TopNav from "../../components/TopNav";
|
||||
import GlobalFeed from "../GlobalFeed";
|
||||
import "./app.scss";
|
||||
import {connectWeb3, useGunLoggedIn, web3Modal} from "../../ducks/web3";
|
||||
import {useDispatch} from "react-redux";
|
||||
import PostView from "../PostView";
|
||||
import ProfileView from "../ProfileView";
|
||||
import HomeFeed from "../HomeFeed";
|
||||
import TagFeed from "../../components/TagFeed";
|
||||
import SignupView, {ViewType} from "../SignupView";
|
||||
import {syncWorker, useSelectedLocalId} from "../../ducks/worker";
|
||||
import BottomNav from "../../components/BottomNav";
|
||||
import InterrepOnboarding from "../InterrepOnboarding";
|
||||
import ConnectTwitterView from "../ConnectTwitterView";
|
||||
import {loginUser} from "../../util/user";
|
||||
import {connectZKPR} from "../../ducks/zkpr";
|
||||
import SettingView from "../SettingView";
|
||||
import MetaPanel from "../../components/MetaPanel";
|
||||
import ChatView from "../ChatView";
|
||||
import {zkchat} from "../../ducks/chats";
|
||||
import {generateECDHKeyPairFromhex, generateZkIdentityFromHex, sha256, signWithP256} from "../../util/crypto";
|
||||
import {Strategy, ZkIdentity} from "@zk-kit/identity";
|
||||
import sse from "../../util/sse";
|
||||
import ThemeContext from "../../components/ThemeContext";
|
||||
import classNames from "classnames";
|
||||
import {Identity} from "@semaphore-protocol/identity";
|
||||
import TazModal from "../../components/TazModal";
|
||||
import React, { ReactElement, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { Redirect, Route, RouteProps, Switch, useHistory, useLocation } from 'react-router';
|
||||
import TopNav from '../../components/TopNav';
|
||||
import GlobalFeed from '../GlobalFeed';
|
||||
import './app.scss';
|
||||
import { connectWeb3, useGunLoggedIn, web3Modal } from '../../ducks/web3';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import PostView from '../PostView';
|
||||
import ProfileView from '../ProfileView';
|
||||
import HomeFeed from '../HomeFeed';
|
||||
import TagFeed from '../../components/TagFeed';
|
||||
import SignupView, { ViewType } from '../SignupView';
|
||||
import { syncWorker, useSelectedLocalId } from '../../ducks/worker';
|
||||
import BottomNav from '../../components/BottomNav';
|
||||
import InterrepOnboarding from '../InterrepOnboarding';
|
||||
import ConnectTwitterView from '../ConnectTwitterView';
|
||||
import { loginUser } from '../../util/user';
|
||||
import { connectZKPR } from '../../ducks/zkpr';
|
||||
import SettingView from '../SettingView';
|
||||
import MetaPanel from '../../components/MetaPanel';
|
||||
import ChatView from '../ChatView';
|
||||
import { zkchat } from '../../ducks/chats';
|
||||
import {
|
||||
generateECDHKeyPairFromhex,
|
||||
generateZkIdentityFromHex,
|
||||
sha256,
|
||||
signWithP256,
|
||||
} from '../../util/crypto';
|
||||
import { Strategy, ZkIdentity } from '@zk-kit/identity';
|
||||
import sse from '../../util/sse';
|
||||
import ThemeContext from '../../components/ThemeContext';
|
||||
import classNames from 'classnames';
|
||||
import { Identity } from '@semaphore-protocol/identity';
|
||||
import TazModal from '../../components/TazModal';
|
||||
|
||||
export default function App(): ReactElement {
|
||||
const dispatch = useDispatch();
|
||||
const selected = useSelectedLocalId();
|
||||
const theme = useContext(ThemeContext);
|
||||
const loc = useLocation();
|
||||
const history = useHistory();
|
||||
const [tazIdentity, setTazIdentity] = useState<string[]|null>(null);
|
||||
const [showingTazModal, showTazModal] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const selected = useSelectedLocalId();
|
||||
const theme = useContext(ThemeContext);
|
||||
const loc = useLocation();
|
||||
const history = useHistory();
|
||||
const [tazIdentity, setTazIdentity] = useState<string[] | null>(null);
|
||||
const [showingTazModal, showTazModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (loc.pathname === '/taz/') {
|
||||
const [nullifer, trapdoor] = loc.hash.slice(1).split('_');
|
||||
history.push('/');
|
||||
setTazIdentity([nullifer, trapdoor]);
|
||||
showTazModal(true);
|
||||
}
|
||||
}, [loc]);
|
||||
useEffect(() => {
|
||||
if (loc.pathname === '/taz/') {
|
||||
const [nullifer, trapdoor] = loc.hash.slice(1).split('_');
|
||||
history.push('/');
|
||||
setTazIdentity([nullifer, trapdoor]);
|
||||
showTazModal(true);
|
||||
}
|
||||
}, [loc]);
|
||||
|
||||
useEffect(() => {
|
||||
const style = document.createElement('style');
|
||||
style.innerText = `
|
||||
useEffect(() => {
|
||||
const style = document.createElement('style');
|
||||
style.innerText = `
|
||||
:root {
|
||||
color-scheme: ${theme};
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style);
|
||||
}, [theme]);
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
(async function onAppMount() {
|
||||
useEffect(() => {
|
||||
(async function onAppMount() {
|
||||
const id: any = await dispatch(syncWorker());
|
||||
|
||||
const id: any = await dispatch(syncWorker());
|
||||
if (id) {
|
||||
await loginUser(id);
|
||||
}
|
||||
|
||||
if (id) {
|
||||
await loginUser(id);
|
||||
}
|
||||
const cachedZKPR = localStorage.getItem('ZKPR_CACHED');
|
||||
|
||||
const cachedZKPR = localStorage.getItem('ZKPR_CACHED');
|
||||
if (cachedZKPR) {
|
||||
await dispatch(connectZKPR());
|
||||
} else if (web3Modal.cachedProvider) {
|
||||
await dispatch(connectWeb3());
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (cachedZKPR) {
|
||||
await dispatch(connectZKPR());
|
||||
} else if (web3Modal.cachedProvider) {
|
||||
await dispatch(connectWeb3());
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected?.type === 'gun') {
|
||||
(async () => {
|
||||
const ecdhseed = await signWithP256(selected.privateKey, 'signing for ecdh - 0');
|
||||
const zkseed = await signWithP256(selected.privateKey, 'signing for zk identity - 0');
|
||||
const ecdhHex = await sha256(ecdhseed);
|
||||
const zkHex = await sha256(zkseed);
|
||||
const keyPair = await generateECDHKeyPairFromhex(ecdhHex);
|
||||
const zkIdentity = await generateZkIdentityFromHex(zkHex);
|
||||
await sse.updateTopics([`ecdh:${keyPair.pub}`]);
|
||||
await zkchat.importIdentity({
|
||||
address: selected.address,
|
||||
zk: zkIdentity,
|
||||
ecdh: keyPair,
|
||||
});
|
||||
})();
|
||||
} else if (selected?.type === 'interrep') {
|
||||
(async () => {
|
||||
const zkIdentity = new ZkIdentity(Strategy.SERIALIZED, selected.serializedIdentity);
|
||||
const ecdhseed = await sha256(zkIdentity.getSecret().map(d => d.toString()).join());
|
||||
const ecdhHex = await sha256(ecdhseed);
|
||||
const keyPair = await generateECDHKeyPairFromhex(ecdhHex);
|
||||
await zkchat.importIdentity({
|
||||
address: selected?.identityCommitment,
|
||||
zk: zkIdentity,
|
||||
ecdh: keyPair,
|
||||
});
|
||||
})();
|
||||
} else if (selected?.type === 'taz') {
|
||||
(async () => {
|
||||
const zkIdentity = new Identity(selected.serializedIdentity);
|
||||
const ecdhseed = await sha256(selected.serializedIdentity);
|
||||
const ecdhHex = await sha256(ecdhseed);
|
||||
const keyPair = await generateECDHKeyPairFromhex(ecdhHex);
|
||||
await zkchat.importIdentity({
|
||||
address: selected?.identityCommitment,
|
||||
zk: zkIdentity,
|
||||
ecdh: keyPair,
|
||||
});
|
||||
})();
|
||||
}
|
||||
}, [selected])
|
||||
|
||||
useEffect(() => {
|
||||
navigator.serviceWorker.addEventListener('message', event => {
|
||||
const data = event.data;
|
||||
|
||||
if (!data) return;
|
||||
|
||||
if (data.target === 'redux') {
|
||||
const action = data.action;
|
||||
dispatch(action);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (selected?.type === 'gun') {
|
||||
(async () => {
|
||||
const ecdhseed = await signWithP256(selected.privateKey, 'signing for ecdh - 0');
|
||||
const zkseed = await signWithP256(selected.privateKey, 'signing for zk identity - 0');
|
||||
const ecdhHex = await sha256(ecdhseed);
|
||||
const zkHex = await sha256(zkseed);
|
||||
const keyPair = await generateECDHKeyPairFromhex(ecdhHex);
|
||||
const zkIdentity = await generateZkIdentityFromHex(zkHex);
|
||||
await sse.updateTopics([`ecdh:${keyPair.pub}`]);
|
||||
await zkchat.importIdentity({
|
||||
address: selected.address,
|
||||
zk: zkIdentity,
|
||||
ecdh: keyPair,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames("flex flex-col flex-nowrap w-screen h-screen overflow-hidden app", {
|
||||
'dark': theme === 'dark',
|
||||
'light': theme !== 'light',
|
||||
})}
|
||||
>
|
||||
{ showingTazModal && (
|
||||
<TazModal
|
||||
tazIdentity={tazIdentity}
|
||||
onClose={() => showTazModal(false)}
|
||||
/>
|
||||
)}
|
||||
<TopNav />
|
||||
<div className="flex flex-row flex-nowrap app__content">
|
||||
<Switch>
|
||||
<Route path="/explore">
|
||||
<GlobalFeed />
|
||||
</Route>
|
||||
<Route path="/tag/:tagName">
|
||||
<TagFeed />
|
||||
</Route>
|
||||
<Route path="/:name/status/:hash">
|
||||
<PostView />
|
||||
</Route>
|
||||
<Route path="/post/:hash">
|
||||
<PostView />
|
||||
</Route>
|
||||
<AuthRoute path="/home">
|
||||
<HomeFeed />
|
||||
</AuthRoute>
|
||||
<Route path="/notifications" />
|
||||
<Route path="/create-local-backup">
|
||||
<SignupView viewType={ViewType.localBackup} />
|
||||
</Route>
|
||||
<Route path="/onboarding/interrep">
|
||||
<InterrepOnboarding />
|
||||
</Route>
|
||||
<Route path="/signup/interep">
|
||||
<InterrepOnboarding />
|
||||
</Route>
|
||||
<Route path="/connect/twitter">
|
||||
<ConnectTwitterView />
|
||||
</Route>
|
||||
<Route path="/signup">
|
||||
<SignupView />
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
<SettingView />
|
||||
</Route>
|
||||
<Route path="/chat">
|
||||
<ChatView />
|
||||
</Route>
|
||||
<Route path="/taz">
|
||||
<GlobalFeed />
|
||||
</Route>
|
||||
<Route path="/:name">
|
||||
<ProfileView />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Redirect to="/explore" />
|
||||
</Route>
|
||||
</Switch>
|
||||
<MetaPanel className="mobile-hidden" />
|
||||
</div>
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AuthRoute(props: {
|
||||
redirect?: string;
|
||||
} & RouteProps): ReactElement {
|
||||
const { redirect = '/', ...routeProps} = props;
|
||||
const loggedIn = useGunLoggedIn();
|
||||
|
||||
if (loggedIn) {
|
||||
return (
|
||||
<Route {...routeProps} />
|
||||
)
|
||||
})();
|
||||
} else if (selected?.type === 'interrep') {
|
||||
(async () => {
|
||||
const zkIdentity = new ZkIdentity(Strategy.SERIALIZED, selected.serializedIdentity);
|
||||
const ecdhseed = await sha256(
|
||||
zkIdentity
|
||||
.getSecret()
|
||||
.map(d => d.toString())
|
||||
.join()
|
||||
);
|
||||
const ecdhHex = await sha256(ecdhseed);
|
||||
const keyPair = await generateECDHKeyPairFromhex(ecdhHex);
|
||||
await zkchat.importIdentity({
|
||||
address: selected?.identityCommitment,
|
||||
zk: zkIdentity,
|
||||
ecdh: keyPair,
|
||||
});
|
||||
})();
|
||||
} else if (selected?.type === 'taz') {
|
||||
(async () => {
|
||||
const zkIdentity = new Identity(selected.serializedIdentity);
|
||||
const ecdhseed = await sha256(selected.serializedIdentity);
|
||||
const ecdhHex = await sha256(ecdhseed);
|
||||
const keyPair = await generateECDHKeyPairFromhex(ecdhHex);
|
||||
await zkchat.importIdentity({
|
||||
address: selected?.identityCommitment,
|
||||
zk: zkIdentity,
|
||||
ecdh: keyPair,
|
||||
});
|
||||
})();
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
return (
|
||||
<Route {...routeProps}>
|
||||
<Redirect to={redirect} />
|
||||
</Route>
|
||||
)
|
||||
useEffect(() => {
|
||||
navigator.serviceWorker.addEventListener('message', event => {
|
||||
const data = event.data;
|
||||
|
||||
if (!data) return;
|
||||
|
||||
if (data.target === 'redux') {
|
||||
const action = data.action;
|
||||
dispatch(action);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('flex flex-col flex-nowrap w-screen h-screen overflow-hidden app', {
|
||||
dark: theme === 'dark',
|
||||
light: theme !== 'light',
|
||||
})}>
|
||||
{showingTazModal && (
|
||||
<TazModal tazIdentity={tazIdentity} onClose={() => showTazModal(false)} />
|
||||
)}
|
||||
<TopNav />
|
||||
<div className="flex flex-row flex-nowrap app__content">
|
||||
<Switch>
|
||||
<Route path="/explore">
|
||||
<GlobalFeed />
|
||||
</Route>
|
||||
<Route path="/tag/:tagName">
|
||||
<TagFeed />
|
||||
</Route>
|
||||
<Route path="/:name/status/:hash">
|
||||
<PostView />
|
||||
</Route>
|
||||
<Route path="/post/:hash">
|
||||
<PostView />
|
||||
</Route>
|
||||
<AuthRoute path="/home">
|
||||
<HomeFeed />
|
||||
</AuthRoute>
|
||||
<Route path="/notifications" />
|
||||
<Route path="/create-local-backup">
|
||||
<SignupView viewType={ViewType.localBackup} />
|
||||
</Route>
|
||||
<Route path="/onboarding/interrep">
|
||||
<InterrepOnboarding />
|
||||
</Route>
|
||||
<Route path="/signup/interep">
|
||||
<InterrepOnboarding />
|
||||
</Route>
|
||||
<Route path="/connect/twitter">
|
||||
<ConnectTwitterView />
|
||||
</Route>
|
||||
<Route path="/signup">
|
||||
<SignupView />
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
<SettingView />
|
||||
</Route>
|
||||
<Route path="/chat">
|
||||
<ChatView />
|
||||
</Route>
|
||||
<Route path="/taz">
|
||||
<GlobalFeed />
|
||||
</Route>
|
||||
<Route path="/:name">
|
||||
<ProfileView />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Redirect to="/explore" />
|
||||
</Route>
|
||||
</Switch>
|
||||
<MetaPanel className="mobile-hidden" />
|
||||
</div>
|
||||
<BottomNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthRoute(
|
||||
props: {
|
||||
redirect?: string;
|
||||
} & RouteProps
|
||||
): ReactElement {
|
||||
const { redirect = '/', ...routeProps } = props;
|
||||
const loggedIn = useGunLoggedIn();
|
||||
|
||||
if (loggedIn) {
|
||||
return <Route {...routeProps} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Route {...routeProps}>
|
||||
<Redirect to={redirect} />
|
||||
</Route>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../util/variable.scss";
|
||||
@import '../../util/variable.scss';
|
||||
|
||||
.dark {
|
||||
.chat-view {
|
||||
@@ -13,4 +13,4 @@
|
||||
border-left: 1px solid $gray-50;
|
||||
border-right: 1px solid $gray-50;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user