--- /dev/null
+fuse.ts
+generate_translations.js
+src/api_tests
package-lock.json
*.orig
+src/shared/translations
+
-import { CSSPlugin, FuseBox, FuseBoxOptions, Sparky } from "fuse-box";\r
-import path = require("path");\r
-import TsTransformClasscat from "ts-transform-classcat";\r
-import TsTransformInferno from "ts-transform-inferno";\r
-/**\r
- * Some of FuseBoxOptions overrides by ts config (module, target, etc)\r
- * https://fuse-box.org/page/working-with-targets\r
- */\r
-let fuse: FuseBox;\r
-const fuseOptions: FuseBoxOptions = {\r
- homeDir: "./src",\r
- output: "dist/$name.js",\r
- sourceMaps: { inline: false, vendor: false },\r
- /**\r
- * Custom TypeScript Transformers (compile Inferno tsx to ts)\r
- */\r
- transformers: {\r
- before: [TsTransformClasscat(), TsTransformInferno()]\r
- }\r
-};\r
-const fuseClientOptions: FuseBoxOptions = {\r
- ...fuseOptions,\r
- plugins: [\r
- /**\r
- * https://fuse-box.org/page/css-resource-plugin\r
- * Compile Sass {SassPlugin()}\r
- * Make .css files modules-like (allow import them like modules) {CSSModules}\r
- * Make .css files modules like and allow import it from node_modules too {CSSResourcePlugin}\r
- * Use them all and bundle with {CSSPlugin}\r
- * */\r
- CSSPlugin()\r
- ]\r
-};\r
-const fuseServerOptions: FuseBoxOptions = {\r
- ...fuseOptions\r
-};\r
-Sparky.task("clean", () => {\r
- /**Clean distribute (dist) folder */\r
- Sparky.src("dist")\r
- .clean("dist")\r
- .exec();\r
-});\r
-Sparky.task("config", () => {\r
- fuse = FuseBox.init(fuseOptions);\r
- fuse.dev();\r
-});\r
-Sparky.task("test", ["&clean", "&config"], () => {\r
- fuse.bundle("client/bundle").test("[**/**.test.tsx]", null);\r
-});\r
-Sparky.task("client", () => {\r
- fuse.opts = fuseClientOptions;\r
- fuse\r
- .bundle("client/bundle")\r
- .target("browser@esnext")\r
- .watch("client/**")\r
- .hmr()\r
- .instructions("> client/index.tsx");\r
-});\r
-Sparky.task("server", () => {\r
- /**Workaround. Should be fixed */\r
- fuse.opts = fuseServerOptions;\r
- fuse\r
- .bundle("server/bundle")\r
- .watch("**")\r
- .target("server@esnext")\r
- .instructions("> [server/index.tsx]")\r
- .completed(proc => {\r
- proc.require({\r
- // tslint:disable-next-line:no-shadowed-variable\r
- close: ({ FuseBox }) => FuseBox.import(FuseBox.mainFile).shutdown()\r
- });\r
- });\r
-});\r
-Sparky.task("dev", ["&clean", "&config", "&client", "&server"], () => {\r
- fuse.run();\r
-});\r
+import { CSSPlugin, FuseBox, FuseBoxOptions, Sparky } from 'fuse-box';
+import path = require('path');
+import TsTransformClasscat from 'ts-transform-classcat';
+import TsTransformInferno from 'ts-transform-inferno';
+/**
+ * Some of FuseBoxOptions overrides by ts config (module, target, etc)
+ * https://fuse-box.org/page/working-with-targets
+ */
+let fuse: FuseBox;
+const fuseOptions: FuseBoxOptions = {
+ homeDir: './src',
+ output: 'dist/$name.js',
+ sourceMaps: { inline: false, vendor: false },
+ /**
+ * Custom TypeScript Transformers (compile Inferno tsx to ts)
+ */
+ transformers: {
+ before: [TsTransformClasscat(), TsTransformInferno()],
+ },
+};
+const fuseClientOptions: FuseBoxOptions = {
+ ...fuseOptions,
+ plugins: [
+ /**
+ * https://fuse-box.org/page/css-resource-plugin
+ * Compile Sass {SassPlugin()}
+ * Make .css files modules-like (allow import them like modules) {CSSModules}
+ * Make .css files modules like and allow import it from node_modules too {CSSResourcePlugin}
+ * Use them all and bundle with {CSSPlugin}
+ * */
+ CSSPlugin(),
+ ],
+};
+const fuseServerOptions: FuseBoxOptions = {
+ ...fuseOptions,
+};
+
+Sparky.task('clean', () => {
+ /**Clean distribute (dist) folder */
+ Sparky.src('dist/').clean('dist/');
+});
+Sparky.task('config', () => {
+ fuse = FuseBox.init(fuseOptions);
+ fuse.dev();
+});
+Sparky.task('test', ['&clean', '&config'], () => {
+ fuse.bundle('client/bundle').test('[**/**.test.tsx]', null);
+});
+Sparky.task('client', () => {
+ fuse.opts = fuseClientOptions;
+ fuse
+ .bundle('client/bundle')
+ .target('browser@esnext')
+ .watch('client/**')
+ .hmr()
+ .instructions('> client/index.tsx');
+});
+Sparky.task('copy-assets', () =>
+ Sparky.src('**/**.*', { base: 'src/assets' }).dest('dist/assets')
+);
+Sparky.task('server', () => {
+ /**Workaround. Should be fixed */
+ fuse.opts = fuseServerOptions;
+ fuse
+ .bundle('server/bundle')
+ .watch('**')
+ .target('server@esnext')
+ .instructions('> [server/index.tsx]')
+ .completed(proc => {
+ proc.require({
+ // tslint:disable-next-line:no-shadowed-variable
+ close: ({ FuseBox }) => FuseBox.import(FuseBox.mainFile).shutdown(),
+ });
+ });
+});
+Sparky.task(
+ 'dev',
+ ['&clean', '&config', '&client', '&server', '©-assets'],
+ () => {
+ fuse.run();
+ }
+);
--- /dev/null
+fs = require('fs');
+
+let translationDir = 'translations/';
+let outDir = 'src/shared/translations/';
+fs.mkdirSync(outDir, { recursive: true });
+fs.readdir(translationDir, (err, files) => {
+ files.forEach(filename => {
+ const lang = filename.split('.')[0];
+ try {
+ const json = JSON.parse(
+ fs.readFileSync(translationDir + filename, 'utf8')
+ );
+ var data = `export const ${lang} = {\n translation: {`;
+ for (var key in json) {
+ if (key in json) {
+ const value = json[key].replace(/"/g, '\\"');
+ data = `${data}\n ${key}: "${value}",`;
+ }
+ }
+ data += '\n },\n};';
+ const target = outDir + lang + '.ts';
+ fs.writeFileSync(target, data);
+ } catch (err) {
+ console.error(err);
+ }
+ });
+});
"author": "Dessalines <tyhou13@gmx.com>",
"license": "AGPL-3.0",
"scripts": {
- "dev": "set NODE_ENV=development && node -r ts-node/register --inspect fuse.ts dev",
"lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
+ "prebuild": "node generate_translations.js",
+ "prestart": "node generate_translations.js",
+ "start": "set NODE_ENV=development && node -r ts-node/register --inspect fuse.ts dev",
"test": "node -r ts-node/register --inspect fuse.ts test"
},
"repository": "https://github.com/LemmyNet/lemmy-isomorphic-ui",
"dependencies": {
+ "@types/autosize": "^3.0.6",
+ "@types/node-fetch": "^2.5.7",
+ "autosize": "^4.0.2",
+ "choices.js": "^9.0.1",
"cookie-parser": "^1.4.3",
+ "emoji-short-name": "^1.0.0",
"express": "~4.17.1",
+ "i18next": "^19.4.1",
"inferno": "^7.4.3",
"inferno-create-element": "^7.4.3",
+ "inferno-helmet": "^5.2.1",
"inferno-hydrate": "^7.4.3",
+ "inferno-i18next": "github:nimbusec-oss/inferno-i18next#semver:^7.4.2",
"inferno-router": "^7.4.3",
"inferno-server": "^7.4.3",
- "serialize-javascript": "^4.0.0"
+ "isomorphic-cookie": "^1.2.4",
+ "isomorphic-ws": "^4.0.1",
+ "js-cookie": "^2.2.0",
+ "jwt-decode": "^2.2.0",
+ "markdown-it": "^11.0.0",
+ "markdown-it-container": "^3.0.0",
+ "markdown-it-emoji": "^1.4.0",
+ "markdown-it-sub": "^1.0.0",
+ "markdown-it-sup": "^1.0.0",
+ "moment": "^2.24.0",
+ "node-fetch": "^2.6.0",
+ "reconnecting-websocket": "^4.4.0",
+ "rxjs": "^6.5.5",
+ "serialize-javascript": "^4.0.0",
+ "terser": "^4.6.11",
+ "tippy.js": "^6.1.1",
+ "toastify-js": "^1.7.0",
+ "tributejs": "^5.1.3",
+ "ws": "^7.3.1"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.1",
"@types/jest": "^26.0.10",
"@types/node": "^14.6.0",
"@types/serialize-javascript": "^4.0.0",
+ "classcat": "^4.1.0",
"enzyme": "^3.3.0",
"enzyme-adapter-inferno": "^1.3.0",
"eslint": "^7.5.0",
"jest": "^26.4.2",
"jsdom": "16.4.0",
"jsdom-global": "3.0.2",
+ "lemmy-js-client": "^1.0.8",
"lint-staged": "^10.1.3",
"prettier": "^2.0.4",
"sortpack": "^2.1.4",
"ts-node": "^9.0.0",
"ts-transform-classcat": "^1.0.0",
"ts-transform-inferno": "^4.0.3",
- "tslint-react-recommended": "^1.0.15",
"typescript": "^4.0.2"
},
+ "engines": {
+ "node": ">=8.9.0"
+ },
+ "engineStrict": true,
"husky": {
"hooks": {
"pre-commit": "lint-staged"
+++ /dev/null
-.text {\r
- color: brown;\r
- font-size: 25pt;\r
-}\r
-.count {\r
- color: blue;\r
-}\r
-.button {\r
- color: red;\r
-}\r
+++ /dev/null
-import 'jsdom-global/register';
-import { configure, mount, render, shallow } from 'enzyme';
-import InfernoEnzymeAdapter = require('enzyme-adapter-inferno');
-import { should } from 'fuse-test-runner';
-import { Component } from 'inferno';
-import { renderToSnapshot } from 'inferno-test-utils';
-import About from './About';
-configure({ adapter: new InfernoEnzymeAdapter() });
-
-export class AboutTest {
- public 'Should be okay'() {
- const wrapper = mount(<About />);
- wrapper.find('.button').simulate('click');
- const countText = wrapper.find('.count').text();
- should(countText).beString().equal('1');
- }
-}
+++ /dev/null
-import { Component } from 'inferno';
-import './About.css';
-interface IState {
- clickCount: number;
-}
-interface IProps {}
-export default class About extends Component<IProps, IState> {
- constructor(props) {
- super(props);
- this.state = {
- clickCount: 0,
- };
- this.increment = this.increment.bind(this);
- }
- protected increment() {
- this.setState({
- clickCount: this.state.clickCount + 1,
- });
- }
- public render() {
- return (
- <div>
- Simple Inferno SSR template
- <p className="text">Hello, world!</p>
- <button onClick={this.increment} className="button">
- Increment
- </button>
- <p className="count">{this.state.clickCount}</p>
- </div>
- );
- }
-}
+++ /dev/null
-import { Component, render } from 'inferno';
-import { Link, Route, StaticRouter, Switch } from 'inferno-router';
-import About from '../About/About';
-import Home from '../Home/Home';
-interface IState {}
-interface IProps {
- name: string;
-}
-export default class App extends Component<IProps, IState> {
- constructor(props) {
- super(props);
- }
- public render() {
- return (
- <div>
- <div>
- <h3>{this.props.name}</h3>
- <div>
- <Link to="/Home">
- <p>Home</p>
- </Link>
- </div>
- <div>
- <Link to="/About" className="link">
- <p>About</p>
- </Link>
- </div>
- </div>
- <div>
- <Switch>
- <Route exact path="/Home" component={Home} />
- <Route exact path="/About" component={About} />
- </Switch>
- </div>
- </div>
- );
- }
-}
+++ /dev/null
-import { Component } from 'inferno';
-interface IState {}
-interface IProps {}
-export default class Home extends Component<IProps, IState> {
- constructor(props) {
- super(props);
- }
- protected click() {
- /**
- * Try to debug next line
- */
- console.log('hi');
- }
- public render() {
- return (
- <div>
- Home page
- <button onClick={this.click}>Click me</button>
- </div>
- );
- }
-}
import { Component } from 'inferno';
import { hydrate } from 'inferno-hydrate';
import { BrowserRouter } from 'inferno-router';
-import App from './components/App/App';
-import { initDevTools } from 'inferno-devtools';
+import { App } from '../shared/components/app';
+/* import { initDevTools } from 'inferno-devtools'; */
declare global {
interface Window {
const wrapper = (
<BrowserRouter>
- <App name={window.isoData.name} />
+ <App />
</BrowserRouter>
);
-initDevTools();
+/* initDevTools(); */
hydrate(wrapper, document.getElementById('root'));
import cookieParser = require('cookie-parser');
-import * as serialize from 'serialize-javascript';
-import * as express from 'express';
+import serialize from 'serialize-javascript';
+import express from 'express';
import { StaticRouter } from 'inferno-router';
import { renderToString } from 'inferno-server';
+import { matchPath } from 'inferno-router';
import path = require('path');
-import App from '../client/components/App/App';
+import { App } from '../shared/components/app';
+import { routes } from '../shared/routes';
+import IsomorphicCookie from 'isomorphic-cookie';
const server = express();
const port = 1234;
server.use(express.json());
server.use(express.urlencoded({ extended: false }));
+server.use('/assets', express.static(path.resolve('./dist/assets')));
server.use('/static', express.static(path.resolve('./dist/client')));
server.use(cookieParser());
server.get('/*', (req, res) => {
+ const activeRoute = routes.find(route => matchPath(req.url, route)) || {};
+ console.log(activeRoute);
const context = {} as any;
const isoData = {
name: 'fishing sux',
};
+ let auth: string = IsomorphicCookie.load('jwt', req);
const wrapper = (
<StaticRouter location={req.url} context={context}>
- <App name={isoData.name} />
+ <App />
</StaticRouter>
);
if (context.url) {
}
res.send(`
- <!doctype html>
- <html>
- <head>
- <title>My Universal App</title>
- <script>window.isoData = ${serialize(isoData)}</script>
- </head>
- <body>
- <div id='root'>${renderToString(wrapper)}</div>
- <script src='./static/bundle.js'></script>
- </body>
- </html>
+ <!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <script>window.isoData = ${serialize(isoData)}</script>
+
+ <!-- Required meta tags -->
+ <meta name="Description" content="Lemmy">
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+
+ <!-- Icons -->
+ <link rel="shortcut icon" type="image/svg+xml" href="/assets/favicon.svg" />
+ <link rel="apple-touch-icon" href="/assets/apple-touch-icon.png" />
+
+ <!-- Styles -->
+ <link rel="stylesheet" type="text/css" href="/assets/css/tribute.css" />
+ <link rel="stylesheet" type="text/css" href="/assets/css/toastify.css" />
+ <link rel="stylesheet" type="text/css" href="/assets/css/choices.min.css" />
+ <link rel="stylesheet" type="text/css" href="/assets/css/tippy.css" />
+ <link rel="stylesheet" type="text/css" href="/assets/css/themes/litely.min.css" id="default-light" media="(prefers-color-scheme: light)" />
+ <link rel="stylesheet" type="text/css" href="/assets/css/themes/darkly.min.css" id="default-dark" media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)" />
+ <link rel="stylesheet" type="text/css" href="/assets/css/main.css" />
+
+ <!-- Scripts -->
+ <script async src="/assets/libs/sortable/sortable.min.js"></script>
+ </head>
+
+ <body>
+ <div id='root'>${renderToString(wrapper)}</div>
+ <script src='./static/bundle.js'></script>
+ </body>
+ </html>
`);
});
let Server = server.listen(port, () => {
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ UserOperation,
+ SiteResponse,
+ GetSiteResponse,
+ SiteConfigForm,
+ GetSiteConfigResponse,
+ WebSocketJsonResponse,
+} from 'lemmy-js-client';
+import { WebSocketService } from '../services';
+import { wsJsonToRes, capitalizeFirstLetter, toast, randomStr } from '../utils';
+import autosize from 'autosize';
+import { SiteForm } from './site-form';
+import { UserListing } from './user-listing';
+import { i18n } from '../i18next';
+
+interface AdminSettingsState {
+ siteRes: GetSiteResponse;
+ siteConfigRes: GetSiteConfigResponse;
+ siteConfigForm: SiteConfigForm;
+ loading: boolean;
+ siteConfigLoading: boolean;
+}
+
+export class AdminSettings extends Component<any, AdminSettingsState> {
+ private siteConfigTextAreaId = `site-config-${randomStr()}`;
+ private subscription: Subscription;
+ private emptyState: AdminSettingsState = {
+ siteRes: {
+ site: {
+ id: null,
+ name: null,
+ creator_id: null,
+ creator_name: null,
+ published: null,
+ number_of_users: null,
+ number_of_posts: null,
+ number_of_comments: null,
+ number_of_communities: null,
+ enable_downvotes: null,
+ open_registration: null,
+ enable_nsfw: null,
+ },
+ admins: [],
+ banned: [],
+ online: null,
+ version: null,
+ federated_instances: null,
+ },
+ siteConfigForm: {
+ config_hjson: null,
+ auth: null,
+ },
+ siteConfigRes: {
+ config_hjson: null,
+ },
+ loading: true,
+ siteConfigLoading: null,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ WebSocketService.Instance.getSite();
+ WebSocketService.Instance.getSiteConfig();
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ get documentTitle(): string {
+ if (this.state.siteRes.site.name) {
+ return `${i18n.t('admin_settings')} - ${this.state.siteRes.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle} />
+ {this.state.loading ? (
+ <h5>
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ </h5>
+ ) : (
+ <div class="row">
+ <div class="col-12 col-md-6">
+ {this.state.siteRes.site.id && (
+ <SiteForm site={this.state.siteRes.site} />
+ )}
+ {this.admins()}
+ {this.bannedUsers()}
+ </div>
+ <div class="col-12 col-md-6">{this.adminSettings()}</div>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ admins() {
+ return (
+ <>
+ <h5>{capitalizeFirstLetter(i18n.t('admins'))}</h5>
+ <ul class="list-unstyled">
+ {this.state.siteRes.admins.map(admin => (
+ <li class="list-inline-item">
+ <UserListing
+ user={{
+ name: admin.name,
+ preferred_username: admin.preferred_username,
+ avatar: admin.avatar,
+ id: admin.id,
+ local: admin.local,
+ actor_id: admin.actor_id,
+ }}
+ />
+ </li>
+ ))}
+ </ul>
+ </>
+ );
+ }
+
+ bannedUsers() {
+ return (
+ <>
+ <h5>{i18n.t('banned_users')}</h5>
+ <ul class="list-unstyled">
+ {this.state.siteRes.banned.map(banned => (
+ <li class="list-inline-item">
+ <UserListing
+ user={{
+ name: banned.name,
+ preferred_username: banned.preferred_username,
+ avatar: banned.avatar,
+ id: banned.id,
+ local: banned.local,
+ actor_id: banned.actor_id,
+ }}
+ />
+ </li>
+ ))}
+ </ul>
+ </>
+ );
+ }
+
+ adminSettings() {
+ return (
+ <div>
+ <h5>{i18n.t('admin_settings')}</h5>
+ <form onSubmit={linkEvent(this, this.handleSiteConfigSubmit)}>
+ <div class="form-group row">
+ <label
+ class="col-12 col-form-label"
+ htmlFor={this.siteConfigTextAreaId}
+ >
+ {i18n.t('site_config')}
+ </label>
+ <div class="col-12">
+ <textarea
+ id={this.siteConfigTextAreaId}
+ value={this.state.siteConfigForm.config_hjson}
+ onInput={linkEvent(this, this.handleSiteConfigHjsonChange)}
+ class="form-control text-monospace"
+ rows={3}
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-12">
+ <button type="submit" class="btn btn-secondary mr-2">
+ {this.state.siteConfigLoading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ capitalizeFirstLetter(i18n.t('save'))
+ )}
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+ }
+
+ handleSiteConfigSubmit(i: AdminSettings, event: any) {
+ event.preventDefault();
+ i.state.siteConfigLoading = true;
+ WebSocketService.Instance.saveSiteConfig(i.state.siteConfigForm);
+ i.setState(i.state);
+ }
+
+ handleSiteConfigHjsonChange(i: AdminSettings, event: any) {
+ i.state.siteConfigForm.config_hjson = event.target.value;
+ i.setState(i.state);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ this.context.router.history.push('/');
+ this.state.loading = false;
+ this.setState(this.state);
+ return;
+ } else if (msg.reconnect) {
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+
+ // This means it hasn't been set up yet
+ if (!data.site) {
+ this.context.router.history.push('/setup');
+ }
+ this.state.siteRes = data;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.EditSite) {
+ let data = res.data as SiteResponse;
+ this.state.siteRes.site = data.site;
+ this.setState(this.state);
+ toast(i18n.t('site_saved'));
+ } else if (res.op == UserOperation.GetSiteConfig) {
+ let data = res.data as GetSiteConfigResponse;
+ this.state.siteConfigRes = data;
+ this.state.loading = false;
+ this.state.siteConfigForm.config_hjson = this.state.siteConfigRes.config_hjson;
+ this.setState(this.state);
+ var textarea: any = document.getElementById(this.siteConfigTextAreaId);
+ autosize(textarea);
+ } else if (res.op == UserOperation.SaveSiteConfig) {
+ let data = res.data as GetSiteConfigResponse;
+ this.state.siteConfigRes = data;
+ this.state.siteConfigForm.config_hjson = this.state.siteConfigRes.config_hjson;
+ this.state.siteConfigLoading = false;
+ toast(i18n.t('site_saved'));
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import { Route, Switch } from 'inferno-router';
+/* import { Provider } from 'inferno-i18next'; */
+/* import { i18n } from './i18next'; */
+import { routes } from '../../shared/routes';
+import { Navbar } from '../../shared/components/navbar';
+import { Footer } from '../../shared/components/footer';
+import { Symbols } from '../../shared/components/symbols';
+
+export class App extends Component<any, any> {
+ constructor(props: any, context: any) {
+ super(props, context);
+ }
+
+ render() {
+ return (
+ <>
+ <h1>Hi there!</h1>
+ {/* <Provider i18next={i18n}> */}
+ <div>
+ <Navbar />
+ <div class="mt-4 p-0 fl-1">
+ <Switch>
+ {routes.map(({ path, exact, component: C, ...rest }) => (
+ <Route
+ key={path}
+ path={path}
+ exact={exact}
+ render={props => <C {...props} {...rest} />}
+ />
+ ))}
+ {/* <Route render={(props) => <NoMatch {...props} />} /> */}
+ </Switch>
+ <Symbols />
+ </div>
+ <Footer />
+ </div>
+ {/* </Provider> */}
+ </>
+ );
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+
+interface BannerIconHeaderProps {
+ banner?: string;
+ icon?: string;
+}
+
+export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
+ constructor(props: any, context: any) {
+ super(props, context);
+ }
+
+ render() {
+ return (
+ <div class="position-relative mb-2">
+ {this.props.banner && (
+ <img src={this.props.banner} class="banner img-fluid" />
+ )}
+ {this.props.icon && (
+ <img
+ src={this.props.icon}
+ className={`ml-2 mb-0 ${
+ this.props.banner ? 'avatar-pushup' : ''
+ } rounded-circle avatar-overlay`}
+ />
+ )}
+ </div>
+ );
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import { i18n } from '../i18next';
+
+interface CakeDayProps {
+ creatorName: string;
+}
+
+export class CakeDay extends Component<CakeDayProps, any> {
+ render() {
+ return (
+ <div
+ className={`mx-2 d-inline-block unselectable pointer`}
+ data-tippy-content={this.cakeDayTippy()}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-cake"></use>
+ </svg>
+ </div>
+ );
+ }
+
+ cakeDayTippy(): string {
+ return i18n.t('cake_day_info', { creator_name: this.props.creatorName });
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import { Link } from 'inferno-router';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ CommentNode as CommentNodeI,
+ CommentForm as CommentFormI,
+ WebSocketJsonResponse,
+ UserOperation,
+ CommentResponse,
+} from 'lemmy-js-client';
+import { capitalizeFirstLetter, wsJsonToRes } from '../utils';
+import { WebSocketService, UserService } from '../services';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
+import { MarkdownTextArea } from './markdown-textarea';
+
+interface CommentFormProps {
+ postId?: number;
+ node?: CommentNodeI;
+ onReplyCancel?(): any;
+ edit?: boolean;
+ disabled?: boolean;
+ focus?: boolean;
+}
+
+interface CommentFormState {
+ commentForm: CommentFormI;
+ buttonTitle: string;
+ finished: boolean;
+}
+
+export class CommentForm extends Component<CommentFormProps, CommentFormState> {
+ private subscription: Subscription;
+ private emptyState: CommentFormState = {
+ commentForm: {
+ auth: null,
+ content: null,
+ post_id: this.props.node
+ ? this.props.node.comment.post_id
+ : this.props.postId,
+ creator_id: UserService.Instance.user
+ ? UserService.Instance.user.id
+ : null,
+ },
+ buttonTitle: !this.props.node
+ ? capitalizeFirstLetter(i18n.t('post'))
+ : this.props.edit
+ ? capitalizeFirstLetter(i18n.t('save'))
+ : capitalizeFirstLetter(i18n.t('reply')),
+ finished: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.handleCommentSubmit = this.handleCommentSubmit.bind(this);
+ this.handleReplyCancel = this.handleReplyCancel.bind(this);
+
+ this.state = this.emptyState;
+
+ if (this.props.node) {
+ if (this.props.edit) {
+ this.state.commentForm.edit_id = this.props.node.comment.id;
+ this.state.commentForm.parent_id = this.props.node.comment.parent_id;
+ this.state.commentForm.content = this.props.node.comment.content;
+ this.state.commentForm.creator_id = this.props.node.comment.creator_id;
+ } else {
+ // A reply gets a new parent id
+ this.state.commentForm.parent_id = this.props.node.comment.id;
+ }
+ }
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ render() {
+ return (
+ <div class="mb-3">
+ {UserService.Instance.user ? (
+ <MarkdownTextArea
+ initialContent={this.state.commentForm.content}
+ buttonTitle={this.state.buttonTitle}
+ finished={this.state.finished}
+ replyType={!!this.props.node}
+ focus={this.props.focus}
+ disabled={this.props.disabled}
+ onSubmit={this.handleCommentSubmit}
+ onReplyCancel={this.handleReplyCancel}
+ />
+ ) : (
+ <div class="alert alert-light" role="alert">
+ <svg class="icon icon-inline mr-2">
+ <use xlinkHref="#icon-alert-triangle"></use>
+ </svg>
+ <T i18nKey="must_login" class="d-inline">
+ #
+ <Link class="alert-link" to="/login">
+ #
+ </Link>
+ </T>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ handleCommentSubmit(msg: { val: string; formId: string }) {
+ this.state.commentForm.content = msg.val;
+ this.state.commentForm.form_id = msg.formId;
+ if (this.props.edit) {
+ WebSocketService.Instance.editComment(this.state.commentForm);
+ } else {
+ WebSocketService.Instance.createComment(this.state.commentForm);
+ }
+ this.setState(this.state);
+ }
+
+ handleReplyCancel() {
+ this.props.onReplyCancel();
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+
+ // Only do the showing and hiding if logged in
+ if (UserService.Instance.user) {
+ if (
+ res.op == UserOperation.CreateComment ||
+ res.op == UserOperation.EditComment
+ ) {
+ let data = res.data as CommentResponse;
+
+ // This only finishes this form, if the randomly generated form_id matches the one received
+ if (this.state.commentForm.form_id == data.form_id) {
+ this.setState({ finished: true });
+
+ // Necessary because it broke tribute for some reaso
+ this.setState({ finished: false });
+ }
+ }
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import {
+ CommentNode as CommentNodeI,
+ CommentLikeForm,
+ DeleteCommentForm,
+ RemoveCommentForm,
+ MarkCommentAsReadForm,
+ MarkUserMentionAsReadForm,
+ SaveCommentForm,
+ BanFromCommunityForm,
+ BanUserForm,
+ CommunityUser,
+ UserView,
+ AddModToCommunityForm,
+ AddAdminForm,
+ TransferCommunityForm,
+ TransferSiteForm,
+ SortType,
+} from 'lemmy-js-client';
+import { CommentSortType, BanType } from '../interfaces';
+import { WebSocketService, UserService } from '../services';
+import {
+ mdToHtml,
+ getUnixTime,
+ canMod,
+ isMod,
+ setupTippy,
+ colorList,
+} from '../utils';
+import moment from 'moment';
+import { MomentTime } from './moment-time';
+import { CommentForm } from './comment-form';
+import { CommentNodes } from './comment-nodes';
+import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
+import { i18n } from '../i18next';
+
+interface CommentNodeState {
+ showReply: boolean;
+ showEdit: boolean;
+ showRemoveDialog: boolean;
+ removeReason: string;
+ showBanDialog: boolean;
+ removeData: boolean;
+ banReason: string;
+ banExpires: string;
+ banType: BanType;
+ showConfirmTransferSite: boolean;
+ showConfirmTransferCommunity: boolean;
+ showConfirmAppointAsMod: boolean;
+ showConfirmAppointAsAdmin: boolean;
+ collapsed: boolean;
+ viewSource: boolean;
+ showAdvanced: boolean;
+ my_vote: number;
+ score: number;
+ upvotes: number;
+ downvotes: number;
+ borderColor: string;
+ readLoading: boolean;
+ saveLoading: boolean;
+}
+
+interface CommentNodeProps {
+ node: CommentNodeI;
+ noBorder?: boolean;
+ noIndent?: boolean;
+ viewOnly?: boolean;
+ locked?: boolean;
+ markable?: boolean;
+ showContext?: boolean;
+ moderators: CommunityUser[];
+ admins: UserView[];
+ // TODO is this necessary, can't I get it from the node itself?
+ postCreatorId?: number;
+ showCommunity?: boolean;
+ sort?: CommentSortType;
+ sortType?: SortType;
+ enableDownvotes: boolean;
+}
+
+export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
+ private emptyState: CommentNodeState = {
+ showReply: false,
+ showEdit: false,
+ showRemoveDialog: false,
+ removeReason: null,
+ showBanDialog: false,
+ removeData: null,
+ banReason: null,
+ banExpires: null,
+ banType: BanType.Community,
+ collapsed: false,
+ viewSource: false,
+ showAdvanced: false,
+ showConfirmTransferSite: false,
+ showConfirmTransferCommunity: false,
+ showConfirmAppointAsMod: false,
+ showConfirmAppointAsAdmin: false,
+ my_vote: this.props.node.comment.my_vote,
+ score: this.props.node.comment.score,
+ upvotes: this.props.node.comment.upvotes,
+ downvotes: this.props.node.comment.downvotes,
+ borderColor: this.props.node.comment.depth
+ ? colorList[this.props.node.comment.depth % colorList.length]
+ : colorList[0],
+ readLoading: false,
+ saveLoading: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handleReplyCancel = this.handleReplyCancel.bind(this);
+ this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
+ this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
+ }
+
+ componentWillReceiveProps(nextProps: CommentNodeProps) {
+ this.state.my_vote = nextProps.node.comment.my_vote;
+ this.state.upvotes = nextProps.node.comment.upvotes;
+ this.state.downvotes = nextProps.node.comment.downvotes;
+ this.state.score = nextProps.node.comment.score;
+ this.state.readLoading = false;
+ this.state.saveLoading = false;
+ this.setState(this.state);
+ }
+
+ render() {
+ let node = this.props.node;
+ return (
+ <div
+ className={`comment ${
+ node.comment.parent_id && !this.props.noIndent ? 'ml-1' : ''
+ }`}
+ >
+ <div
+ id={`comment-${node.comment.id}`}
+ className={`details comment-node py-2 ${
+ !this.props.noBorder ? 'border-top border-light' : ''
+ } ${this.isCommentNew ? 'mark' : ''}`}
+ style={
+ !this.props.noIndent &&
+ this.props.node.comment.parent_id &&
+ `border-left: 2px ${this.state.borderColor} solid !important`
+ }
+ >
+ <div
+ class={`${
+ !this.props.noIndent &&
+ this.props.node.comment.parent_id &&
+ 'ml-2'
+ }`}
+ >
+ <div class="d-flex flex-wrap align-items-center text-muted small">
+ <span class="mr-2">
+ <UserListing
+ user={{
+ name: node.comment.creator_name,
+ preferred_username: node.comment.creator_preferred_username,
+ avatar: node.comment.creator_avatar,
+ id: node.comment.creator_id,
+ local: node.comment.creator_local,
+ actor_id: node.comment.creator_actor_id,
+ published: node.comment.creator_published,
+ }}
+ />
+ </span>
+
+ {this.isMod && (
+ <div className="badge badge-light d-none d-sm-inline mr-2">
+ {i18n.t('mod')}
+ </div>
+ )}
+ {this.isAdmin && (
+ <div className="badge badge-light d-none d-sm-inline mr-2">
+ {i18n.t('admin')}
+ </div>
+ )}
+ {this.isPostCreator && (
+ <div className="badge badge-light d-none d-sm-inline mr-2">
+ {i18n.t('creator')}
+ </div>
+ )}
+ {(node.comment.banned_from_community || node.comment.banned) && (
+ <div className="badge badge-danger mr-2">
+ {i18n.t('banned')}
+ </div>
+ )}
+ {this.props.showCommunity && (
+ <>
+ <span class="mx-1">{i18n.t('to')}</span>
+ <CommunityLink
+ community={{
+ name: node.comment.community_name,
+ id: node.comment.community_id,
+ local: node.comment.community_local,
+ actor_id: node.comment.community_actor_id,
+ icon: node.comment.community_icon,
+ }}
+ />
+ <span class="mx-2">•</span>
+ <Link class="mr-2" to={`/post/${node.comment.post_id}`}>
+ {node.comment.post_name}
+ </Link>
+ </>
+ )}
+ <button
+ class="btn text-muted"
+ onClick={linkEvent(this, this.handleCommentCollapse)}
+ >
+ {this.state.collapsed ? (
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-plus-square"></use>
+ </svg>
+ ) : (
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-minus-square"></use>
+ </svg>
+ )}
+ </button>
+ {/* This is an expanding spacer for mobile */}
+ <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
+ <button
+ className={`btn p-0 unselectable pointer ${this.scoreColor}`}
+ onClick={linkEvent(node, this.handleCommentUpvote)}
+ data-tippy-content={this.pointsTippy}
+ >
+ <svg class="icon icon-inline mr-1">
+ <use xlinkHref="#icon-zap"></use>
+ </svg>
+ <span class="mr-1">{this.state.score}</span>
+ </button>
+ <span className="mr-1">•</span>
+ <span>
+ <MomentTime data={node.comment} />
+ </span>
+ </div>
+ {/* end of user row */}
+ {this.state.showEdit && (
+ <CommentForm
+ node={node}
+ edit
+ onReplyCancel={this.handleReplyCancel}
+ disabled={this.props.locked}
+ focus
+ />
+ )}
+ {!this.state.showEdit && !this.state.collapsed && (
+ <div>
+ {this.state.viewSource ? (
+ <pre>{this.commentUnlessRemoved}</pre>
+ ) : (
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(
+ this.commentUnlessRemoved
+ )}
+ />
+ )}
+ <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
+ {this.props.showContext && this.linkBtn}
+ {this.props.markable && (
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleMarkRead)}
+ data-tippy-content={
+ node.comment.read
+ ? i18n.t('mark_as_unread')
+ : i18n.t('mark_as_read')
+ }
+ >
+ {this.state.readLoading ? (
+ this.loadingIcon
+ ) : (
+ <svg
+ class={`icon icon-inline ${
+ node.comment.read && 'text-success'
+ }`}
+ >
+ <use xlinkHref="#icon-check"></use>
+ </svg>
+ )}
+ </button>
+ )}
+ {UserService.Instance.user && !this.props.viewOnly && (
+ <>
+ <button
+ className={`btn btn-link btn-animate ${
+ this.state.my_vote == 1 ? 'text-info' : 'text-muted'
+ }`}
+ onClick={linkEvent(node, this.handleCommentUpvote)}
+ data-tippy-content={i18n.t('upvote')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-arrow-up"></use>
+ </svg>
+ {this.state.upvotes !== this.state.score && (
+ <span class="ml-1">{this.state.upvotes}</span>
+ )}
+ </button>
+ {this.props.enableDownvotes && (
+ <button
+ className={`btn btn-link btn-animate ${
+ this.state.my_vote == -1
+ ? 'text-danger'
+ : 'text-muted'
+ }`}
+ onClick={linkEvent(node, this.handleCommentDownvote)}
+ data-tippy-content={i18n.t('downvote')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-arrow-down"></use>
+ </svg>
+ {this.state.upvotes !== this.state.score && (
+ <span class="ml-1">{this.state.downvotes}</span>
+ )}
+ </button>
+ )}
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleReplyClick)}
+ data-tippy-content={i18n.t('reply')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-reply1"></use>
+ </svg>
+ </button>
+ {!this.state.showAdvanced ? (
+ <button
+ className="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleShowAdvanced)}
+ data-tippy-content={i18n.t('more')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-more-vertical"></use>
+ </svg>
+ </button>
+ ) : (
+ <>
+ {!this.myComment && (
+ <button class="btn btn-link btn-animate">
+ <Link
+ class="text-muted"
+ to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
+ title={i18n.t('message').toLowerCase()}
+ >
+ <svg class="icon">
+ <use xlinkHref="#icon-mail"></use>
+ </svg>
+ </Link>
+ </button>
+ )}
+ {!this.props.showContext && this.linkBtn}
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleSaveCommentClick
+ )}
+ data-tippy-content={
+ node.comment.saved
+ ? i18n.t('unsave')
+ : i18n.t('save')
+ }
+ >
+ {this.state.saveLoading ? (
+ this.loadingIcon
+ ) : (
+ <svg
+ class={`icon icon-inline ${
+ node.comment.saved && 'text-warning'
+ }`}
+ >
+ <use xlinkHref="#icon-star"></use>
+ </svg>
+ )}
+ </button>
+ <button
+ className="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleViewSource)}
+ data-tippy-content={i18n.t('view_source')}
+ >
+ <svg
+ class={`icon icon-inline ${
+ this.state.viewSource && 'text-success'
+ }`}
+ >
+ <use xlinkHref="#icon-file-text"></use>
+ </svg>
+ </button>
+ {this.myComment && (
+ <>
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleEditClick)}
+ data-tippy-content={i18n.t('edit')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-edit"></use>
+ </svg>
+ </button>
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleDeleteClick
+ )}
+ data-tippy-content={
+ !node.comment.deleted
+ ? i18n.t('delete')
+ : i18n.t('restore')
+ }
+ >
+ <svg
+ class={`icon icon-inline ${
+ node.comment.deleted && 'text-danger'
+ }`}
+ >
+ <use xlinkHref="#icon-trash"></use>
+ </svg>
+ </button>
+ </>
+ )}
+ {/* Admins and mods can remove comments */}
+ {(this.canMod || this.canAdmin) && (
+ <>
+ {!node.comment.removed ? (
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleModRemoveShow
+ )}
+ >
+ {i18n.t('remove')}
+ </button>
+ ) : (
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleModRemoveSubmit
+ )}
+ >
+ {i18n.t('restore')}
+ </button>
+ )}
+ </>
+ )}
+ {/* Mods can ban from community, and appoint as mods to community */}
+ {this.canMod && (
+ <>
+ {!this.isMod &&
+ (!node.comment.banned_from_community ? (
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleModBanFromCommunityShow
+ )}
+ >
+ {i18n.t('ban')}
+ </button>
+ ) : (
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleModBanFromCommunitySubmit
+ )}
+ >
+ {i18n.t('unban')}
+ </button>
+ ))}
+ {!node.comment.banned_from_community &&
+ node.comment.creator_local &&
+ (!this.state.showConfirmAppointAsMod ? (
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleShowConfirmAppointAsMod
+ )}
+ >
+ {this.isMod
+ ? i18n.t('remove_as_mod')
+ : i18n.t('appoint_as_mod')}
+ </button>
+ ) : (
+ <>
+ <button class="btn btn-link btn-animate text-muted">
+ {i18n.t('are_you_sure')}
+ </button>
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleAddModToCommunity
+ )}
+ >
+ {i18n.t('yes')}
+ </button>
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleCancelConfirmAppointAsMod
+ )}
+ >
+ {i18n.t('no')}
+ </button>
+ </>
+ ))}
+ </>
+ )}
+ {/* Community creators and admins can transfer community to another mod */}
+ {(this.amCommunityCreator || this.canAdmin) &&
+ this.isMod &&
+ node.comment.creator_local &&
+ (!this.state.showConfirmTransferCommunity ? (
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleShowConfirmTransferCommunity
+ )}
+ >
+ {i18n.t('transfer_community')}
+ </button>
+ ) : (
+ <>
+ <button class="btn btn-link btn-animate text-muted">
+ {i18n.t('are_you_sure')}
+ </button>
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleTransferCommunity
+ )}
+ >
+ {i18n.t('yes')}
+ </button>
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this
+ .handleCancelShowConfirmTransferCommunity
+ )}
+ >
+ {i18n.t('no')}
+ </button>
+ </>
+ ))}
+ {/* Admins can ban from all, and appoint other admins */}
+ {this.canAdmin && (
+ <>
+ {!this.isAdmin &&
+ (!node.comment.banned ? (
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleModBanShow
+ )}
+ >
+ {i18n.t('ban_from_site')}
+ </button>
+ ) : (
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleModBanSubmit
+ )}
+ >
+ {i18n.t('unban_from_site')}
+ </button>
+ ))}
+ {!node.comment.banned &&
+ node.comment.creator_local &&
+ (!this.state.showConfirmAppointAsAdmin ? (
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleShowConfirmAppointAsAdmin
+ )}
+ >
+ {this.isAdmin
+ ? i18n.t('remove_as_admin')
+ : i18n.t('appoint_as_admin')}
+ </button>
+ ) : (
+ <>
+ <button class="btn btn-link btn-animate text-muted">
+ {i18n.t('are_you_sure')}
+ </button>
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleAddAdmin
+ )}
+ >
+ {i18n.t('yes')}
+ </button>
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleCancelConfirmAppointAsAdmin
+ )}
+ >
+ {i18n.t('no')}
+ </button>
+ </>
+ ))}
+ </>
+ )}
+ {/* Site Creator can transfer to another admin */}
+ {this.amSiteCreator &&
+ this.isAdmin &&
+ node.comment.creator_local &&
+ (!this.state.showConfirmTransferSite ? (
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleShowConfirmTransferSite
+ )}
+ >
+ {i18n.t('transfer_site')}
+ </button>
+ ) : (
+ <>
+ <button class="btn btn-link btn-animate text-muted">
+ {i18n.t('are_you_sure')}
+ </button>
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleTransferSite
+ )}
+ >
+ {i18n.t('yes')}
+ </button>
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleCancelShowConfirmTransferSite
+ )}
+ >
+ {i18n.t('no')}
+ </button>
+ </>
+ ))}
+ </>
+ )}
+ </>
+ )}
+ </div>
+ {/* end of button group */}
+ </div>
+ )}
+ </div>
+ </div>
+ {/* end of details */}
+ {this.state.showRemoveDialog && (
+ <form
+ class="form-inline"
+ onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
+ >
+ <input
+ type="text"
+ class="form-control mr-2"
+ placeholder={i18n.t('reason')}
+ value={this.state.removeReason}
+ onInput={linkEvent(this, this.handleModRemoveReasonChange)}
+ />
+ <button type="submit" class="btn btn-secondary">
+ {i18n.t('remove_comment')}
+ </button>
+ </form>
+ )}
+ {this.state.showBanDialog && (
+ <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
+ <div class="form-group row">
+ <label class="col-form-label">{i18n.t('reason')}</label>
+ <input
+ type="text"
+ class="form-control mr-2"
+ placeholder={i18n.t('reason')}
+ value={this.state.banReason}
+ onInput={linkEvent(this, this.handleModBanReasonChange)}
+ />
+ <div class="form-group">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="mod-ban-remove-data"
+ type="checkbox"
+ checked={this.state.removeData}
+ onChange={linkEvent(this, this.handleModRemoveDataChange)}
+ />
+ <label class="form-check-label" htmlFor="mod-ban-remove-data">
+ {i18n.t('remove_posts_comments')}
+ </label>
+ </div>
+ </div>
+ </div>
+ {/* TODO hold off on expires until later */}
+ {/* <div class="form-group row"> */}
+ {/* <label class="col-form-label">Expires</label> */}
+ {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
+ {/* </div> */}
+ <div class="form-group row">
+ <button type="submit" class="btn btn-secondary">
+ {i18n.t('ban')} {node.comment.creator_name}
+ </button>
+ </div>
+ </form>
+ )}
+ {this.state.showReply && (
+ <CommentForm
+ node={node}
+ onReplyCancel={this.handleReplyCancel}
+ disabled={this.props.locked}
+ focus
+ />
+ )}
+ {node.children && !this.state.collapsed && (
+ <CommentNodes
+ nodes={node.children}
+ locked={this.props.locked}
+ moderators={this.props.moderators}
+ admins={this.props.admins}
+ postCreatorId={this.props.postCreatorId}
+ sort={this.props.sort}
+ sortType={this.props.sortType}
+ enableDownvotes={this.props.enableDownvotes}
+ />
+ )}
+ {/* A collapsed clearfix */}
+ {this.state.collapsed && <div class="row col-12"></div>}
+ </div>
+ );
+ }
+
+ get linkBtn() {
+ let node = this.props.node;
+ return (
+ <Link
+ class="btn btn-link btn-animate text-muted"
+ to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
+ title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-link"></use>
+ </svg>
+ </Link>
+ );
+ }
+
+ get loadingIcon() {
+ return (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ );
+ }
+
+ get myComment(): boolean {
+ return (
+ UserService.Instance.user &&
+ this.props.node.comment.creator_id == UserService.Instance.user.id
+ );
+ }
+
+ get isMod(): boolean {
+ return (
+ this.props.moderators &&
+ isMod(
+ this.props.moderators.map(m => m.user_id),
+ this.props.node.comment.creator_id
+ )
+ );
+ }
+
+ get isAdmin(): boolean {
+ return (
+ this.props.admins &&
+ isMod(
+ this.props.admins.map(a => a.id),
+ this.props.node.comment.creator_id
+ )
+ );
+ }
+
+ get isPostCreator(): boolean {
+ return this.props.node.comment.creator_id == this.props.postCreatorId;
+ }
+
+ get canMod(): boolean {
+ if (this.props.admins && this.props.moderators) {
+ let adminsThenMods = this.props.admins
+ .map(a => a.id)
+ .concat(this.props.moderators.map(m => m.user_id));
+
+ return canMod(
+ UserService.Instance.user,
+ adminsThenMods,
+ this.props.node.comment.creator_id
+ );
+ } else {
+ return false;
+ }
+ }
+
+ get canAdmin(): boolean {
+ return (
+ this.props.admins &&
+ canMod(
+ UserService.Instance.user,
+ this.props.admins.map(a => a.id),
+ this.props.node.comment.creator_id
+ )
+ );
+ }
+
+ get amCommunityCreator(): boolean {
+ return (
+ this.props.moderators &&
+ UserService.Instance.user &&
+ this.props.node.comment.creator_id != UserService.Instance.user.id &&
+ UserService.Instance.user.id == this.props.moderators[0].user_id
+ );
+ }
+
+ get amSiteCreator(): boolean {
+ return (
+ this.props.admins &&
+ UserService.Instance.user &&
+ this.props.node.comment.creator_id != UserService.Instance.user.id &&
+ UserService.Instance.user.id == this.props.admins[0].id
+ );
+ }
+
+ get commentUnlessRemoved(): string {
+ let node = this.props.node;
+ return node.comment.removed
+ ? `*${i18n.t('removed')}*`
+ : node.comment.deleted
+ ? `*${i18n.t('deleted')}*`
+ : node.comment.content;
+ }
+
+ handleReplyClick(i: CommentNode) {
+ i.state.showReply = true;
+ i.setState(i.state);
+ }
+
+ handleEditClick(i: CommentNode) {
+ i.state.showEdit = true;
+ i.setState(i.state);
+ }
+
+ handleDeleteClick(i: CommentNode) {
+ let deleteForm: DeleteCommentForm = {
+ edit_id: i.props.node.comment.id,
+ deleted: !i.props.node.comment.deleted,
+ auth: null,
+ };
+ WebSocketService.Instance.deleteComment(deleteForm);
+ }
+
+ handleSaveCommentClick(i: CommentNode) {
+ let saved =
+ i.props.node.comment.saved == undefined
+ ? true
+ : !i.props.node.comment.saved;
+ let form: SaveCommentForm = {
+ comment_id: i.props.node.comment.id,
+ save: saved,
+ };
+
+ WebSocketService.Instance.saveComment(form);
+
+ i.state.saveLoading = true;
+ i.setState(this.state);
+ }
+
+ handleReplyCancel() {
+ this.state.showReply = false;
+ this.state.showEdit = false;
+ this.setState(this.state);
+ }
+
+ handleCommentUpvote(i: CommentNodeI) {
+ let new_vote = this.state.my_vote == 1 ? 0 : 1;
+
+ if (this.state.my_vote == 1) {
+ this.state.score--;
+ this.state.upvotes--;
+ } else if (this.state.my_vote == -1) {
+ this.state.downvotes--;
+ this.state.upvotes++;
+ this.state.score += 2;
+ } else {
+ this.state.upvotes++;
+ this.state.score++;
+ }
+
+ this.state.my_vote = new_vote;
+
+ let form: CommentLikeForm = {
+ comment_id: i.comment.id,
+ score: this.state.my_vote,
+ };
+
+ WebSocketService.Instance.likeComment(form);
+ this.setState(this.state);
+ setupTippy();
+ }
+
+ handleCommentDownvote(i: CommentNodeI) {
+ let new_vote = this.state.my_vote == -1 ? 0 : -1;
+
+ if (this.state.my_vote == 1) {
+ this.state.score -= 2;
+ this.state.upvotes--;
+ this.state.downvotes++;
+ } else if (this.state.my_vote == -1) {
+ this.state.downvotes--;
+ this.state.score++;
+ } else {
+ this.state.downvotes++;
+ this.state.score--;
+ }
+
+ this.state.my_vote = new_vote;
+
+ let form: CommentLikeForm = {
+ comment_id: i.comment.id,
+ score: this.state.my_vote,
+ };
+
+ WebSocketService.Instance.likeComment(form);
+ this.setState(this.state);
+ setupTippy();
+ }
+
+ handleModRemoveShow(i: CommentNode) {
+ i.state.showRemoveDialog = true;
+ i.setState(i.state);
+ }
+
+ handleModRemoveReasonChange(i: CommentNode, event: any) {
+ i.state.removeReason = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleModRemoveDataChange(i: CommentNode, event: any) {
+ i.state.removeData = event.target.checked;
+ i.setState(i.state);
+ }
+
+ handleModRemoveSubmit(i: CommentNode) {
+ event.preventDefault();
+ let form: RemoveCommentForm = {
+ edit_id: i.props.node.comment.id,
+ removed: !i.props.node.comment.removed,
+ reason: i.state.removeReason,
+ auth: null,
+ };
+ WebSocketService.Instance.removeComment(form);
+
+ i.state.showRemoveDialog = false;
+ i.setState(i.state);
+ }
+
+ handleMarkRead(i: CommentNode) {
+ // if it has a user_mention_id field, then its a mention
+ if (i.props.node.comment.user_mention_id) {
+ let form: MarkUserMentionAsReadForm = {
+ user_mention_id: i.props.node.comment.user_mention_id,
+ read: !i.props.node.comment.read,
+ };
+ WebSocketService.Instance.markUserMentionAsRead(form);
+ } else {
+ let form: MarkCommentAsReadForm = {
+ edit_id: i.props.node.comment.id,
+ read: !i.props.node.comment.read,
+ auth: null,
+ };
+ WebSocketService.Instance.markCommentAsRead(form);
+ }
+
+ i.state.readLoading = true;
+ i.setState(this.state);
+ }
+
+ handleModBanFromCommunityShow(i: CommentNode) {
+ i.state.showBanDialog = !i.state.showBanDialog;
+ i.state.banType = BanType.Community;
+ i.setState(i.state);
+ }
+
+ handleModBanShow(i: CommentNode) {
+ i.state.showBanDialog = !i.state.showBanDialog;
+ i.state.banType = BanType.Site;
+ i.setState(i.state);
+ }
+
+ handleModBanReasonChange(i: CommentNode, event: any) {
+ i.state.banReason = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleModBanExpiresChange(i: CommentNode, event: any) {
+ i.state.banExpires = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleModBanFromCommunitySubmit(i: CommentNode) {
+ i.state.banType = BanType.Community;
+ i.setState(i.state);
+ i.handleModBanBothSubmit(i);
+ }
+
+ handleModBanSubmit(i: CommentNode) {
+ i.state.banType = BanType.Site;
+ i.setState(i.state);
+ i.handleModBanBothSubmit(i);
+ }
+
+ handleModBanBothSubmit(i: CommentNode) {
+ event.preventDefault();
+
+ if (i.state.banType == BanType.Community) {
+ // If its an unban, restore all their data
+ let ban = !i.props.node.comment.banned_from_community;
+ if (ban == false) {
+ i.state.removeData = false;
+ }
+ let form: BanFromCommunityForm = {
+ user_id: i.props.node.comment.creator_id,
+ community_id: i.props.node.comment.community_id,
+ ban,
+ remove_data: i.state.removeData,
+ reason: i.state.banReason,
+ expires: getUnixTime(i.state.banExpires),
+ };
+ WebSocketService.Instance.banFromCommunity(form);
+ } else {
+ // If its an unban, restore all their data
+ let ban = !i.props.node.comment.banned;
+ if (ban == false) {
+ i.state.removeData = false;
+ }
+ let form: BanUserForm = {
+ user_id: i.props.node.comment.creator_id,
+ ban,
+ remove_data: i.state.removeData,
+ reason: i.state.banReason,
+ expires: getUnixTime(i.state.banExpires),
+ };
+ WebSocketService.Instance.banUser(form);
+ }
+
+ i.state.showBanDialog = false;
+ i.setState(i.state);
+ }
+
+ handleShowConfirmAppointAsMod(i: CommentNode) {
+ i.state.showConfirmAppointAsMod = true;
+ i.setState(i.state);
+ }
+
+ handleCancelConfirmAppointAsMod(i: CommentNode) {
+ i.state.showConfirmAppointAsMod = false;
+ i.setState(i.state);
+ }
+
+ handleAddModToCommunity(i: CommentNode) {
+ let form: AddModToCommunityForm = {
+ user_id: i.props.node.comment.creator_id,
+ community_id: i.props.node.comment.community_id,
+ added: !i.isMod,
+ };
+ WebSocketService.Instance.addModToCommunity(form);
+ i.state.showConfirmAppointAsMod = false;
+ i.setState(i.state);
+ }
+
+ handleShowConfirmAppointAsAdmin(i: CommentNode) {
+ i.state.showConfirmAppointAsAdmin = true;
+ i.setState(i.state);
+ }
+
+ handleCancelConfirmAppointAsAdmin(i: CommentNode) {
+ i.state.showConfirmAppointAsAdmin = false;
+ i.setState(i.state);
+ }
+
+ handleAddAdmin(i: CommentNode) {
+ let form: AddAdminForm = {
+ user_id: i.props.node.comment.creator_id,
+ added: !i.isAdmin,
+ };
+ WebSocketService.Instance.addAdmin(form);
+ i.state.showConfirmAppointAsAdmin = false;
+ i.setState(i.state);
+ }
+
+ handleShowConfirmTransferCommunity(i: CommentNode) {
+ i.state.showConfirmTransferCommunity = true;
+ i.setState(i.state);
+ }
+
+ handleCancelShowConfirmTransferCommunity(i: CommentNode) {
+ i.state.showConfirmTransferCommunity = false;
+ i.setState(i.state);
+ }
+
+ handleTransferCommunity(i: CommentNode) {
+ let form: TransferCommunityForm = {
+ community_id: i.props.node.comment.community_id,
+ user_id: i.props.node.comment.creator_id,
+ };
+ WebSocketService.Instance.transferCommunity(form);
+ i.state.showConfirmTransferCommunity = false;
+ i.setState(i.state);
+ }
+
+ handleShowConfirmTransferSite(i: CommentNode) {
+ i.state.showConfirmTransferSite = true;
+ i.setState(i.state);
+ }
+
+ handleCancelShowConfirmTransferSite(i: CommentNode) {
+ i.state.showConfirmTransferSite = false;
+ i.setState(i.state);
+ }
+
+ handleTransferSite(i: CommentNode) {
+ let form: TransferSiteForm = {
+ user_id: i.props.node.comment.creator_id,
+ };
+ WebSocketService.Instance.transferSite(form);
+ i.state.showConfirmTransferSite = false;
+ i.setState(i.state);
+ }
+
+ get isCommentNew(): boolean {
+ let now = moment.utc().subtract(10, 'minutes');
+ let then = moment.utc(this.props.node.comment.published);
+ return now.isBefore(then);
+ }
+
+ handleCommentCollapse(i: CommentNode) {
+ i.state.collapsed = !i.state.collapsed;
+ i.setState(i.state);
+ }
+
+ handleViewSource(i: CommentNode) {
+ i.state.viewSource = !i.state.viewSource;
+ i.setState(i.state);
+ }
+
+ handleShowAdvanced(i: CommentNode) {
+ i.state.showAdvanced = !i.state.showAdvanced;
+ i.setState(i.state);
+ setupTippy();
+ }
+
+ get scoreColor() {
+ if (this.state.my_vote == 1) {
+ return 'text-info';
+ } else if (this.state.my_vote == -1) {
+ return 'text-danger';
+ } else {
+ return 'text-muted';
+ }
+ }
+
+ get pointsTippy(): string {
+ let points = i18n.t('number_of_points', {
+ count: this.state.score,
+ });
+
+ let upvotes = i18n.t('number_of_upvotes', {
+ count: this.state.upvotes,
+ });
+
+ let downvotes = i18n.t('number_of_downvotes', {
+ count: this.state.downvotes,
+ });
+
+ return `${points} • ${upvotes} • ${downvotes}`;
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import { CommentSortType } from '../interfaces';
+import {
+ CommentNode as CommentNodeI,
+ CommunityUser,
+ UserView,
+ SortType,
+} from 'lemmy-js-client';
+import { commentSort, commentSortSortType } from '../utils';
+import { CommentNode } from './comment-node';
+
+interface CommentNodesState {}
+
+interface CommentNodesProps {
+ nodes: CommentNodeI[];
+ moderators?: CommunityUser[];
+ admins?: UserView[];
+ postCreatorId?: number;
+ noBorder?: boolean;
+ noIndent?: boolean;
+ viewOnly?: boolean;
+ locked?: boolean;
+ markable?: boolean;
+ showContext?: boolean;
+ showCommunity?: boolean;
+ sort?: CommentSortType;
+ sortType?: SortType;
+ enableDownvotes: boolean;
+}
+
+export class CommentNodes extends Component<
+ CommentNodesProps,
+ CommentNodesState
+> {
+ constructor(props: any, context: any) {
+ super(props, context);
+ }
+
+ render() {
+ return (
+ <div className="comments">
+ {this.sorter().map(node => (
+ <CommentNode
+ key={node.comment.id}
+ node={node}
+ noBorder={this.props.noBorder}
+ noIndent={this.props.noIndent}
+ viewOnly={this.props.viewOnly}
+ locked={this.props.locked}
+ moderators={this.props.moderators}
+ admins={this.props.admins}
+ postCreatorId={this.props.postCreatorId}
+ markable={this.props.markable}
+ showContext={this.props.showContext}
+ showCommunity={this.props.showCommunity}
+ sort={this.props.sort}
+ sortType={this.props.sortType}
+ enableDownvotes={this.props.enableDownvotes}
+ />
+ ))}
+ </div>
+ );
+ }
+
+ sorter(): CommentNodeI[] {
+ if (this.props.sort !== undefined) {
+ commentSort(this.props.nodes, this.props.sort);
+ } else if (this.props.sortType !== undefined) {
+ commentSortSortType(this.props.nodes, this.props.sortType);
+ }
+
+ return this.props.nodes;
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ UserOperation,
+ Community,
+ ListCommunitiesResponse,
+ CommunityResponse,
+ FollowCommunityForm,
+ ListCommunitiesForm,
+ SortType,
+ WebSocketJsonResponse,
+ GetSiteResponse,
+ Site,
+} from 'lemmy-js-client';
+import { WebSocketService } from '../services';
+import { wsJsonToRes, toast, getPageFromProps } from '../utils';
+import { CommunityLink } from './community-link';
+import { i18n } from '../i18next';
+
+declare const Sortable: any;
+
+const communityLimit = 100;
+
+interface CommunitiesState {
+ communities: Community[];
+ page: number;
+ loading: boolean;
+ site: Site;
+}
+
+interface CommunitiesProps {
+ page: number;
+}
+
+export class Communities extends Component<any, CommunitiesState> {
+ private subscription: Subscription;
+ private emptyState: CommunitiesState = {
+ communities: [],
+ loading: true,
+ page: getPageFromProps(this.props),
+ site: undefined,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.state = this.emptyState;
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ this.refetch();
+ WebSocketService.Instance.getSite();
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ static getDerivedStateFromProps(props: any): CommunitiesProps {
+ return {
+ page: getPageFromProps(props),
+ };
+ }
+
+ componentDidUpdate(_: any, lastState: CommunitiesState) {
+ if (lastState.page !== this.state.page) {
+ this.setState({ loading: true });
+ this.refetch();
+ }
+ }
+
+ get documentTitle(): string {
+ if (this.state.site) {
+ return `${i18n.t('communities')} - ${this.state.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle} />
+ {this.state.loading ? (
+ <h5 class="">
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ </h5>
+ ) : (
+ <div>
+ <h5>{i18n.t('list_of_communities')}</h5>
+ <div class="table-responsive">
+ <table id="community_table" class="table table-sm table-hover">
+ <thead class="pointer">
+ <tr>
+ <th>{i18n.t('name')}</th>
+ <th>{i18n.t('category')}</th>
+ <th class="text-right">{i18n.t('subscribers')}</th>
+ <th class="text-right d-none d-lg-table-cell">
+ {i18n.t('posts')}
+ </th>
+ <th class="text-right d-none d-lg-table-cell">
+ {i18n.t('comments')}
+ </th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {this.state.communities.map(community => (
+ <tr>
+ <td>
+ <CommunityLink community={community} />
+ </td>
+ <td>{community.category_name}</td>
+ <td class="text-right">
+ {community.number_of_subscribers}
+ </td>
+ <td class="text-right d-none d-lg-table-cell">
+ {community.number_of_posts}
+ </td>
+ <td class="text-right d-none d-lg-table-cell">
+ {community.number_of_comments}
+ </td>
+ <td class="text-right">
+ {community.subscribed ? (
+ <span
+ class="pointer btn-link"
+ onClick={linkEvent(
+ community.id,
+ this.handleUnsubscribe
+ )}
+ >
+ {i18n.t('unsubscribe')}
+ </span>
+ ) : (
+ <span
+ class="pointer btn-link"
+ onClick={linkEvent(
+ community.id,
+ this.handleSubscribe
+ )}
+ >
+ {i18n.t('subscribe')}
+ </span>
+ )}
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ {this.paginator()}
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ paginator() {
+ return (
+ <div class="mt-2">
+ {this.state.page > 1 && (
+ <button
+ class="btn btn-secondary mr-1"
+ onClick={linkEvent(this, this.prevPage)}
+ >
+ {i18n.t('prev')}
+ </button>
+ )}
+
+ {this.state.communities.length > 0 && (
+ <button
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.nextPage)}
+ >
+ {i18n.t('next')}
+ </button>
+ )}
+ </div>
+ );
+ }
+
+ updateUrl(paramUpdates: CommunitiesProps) {
+ const page = paramUpdates.page || this.state.page;
+ this.props.history.push(`/communities/page/${page}`);
+ }
+
+ nextPage(i: Communities) {
+ i.updateUrl({ page: i.state.page + 1 });
+ }
+
+ prevPage(i: Communities) {
+ i.updateUrl({ page: i.state.page - 1 });
+ }
+
+ handleUnsubscribe(communityId: number) {
+ let form: FollowCommunityForm = {
+ community_id: communityId,
+ follow: false,
+ };
+ WebSocketService.Instance.followCommunity(form);
+ }
+
+ handleSubscribe(communityId: number) {
+ let form: FollowCommunityForm = {
+ community_id: communityId,
+ follow: true,
+ };
+ WebSocketService.Instance.followCommunity(form);
+ }
+
+ refetch() {
+ let listCommunitiesForm: ListCommunitiesForm = {
+ sort: SortType.TopAll,
+ limit: communityLimit,
+ page: this.state.page,
+ };
+
+ WebSocketService.Instance.listCommunities(listCommunitiesForm);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ return;
+ } else if (res.op == UserOperation.ListCommunities) {
+ let data = res.data as ListCommunitiesResponse;
+ this.state.communities = data.communities;
+ this.state.communities.sort(
+ (a, b) => b.number_of_subscribers - a.number_of_subscribers
+ );
+ this.state.loading = false;
+ window.scrollTo(0, 0);
+ this.setState(this.state);
+ let table = document.querySelector('#community_table');
+ Sortable.initTable(table);
+ } else if (res.op == UserOperation.FollowCommunity) {
+ let data = res.data as CommunityResponse;
+ let found = this.state.communities.find(c => c.id == data.community.id);
+ found.subscribed = data.community.subscribed;
+ found.number_of_subscribers = data.community.number_of_subscribers;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.site = data.site;
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Prompt } from 'inferno-router';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ CommunityForm as CommunityFormI,
+ UserOperation,
+ Category,
+ ListCategoriesResponse,
+ CommunityResponse,
+ WebSocketJsonResponse,
+ Community,
+} from 'lemmy-js-client';
+import { WebSocketService } from '../services';
+import { wsJsonToRes, capitalizeFirstLetter, toast, randomStr } from '../utils';
+import { i18n } from '../i18next';
+
+import { MarkdownTextArea } from './markdown-textarea';
+import { ImageUploadForm } from './image-upload-form';
+
+interface CommunityFormProps {
+ community?: Community; // If a community is given, that means this is an edit
+ onCancel?(): any;
+ onCreate?(community: Community): any;
+ onEdit?(community: Community): any;
+ enableNsfw: boolean;
+}
+
+interface CommunityFormState {
+ communityForm: CommunityFormI;
+ categories: Category[];
+ loading: boolean;
+}
+
+export class CommunityForm extends Component<
+ CommunityFormProps,
+ CommunityFormState
+> {
+ private id = `community-form-${randomStr()}`;
+ private subscription: Subscription;
+
+ private emptyState: CommunityFormState = {
+ communityForm: {
+ name: null,
+ title: null,
+ category_id: null,
+ nsfw: false,
+ icon: null,
+ banner: null,
+ },
+ categories: [],
+ loading: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ this.handleCommunityDescriptionChange = this.handleCommunityDescriptionChange.bind(
+ this
+ );
+
+ this.handleIconUpload = this.handleIconUpload.bind(this);
+ this.handleIconRemove = this.handleIconRemove.bind(this);
+
+ this.handleBannerUpload = this.handleBannerUpload.bind(this);
+ this.handleBannerRemove = this.handleBannerRemove.bind(this);
+
+ if (this.props.community) {
+ this.state.communityForm = {
+ name: this.props.community.name,
+ title: this.props.community.title,
+ category_id: this.props.community.category_id,
+ description: this.props.community.description,
+ edit_id: this.props.community.id,
+ nsfw: this.props.community.nsfw,
+ icon: this.props.community.icon,
+ banner: this.props.community.banner,
+ auth: null,
+ };
+ }
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ WebSocketService.Instance.listCategories();
+ }
+
+ componentDidUpdate() {
+ if (
+ !this.state.loading &&
+ (this.state.communityForm.name ||
+ this.state.communityForm.title ||
+ this.state.communityForm.description)
+ ) {
+ window.onbeforeunload = () => true;
+ } else {
+ window.onbeforeunload = undefined;
+ }
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ window.onbeforeunload = null;
+ }
+
+ render() {
+ return (
+ <>
+ <Prompt
+ when={
+ !this.state.loading &&
+ (this.state.communityForm.name ||
+ this.state.communityForm.title ||
+ this.state.communityForm.description)
+ }
+ message={i18n.t('block_leaving')}
+ />
+ <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
+ {!this.props.community && (
+ <div class="form-group row">
+ <label class="col-12 col-form-label" htmlFor="community-name">
+ {i18n.t('name')}
+ <span
+ class="pointer unselectable ml-2 text-muted"
+ data-tippy-content={i18n.t('name_explain')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-help-circle"></use>
+ </svg>
+ </span>
+ </label>
+ <div class="col-12">
+ <input
+ type="text"
+ id="community-name"
+ class="form-control"
+ value={this.state.communityForm.name}
+ onInput={linkEvent(this, this.handleCommunityNameChange)}
+ required
+ minLength={3}
+ maxLength={20}
+ pattern="[a-z0-9_]+"
+ title={i18n.t('community_reqs')}
+ />
+ </div>
+ </div>
+ )}
+ <div class="form-group row">
+ <label class="col-12 col-form-label" htmlFor="community-title">
+ {i18n.t('display_name')}
+ <span
+ class="pointer unselectable ml-2 text-muted"
+ data-tippy-content={i18n.t('display_name_explain')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-help-circle"></use>
+ </svg>
+ </span>
+ </label>
+ <div class="col-12">
+ <input
+ type="text"
+ id="community-title"
+ value={this.state.communityForm.title}
+ onInput={linkEvent(this, this.handleCommunityTitleChange)}
+ class="form-control"
+ required
+ minLength={3}
+ maxLength={100}
+ />
+ </div>
+ </div>
+ <div class="form-group">
+ <label>{i18n.t('icon')}</label>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_icon')}
+ imageSrc={this.state.communityForm.icon}
+ onUpload={this.handleIconUpload}
+ onRemove={this.handleIconRemove}
+ rounded
+ />
+ </div>
+ <div class="form-group">
+ <label>{i18n.t('banner')}</label>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_banner')}
+ imageSrc={this.state.communityForm.banner}
+ onUpload={this.handleBannerUpload}
+ onRemove={this.handleBannerRemove}
+ />
+ </div>
+ <div class="form-group row">
+ <label class="col-12 col-form-label" htmlFor={this.id}>
+ {i18n.t('sidebar')}
+ </label>
+ <div class="col-12">
+ <MarkdownTextArea
+ initialContent={this.state.communityForm.description}
+ onContentChange={this.handleCommunityDescriptionChange}
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-12 col-form-label" htmlFor="community-category">
+ {i18n.t('category')}
+ </label>
+ <div class="col-12">
+ <select
+ class="form-control"
+ id="community-category"
+ value={this.state.communityForm.category_id}
+ onInput={linkEvent(this, this.handleCommunityCategoryChange)}
+ >
+ {this.state.categories.map(category => (
+ <option value={category.id}>{category.name}</option>
+ ))}
+ </select>
+ </div>
+ </div>
+
+ {this.props.enableNsfw && (
+ <div class="form-group row">
+ <div class="col-12">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="community-nsfw"
+ type="checkbox"
+ checked={this.state.communityForm.nsfw}
+ onChange={linkEvent(this, this.handleCommunityNsfwChange)}
+ />
+ <label class="form-check-label" htmlFor="community-nsfw">
+ {i18n.t('nsfw')}
+ </label>
+ </div>
+ </div>
+ </div>
+ )}
+ <div class="form-group row">
+ <div class="col-12">
+ <button
+ type="submit"
+ class="btn btn-secondary mr-2"
+ disabled={this.state.loading}
+ >
+ {this.state.loading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : this.props.community ? (
+ capitalizeFirstLetter(i18n.t('save'))
+ ) : (
+ capitalizeFirstLetter(i18n.t('create'))
+ )}
+ </button>
+ {this.props.community && (
+ <button
+ type="button"
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.handleCancel)}
+ >
+ {i18n.t('cancel')}
+ </button>
+ )}
+ </div>
+ </div>
+ </form>
+ </>
+ );
+ }
+
+ handleCreateCommunitySubmit(i: CommunityForm, event: any) {
+ event.preventDefault();
+ i.state.loading = true;
+ if (i.props.community) {
+ WebSocketService.Instance.editCommunity(i.state.communityForm);
+ } else {
+ WebSocketService.Instance.createCommunity(i.state.communityForm);
+ }
+ i.setState(i.state);
+ }
+
+ handleCommunityNameChange(i: CommunityForm, event: any) {
+ i.state.communityForm.name = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleCommunityTitleChange(i: CommunityForm, event: any) {
+ i.state.communityForm.title = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleCommunityDescriptionChange(val: string) {
+ this.state.communityForm.description = val;
+ this.setState(this.state);
+ }
+
+ handleCommunityCategoryChange(i: CommunityForm, event: any) {
+ i.state.communityForm.category_id = Number(event.target.value);
+ i.setState(i.state);
+ }
+
+ handleCommunityNsfwChange(i: CommunityForm, event: any) {
+ i.state.communityForm.nsfw = event.target.checked;
+ i.setState(i.state);
+ }
+
+ handleCancel(i: CommunityForm) {
+ i.props.onCancel();
+ }
+
+ handleIconUpload(url: string) {
+ this.state.communityForm.icon = url;
+ this.setState(this.state);
+ }
+
+ handleIconRemove() {
+ this.state.communityForm.icon = '';
+ this.setState(this.state);
+ }
+
+ handleBannerUpload(url: string) {
+ this.state.communityForm.banner = url;
+ this.setState(this.state);
+ }
+
+ handleBannerRemove() {
+ this.state.communityForm.banner = '';
+ this.setState(this.state);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+ console.log(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ this.state.loading = false;
+ this.setState(this.state);
+ return;
+ } else if (res.op == UserOperation.ListCategories) {
+ let data = res.data as ListCategoriesResponse;
+ this.state.categories = data.categories;
+ if (!this.props.community) {
+ this.state.communityForm.category_id = data.categories[0].id;
+ }
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreateCommunity) {
+ let data = res.data as CommunityResponse;
+ this.state.loading = false;
+ this.props.onCreate(data.community);
+ } else if (res.op == UserOperation.EditCommunity) {
+ let data = res.data as CommunityResponse;
+ this.state.loading = false;
+ this.props.onEdit(data.community);
+ }
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import { Link } from 'inferno-router';
+import { Community } from 'lemmy-js-client';
+import { hostname, pictrsAvatarThumbnail, showAvatars } from '../utils';
+
+interface CommunityOther {
+ name: string;
+ id?: number; // Necessary if its federated
+ icon?: string;
+ local?: boolean;
+ actor_id?: string;
+}
+
+interface CommunityLinkProps {
+ community: Community | CommunityOther;
+ realLink?: boolean;
+ useApubName?: boolean;
+ muted?: boolean;
+ hideAvatar?: boolean;
+}
+
+export class CommunityLink extends Component<CommunityLinkProps, any> {
+ constructor(props: any, context: any) {
+ super(props, context);
+ }
+
+ render() {
+ let community = this.props.community;
+ let name_: string, link: string;
+ let local = community.local == null ? true : community.local;
+ if (local) {
+ name_ = community.name;
+ link = `/c/${community.name}`;
+ } else {
+ name_ = `${community.name}@${hostname(community.actor_id)}`;
+ link = !this.props.realLink
+ ? `/community/${community.id}`
+ : community.actor_id;
+ }
+
+ let apubName = `!${name_}`;
+ let displayName = this.props.useApubName ? apubName : name_;
+ return (
+ <Link
+ title={apubName}
+ className={`${this.props.muted ? 'text-muted' : ''}`}
+ to={link}
+ >
+ {!this.props.hideAvatar && community.icon && showAvatars() && (
+ <img
+ style="width: 2rem; height: 2rem;"
+ src={pictrsAvatarThumbnail(community.icon)}
+ class="rounded-circle mr-2"
+ />
+ )}
+ <span>{displayName}</span>
+ </Link>
+ );
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { DataType } from '../interfaces';
+import {
+ UserOperation,
+ Community as CommunityI,
+ GetCommunityResponse,
+ CommunityResponse,
+ CommunityUser,
+ UserView,
+ SortType,
+ Post,
+ GetPostsForm,
+ GetCommunityForm,
+ ListingType,
+ GetPostsResponse,
+ PostResponse,
+ AddModToCommunityResponse,
+ BanFromCommunityResponse,
+ Comment,
+ GetCommentsForm,
+ GetCommentsResponse,
+ CommentResponse,
+ WebSocketJsonResponse,
+ GetSiteResponse,
+ Site,
+} from 'lemmy-js-client';
+import { WebSocketService } from '../services';
+import { PostListings } from './post-listings';
+import { CommentNodes } from './comment-nodes';
+import { SortSelect } from './sort-select';
+import { DataTypeSelect } from './data-type-select';
+import { Sidebar } from './sidebar';
+import { CommunityLink } from './community-link';
+import { BannerIconHeader } from './banner-icon-header';
+import {
+ wsJsonToRes,
+ fetchLimit,
+ toast,
+ getPageFromProps,
+ getSortTypeFromProps,
+ getDataTypeFromProps,
+ editCommentRes,
+ saveCommentRes,
+ createCommentLikeRes,
+ createPostLikeFindRes,
+ editPostFindRes,
+ commentsToFlatNodes,
+ setupTippy,
+ favIconUrl,
+ notifyPost,
+} from '../utils';
+import { i18n } from '../i18next';
+
+interface State {
+ community: CommunityI;
+ communityId: number;
+ communityName: string;
+ moderators: CommunityUser[];
+ admins: UserView[];
+ online: number;
+ loading: boolean;
+ posts: Post[];
+ comments: Comment[];
+ dataType: DataType;
+ sort: SortType;
+ page: number;
+ site: Site;
+}
+
+interface CommunityProps {
+ dataType: DataType;
+ sort: SortType;
+ page: number;
+}
+
+interface UrlParams {
+ dataType?: string;
+ sort?: SortType;
+ page?: number;
+}
+
+export class Community extends Component<any, State> {
+ private subscription: Subscription;
+ private emptyState: State = {
+ community: {
+ id: null,
+ name: null,
+ title: null,
+ category_id: null,
+ category_name: null,
+ creator_id: null,
+ creator_name: null,
+ number_of_subscribers: null,
+ number_of_posts: null,
+ number_of_comments: null,
+ published: null,
+ removed: null,
+ nsfw: false,
+ deleted: null,
+ local: null,
+ actor_id: null,
+ last_refreshed_at: null,
+ creator_actor_id: null,
+ creator_local: null,
+ },
+ moderators: [],
+ admins: [],
+ communityId: Number(this.props.match.params.id),
+ communityName: this.props.match.params.name,
+ online: null,
+ loading: true,
+ posts: [],
+ comments: [],
+ dataType: getDataTypeFromProps(this.props),
+ sort: getSortTypeFromProps(this.props),
+ page: getPageFromProps(this.props),
+ site: {
+ id: undefined,
+ name: undefined,
+ creator_id: undefined,
+ published: undefined,
+ creator_name: undefined,
+ number_of_users: undefined,
+ number_of_posts: undefined,
+ number_of_comments: undefined,
+ number_of_communities: undefined,
+ enable_downvotes: undefined,
+ open_registration: undefined,
+ enable_nsfw: undefined,
+ icon: undefined,
+ banner: undefined,
+ creator_preferred_username: undefined,
+ },
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handleSortChange = this.handleSortChange.bind(this);
+ this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ let form: GetCommunityForm = {
+ id: this.state.communityId ? this.state.communityId : null,
+ name: this.state.communityName ? this.state.communityName : null,
+ };
+ WebSocketService.Instance.getCommunity(form);
+ WebSocketService.Instance.getSite();
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ static getDerivedStateFromProps(props: any): CommunityProps {
+ return {
+ dataType: getDataTypeFromProps(props),
+ sort: getSortTypeFromProps(props),
+ page: getPageFromProps(props),
+ };
+ }
+
+ componentDidUpdate(_: any, lastState: State) {
+ if (
+ lastState.dataType !== this.state.dataType ||
+ lastState.sort !== this.state.sort ||
+ lastState.page !== this.state.page
+ ) {
+ this.setState({ loading: true });
+ this.fetchData();
+ }
+ }
+
+ get documentTitle(): string {
+ if (this.state.community.title) {
+ return `${this.state.community.title} - ${this.state.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ get favIcon(): string {
+ return this.state.site.icon ? this.state.site.icon : favIconUrl;
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle}>
+ <link
+ id="favicon"
+ rel="icon"
+ type="image/x-icon"
+ href={this.favIcon}
+ />
+ </Helmet>
+ {this.state.loading ? (
+ <h5>
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ </h5>
+ ) : (
+ <div class="row">
+ <div class="col-12 col-md-8">
+ {this.communityInfo()}
+ {this.selects()}
+ {this.listings()}
+ {this.paginator()}
+ </div>
+ <div class="col-12 col-md-4">
+ <Sidebar
+ community={this.state.community}
+ moderators={this.state.moderators}
+ admins={this.state.admins}
+ online={this.state.online}
+ enableNsfw={this.state.site.enable_nsfw}
+ />
+ </div>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ listings() {
+ return this.state.dataType == DataType.Post ? (
+ <PostListings
+ posts={this.state.posts}
+ removeDuplicates
+ sort={this.state.sort}
+ enableDownvotes={this.state.site.enable_downvotes}
+ enableNsfw={this.state.site.enable_nsfw}
+ />
+ ) : (
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.comments)}
+ noIndent
+ sortType={this.state.sort}
+ showContext
+ enableDownvotes={this.state.site.enable_downvotes}
+ />
+ );
+ }
+
+ communityInfo() {
+ return (
+ <div>
+ <BannerIconHeader
+ banner={this.state.community.banner}
+ icon={this.state.community.icon}
+ />
+ <h5 class="mb-0">{this.state.community.title}</h5>
+ <CommunityLink
+ community={this.state.community}
+ realLink
+ useApubName
+ muted
+ hideAvatar
+ />
+ <hr />
+ </div>
+ );
+ }
+
+ selects() {
+ return (
+ <div class="mb-3">
+ <span class="mr-3">
+ <DataTypeSelect
+ type_={this.state.dataType}
+ onChange={this.handleDataTypeChange}
+ />
+ </span>
+ <span class="mr-2">
+ <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
+ </span>
+ <a
+ href={`/feeds/c/${this.state.communityName}.xml?sort=${this.state.sort}`}
+ target="_blank"
+ title="RSS"
+ rel="noopener"
+ >
+ <svg class="icon text-muted small">
+ <use xlinkHref="#icon-rss">#</use>
+ </svg>
+ </a>
+ </div>
+ );
+ }
+
+ paginator() {
+ return (
+ <div class="my-2">
+ {this.state.page > 1 && (
+ <button
+ class="btn btn-secondary mr-1"
+ onClick={linkEvent(this, this.prevPage)}
+ >
+ {i18n.t('prev')}
+ </button>
+ )}
+ {this.state.posts.length > 0 && (
+ <button
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.nextPage)}
+ >
+ {i18n.t('next')}
+ </button>
+ )}
+ </div>
+ );
+ }
+
+ nextPage(i: Community) {
+ i.updateUrl({ page: i.state.page + 1 });
+ window.scrollTo(0, 0);
+ }
+
+ prevPage(i: Community) {
+ i.updateUrl({ page: i.state.page - 1 });
+ window.scrollTo(0, 0);
+ }
+
+ handleSortChange(val: SortType) {
+ this.updateUrl({ sort: val, page: 1 });
+ window.scrollTo(0, 0);
+ }
+
+ handleDataTypeChange(val: DataType) {
+ this.updateUrl({ dataType: DataType[val], page: 1 });
+ window.scrollTo(0, 0);
+ }
+
+ updateUrl(paramUpdates: UrlParams) {
+ const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
+ const sortStr = paramUpdates.sort || this.state.sort;
+ const page = paramUpdates.page || this.state.page;
+ this.props.history.push(
+ `/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${page}`
+ );
+ }
+
+ fetchData() {
+ if (this.state.dataType == DataType.Post) {
+ let getPostsForm: GetPostsForm = {
+ page: this.state.page,
+ limit: fetchLimit,
+ sort: this.state.sort,
+ type_: ListingType.Community,
+ community_id: this.state.community.id,
+ };
+ WebSocketService.Instance.getPosts(getPostsForm);
+ } else {
+ let getCommentsForm: GetCommentsForm = {
+ page: this.state.page,
+ limit: fetchLimit,
+ sort: this.state.sort,
+ type_: ListingType.Community,
+ community_id: this.state.community.id,
+ };
+ WebSocketService.Instance.getComments(getCommentsForm);
+ }
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ this.context.router.history.push('/');
+ return;
+ } else if (msg.reconnect) {
+ this.fetchData();
+ } else if (res.op == UserOperation.GetCommunity) {
+ let data = res.data as GetCommunityResponse;
+ this.state.community = data.community;
+ this.state.moderators = data.moderators;
+ this.state.online = data.online;
+ this.setState(this.state);
+ this.fetchData();
+ } else if (
+ res.op == UserOperation.EditCommunity ||
+ res.op == UserOperation.DeleteCommunity ||
+ res.op == UserOperation.RemoveCommunity
+ ) {
+ let data = res.data as CommunityResponse;
+ this.state.community = data.community;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.FollowCommunity) {
+ let data = res.data as CommunityResponse;
+ this.state.community.subscribed = data.community.subscribed;
+ this.state.community.number_of_subscribers =
+ data.community.number_of_subscribers;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.GetPosts) {
+ let data = res.data as GetPostsResponse;
+ this.state.posts = data.posts;
+ this.state.loading = false;
+ this.setState(this.state);
+ setupTippy();
+ } else if (
+ res.op == UserOperation.EditPost ||
+ res.op == UserOperation.DeletePost ||
+ res.op == UserOperation.RemovePost ||
+ res.op == UserOperation.LockPost ||
+ res.op == UserOperation.StickyPost
+ ) {
+ let data = res.data as PostResponse;
+ editPostFindRes(data, this.state.posts);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreatePost) {
+ let data = res.data as PostResponse;
+ this.state.posts.unshift(data.post);
+ notifyPost(data.post, this.context.router);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreatePostLike) {
+ let data = res.data as PostResponse;
+ createPostLikeFindRes(data, this.state.posts);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.AddModToCommunity) {
+ let data = res.data as AddModToCommunityResponse;
+ this.state.moderators = data.moderators;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.BanFromCommunity) {
+ let data = res.data as BanFromCommunityResponse;
+
+ this.state.posts
+ .filter(p => p.creator_id == data.user.id)
+ .forEach(p => (p.banned = data.banned));
+
+ this.setState(this.state);
+ } else if (res.op == UserOperation.GetComments) {
+ let data = res.data as GetCommentsResponse;
+ this.state.comments = data.comments;
+ this.state.loading = false;
+ this.setState(this.state);
+ } else if (
+ res.op == UserOperation.EditComment ||
+ res.op == UserOperation.DeleteComment ||
+ res.op == UserOperation.RemoveComment
+ ) {
+ let data = res.data as CommentResponse;
+ editCommentRes(data, this.state.comments);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreateComment) {
+ let data = res.data as CommentResponse;
+
+ // Necessary since it might be a user reply
+ if (data.recipient_ids.length == 0) {
+ this.state.comments.unshift(data.comment);
+ this.setState(this.state);
+ }
+ } else if (res.op == UserOperation.SaveComment) {
+ let data = res.data as CommentResponse;
+ saveCommentRes(data, this.state.comments);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreateCommentLike) {
+ let data = res.data as CommentResponse;
+ createCommentLikeRes(data, this.state.comments);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.site = data.site;
+ this.state.admins = data.admins;
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { CommunityForm } from './community-form';
+import {
+ Community,
+ UserOperation,
+ WebSocketJsonResponse,
+ GetSiteResponse,
+ Site,
+} from 'lemmy-js-client';
+import { toast, wsJsonToRes } from '../utils';
+import { WebSocketService, UserService } from '../services';
+import { i18n } from '../i18next';
+
+interface CreateCommunityState {
+ site: Site;
+}
+
+export class CreateCommunity extends Component<any, CreateCommunityState> {
+ private subscription: Subscription;
+ private emptyState: CreateCommunityState = {
+ site: {
+ id: undefined,
+ name: undefined,
+ creator_id: undefined,
+ published: undefined,
+ creator_name: undefined,
+ number_of_users: undefined,
+ number_of_posts: undefined,
+ number_of_comments: undefined,
+ number_of_communities: undefined,
+ enable_downvotes: undefined,
+ open_registration: undefined,
+ enable_nsfw: undefined,
+ },
+ };
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
+ this.state = this.emptyState;
+
+ if (!UserService.Instance.user) {
+ toast(i18n.t('not_logged_in'), 'danger');
+ this.context.router.history.push(`/login`);
+ }
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ WebSocketService.Instance.getSite();
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ get documentTitle(): string {
+ if (this.state.site.name) {
+ return `${i18n.t('create_community')} - ${this.state.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle} />
+ <div class="row">
+ <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+ <h5>{i18n.t('create_community')}</h5>
+ <CommunityForm
+ onCreate={this.handleCommunityCreate}
+ enableNsfw={this.state.site.enable_nsfw}
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ handleCommunityCreate(community: Community) {
+ this.props.history.push(`/c/${community.name}`);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ // Toast errors are already handled by community-form
+ return;
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.site = data.site;
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { PostForm } from './post-form';
+import { toast, wsJsonToRes } from '../utils';
+import { WebSocketService, UserService } from '../services';
+import {
+ UserOperation,
+ PostFormParams,
+ WebSocketJsonResponse,
+ GetSiteResponse,
+ Site,
+} from 'lemmy-js-client';
+import { i18n } from '../i18next';
+
+interface CreatePostState {
+ site: Site;
+}
+
+export class CreatePost extends Component<any, CreatePostState> {
+ private subscription: Subscription;
+ private emptyState: CreatePostState = {
+ site: {
+ id: undefined,
+ name: undefined,
+ creator_id: undefined,
+ published: undefined,
+ creator_name: undefined,
+ number_of_users: undefined,
+ number_of_posts: undefined,
+ number_of_comments: undefined,
+ number_of_communities: undefined,
+ enable_downvotes: undefined,
+ open_registration: undefined,
+ enable_nsfw: undefined,
+ },
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.handlePostCreate = this.handlePostCreate.bind(this);
+ this.state = this.emptyState;
+
+ if (!UserService.Instance.user) {
+ toast(i18n.t('not_logged_in'), 'danger');
+ this.context.router.history.push(`/login`);
+ }
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ WebSocketService.Instance.getSite();
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ get documentTitle(): string {
+ if (this.state.site.name) {
+ return `${i18n.t('create_post')} - ${this.state.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle} />
+ <div class="row">
+ <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+ <h5>{i18n.t('create_post')}</h5>
+ <PostForm
+ onCreate={this.handlePostCreate}
+ params={this.params}
+ enableDownvotes={this.state.site.enable_downvotes}
+ enableNsfw={this.state.site.enable_nsfw}
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ get params(): PostFormParams {
+ let urlParams = new URLSearchParams(this.props.location.search);
+ let params: PostFormParams = {
+ name: urlParams.get('title'),
+ community: urlParams.get('community') || this.prevCommunityName,
+ body: urlParams.get('body'),
+ url: urlParams.get('url'),
+ };
+
+ return params;
+ }
+
+ get prevCommunityName(): string {
+ if (this.props.match.params.name) {
+ return this.props.match.params.name;
+ } else if (this.props.location.state) {
+ let lastLocation = this.props.location.state.prevPath;
+ if (lastLocation.includes('/c/')) {
+ return lastLocation.split('/c/')[1];
+ }
+ }
+ return;
+ }
+
+ handlePostCreate(id: number) {
+ this.props.history.push(`/post/${id}`);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ return;
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.site = data.site;
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { PrivateMessageForm } from './private-message-form';
+import { WebSocketService, UserService } from '../services';
+import {
+ UserOperation,
+ WebSocketJsonResponse,
+ GetSiteResponse,
+ Site,
+ PrivateMessageFormParams,
+} from 'lemmy-js-client';
+import { toast, wsJsonToRes } from '../utils';
+import { i18n } from '../i18next';
+
+interface CreatePrivateMessageState {
+ site: Site;
+}
+
+export class CreatePrivateMessage extends Component<
+ any,
+ CreatePrivateMessageState
+> {
+ private subscription: Subscription;
+ private emptyState: CreatePrivateMessageState = {
+ site: undefined,
+ };
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.state = this.emptyState;
+ this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
+ this
+ );
+
+ if (!UserService.Instance.user) {
+ toast(i18n.t('not_logged_in'), 'danger');
+ this.context.router.history.push(`/login`);
+ }
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ WebSocketService.Instance.getSite();
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ get documentTitle(): string {
+ if (this.state.site) {
+ return `${i18n.t('create_private_message')} - ${this.state.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle} />
+ <div class="row">
+ <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+ <h5>{i18n.t('create_private_message')}</h5>
+ <PrivateMessageForm
+ onCreate={this.handlePrivateMessageCreate}
+ params={this.params}
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ get params(): PrivateMessageFormParams {
+ let urlParams = new URLSearchParams(this.props.location.search);
+ let params: PrivateMessageFormParams = {
+ recipient_id: Number(urlParams.get('recipient_id')),
+ };
+
+ return params;
+ }
+
+ handlePrivateMessageCreate() {
+ toast(i18n.t('message_sent'));
+
+ // Navigate to the front
+ this.props.history.push(`/`);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ return;
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.site = data.site;
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { DataType } from '../interfaces';
+
+import { i18n } from '../i18next';
+
+interface DataTypeSelectProps {
+ type_: DataType;
+ onChange?(val: DataType): any;
+}
+
+interface DataTypeSelectState {
+ type_: DataType;
+}
+
+export class DataTypeSelect extends Component<
+ DataTypeSelectProps,
+ DataTypeSelectState
+> {
+ private emptyState: DataTypeSelectState = {
+ type_: this.props.type_,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.state = this.emptyState;
+ console.log(this.state);
+ }
+
+ static getDerivedStateFromProps(props: any): DataTypeSelectProps {
+ return {
+ type_: props.type_,
+ };
+ }
+
+ render() {
+ return (
+ <div class="btn-group btn-group-toggle flex-wrap mb-2">
+ <label
+ className={`pointer btn btn-outline-secondary
+ ${this.state.type_ == DataType.Post && 'active'}
+ `}
+ >
+ <input
+ type="radio"
+ value={DataType.Post}
+ checked={this.state.type_ == DataType.Post}
+ onChange={linkEvent(this, this.handleTypeChange)}
+ />
+ {i18n.t('posts')}
+ </label>
+ <label
+ className={`pointer btn btn-outline-secondary ${
+ this.state.type_ == DataType.Comment && 'active'
+ }`}
+ >
+ <input
+ type="radio"
+ value={DataType.Comment}
+ checked={this.state.type_ == DataType.Comment}
+ onChange={linkEvent(this, this.handleTypeChange)}
+ />
+ {i18n.t('comments')}
+ </label>
+ </div>
+ );
+ }
+
+ handleTypeChange(i: DataTypeSelect, event: any) {
+ i.props.onChange(Number(event.target.value));
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import { Link } from 'inferno-router';
+import { i18n } from '../i18next';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { WebSocketService } from '../services';
+import { repoUrl, wsJsonToRes, isBrowser } from '../utils';
+import {
+ UserOperation,
+ WebSocketJsonResponse,
+ GetSiteResponse,
+} from 'lemmy-js-client';
+
+interface FooterState {
+ version: string;
+}
+
+export class Footer extends Component<any, FooterState> {
+ private wsSub: Subscription;
+ emptyState: FooterState = {
+ version: null,
+ };
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ if (isBrowser()) {
+ this.wsSub = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+ }
+ }
+
+ componentWillUnmount() {
+ this.wsSub.unsubscribe();
+ }
+
+ render() {
+ return (
+ <nav class="container navbar navbar-expand-md navbar-light navbar-bg p-0 px-3 mt-2">
+ <div className="navbar-collapse">
+ <ul class="navbar-nav ml-auto">
+ <li class="nav-item">
+ <span class="navbar-text">{this.state.version}</span>
+ </li>
+ <li class="nav-item">
+ <Link class="nav-link" to="/modlog">
+ {i18n.t('modlog')}
+ </Link>
+ </li>
+ <li class="nav-item">
+ <Link class="nav-link" to="/instances">
+ {i18n.t('instances')}
+ </Link>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href={'/docs/index.html'}>
+ {i18n.t('docs')}
+ </a>
+ </li>
+ <li class="nav-item">
+ <Link class="nav-link" to="/sponsors">
+ {i18n.t('donate')}
+ </Link>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href={repoUrl}>
+ {i18n.t('code')}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </nav>
+ );
+ }
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+
+ if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.setState({ version: data.version });
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Post } from 'lemmy-js-client';
+import { mdToHtml } from '../utils';
+import { i18n } from '../i18next';
+
+interface FramelyCardProps {
+ post: Post;
+}
+
+interface FramelyCardState {
+ expanded: boolean;
+}
+
+export class IFramelyCard extends Component<
+ FramelyCardProps,
+ FramelyCardState
+> {
+ private emptyState: FramelyCardState = {
+ expanded: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.state = this.emptyState;
+ }
+
+ render() {
+ let post = this.props.post;
+ return (
+ <>
+ {post.embed_title && !this.state.expanded && (
+ <div class="card bg-transparent border-secondary mt-3 mb-2">
+ <div class="row">
+ <div class="col-12">
+ <div class="card-body">
+ <h5 class="card-title d-inline">
+ {post.embed_html ? (
+ <span
+ class="unselectable pointer"
+ onClick={linkEvent(this, this.handleIframeExpand)}
+ data-tippy-content={i18n.t('expand_here')}
+ >
+ {post.embed_title}
+ </span>
+ ) : (
+ <span>
+ <a
+ class="text-body"
+ target="_blank"
+ href={post.url}
+ rel="noopener"
+ >
+ {post.embed_title}
+ </a>
+ </span>
+ )}
+ </h5>
+ <span class="d-inline-block ml-2 mb-2 small text-muted">
+ <a
+ class="text-muted font-italic"
+ target="_blank"
+ href={post.url}
+ rel="noopener"
+ >
+ {new URL(post.url).hostname}
+ <svg class="ml-1 icon">
+ <use xlinkHref="#icon-external-link"></use>
+ </svg>
+ </a>
+ {post.embed_html && (
+ <span
+ class="ml-2 pointer text-monospace"
+ onClick={linkEvent(this, this.handleIframeExpand)}
+ data-tippy-content={i18n.t('expand_here')}
+ >
+ {this.state.expanded ? '[-]' : '[+]'}
+ </span>
+ )}
+ </span>
+ {post.embed_description && (
+ <div
+ className="card-text small text-muted md-div"
+ dangerouslySetInnerHTML={mdToHtml(post.embed_description)}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ )}
+ {this.state.expanded && (
+ <div
+ class="mt-3 mb-2"
+ dangerouslySetInnerHTML={{ __html: post.embed_html }}
+ />
+ )}
+ </>
+ );
+ }
+
+ handleIframeExpand(i: IFramelyCard) {
+ i.state.expanded = !i.state.expanded;
+ i.setState(i.state);
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { UserService } from '../services';
+import { toast, randomStr } from '../utils';
+
+interface ImageUploadFormProps {
+ uploadTitle: string;
+ imageSrc: string;
+ onUpload(url: string): any;
+ onRemove(): any;
+ rounded?: boolean;
+}
+
+interface ImageUploadFormState {
+ loading: boolean;
+}
+
+export class ImageUploadForm extends Component<
+ ImageUploadFormProps,
+ ImageUploadFormState
+> {
+ private id = `image-upload-form-${randomStr()}`;
+ private emptyState: ImageUploadFormState = {
+ loading: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.state = this.emptyState;
+ }
+
+ render() {
+ return (
+ <form class="d-inline">
+ <label
+ htmlFor={this.id}
+ class="pointer ml-4 text-muted small font-weight-bold"
+ >
+ {!this.props.imageSrc ? (
+ <span class="btn btn-secondary">{this.props.uploadTitle}</span>
+ ) : (
+ <span class="d-inline-block position-relative">
+ <img
+ src={this.props.imageSrc}
+ height={this.props.rounded ? 60 : ''}
+ width={this.props.rounded ? 60 : ''}
+ className={`img-fluid ${
+ this.props.rounded ? 'rounded-circle' : ''
+ }`}
+ />
+ <a onClick={linkEvent(this, this.handleRemoveImage)}>
+ <svg class="icon mini-overlay">
+ <use xlinkHref="#icon-x"></use>
+ </svg>
+ </a>
+ </span>
+ )}
+ </label>
+ <input
+ id={this.id}
+ type="file"
+ accept="image/*,video/*"
+ name={this.id}
+ class="d-none"
+ disabled={!UserService.Instance.user}
+ onChange={linkEvent(this, this.handleImageUpload)}
+ />
+ </form>
+ );
+ }
+
+ handleImageUpload(i: ImageUploadForm, event: any) {
+ event.preventDefault();
+ let file = event.target.files[0];
+ const imageUploadUrl = `/pictrs/image`;
+ const formData = new FormData();
+ formData.append('images[]', file);
+
+ i.state.loading = true;
+ i.setState(i.state);
+
+ fetch(imageUploadUrl, {
+ method: 'POST',
+ body: formData,
+ })
+ .then(res => res.json())
+ .then(res => {
+ console.log('pictrs upload:');
+ console.log(res);
+ if (res.msg == 'ok') {
+ let hash = res.files[0].file;
+ let url = `${window.location.origin}/pictrs/image/${hash}`;
+ i.state.loading = false;
+ i.setState(i.state);
+ i.props.onUpload(url);
+ } else {
+ i.state.loading = false;
+ i.setState(i.state);
+ toast(JSON.stringify(res), 'danger');
+ }
+ })
+ .catch(error => {
+ i.state.loading = false;
+ i.setState(i.state);
+ toast(error, 'danger');
+ });
+ }
+
+ handleRemoveImage(i: ImageUploadForm, event: any) {
+ event.preventDefault();
+ i.state.loading = true;
+ i.setState(i.state);
+ i.props.onRemove();
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ UserOperation,
+ Comment,
+ SortType,
+ GetRepliesForm,
+ GetRepliesResponse,
+ GetUserMentionsForm,
+ GetUserMentionsResponse,
+ UserMentionResponse,
+ CommentResponse,
+ WebSocketJsonResponse,
+ PrivateMessage as PrivateMessageI,
+ GetPrivateMessagesForm,
+ PrivateMessagesResponse,
+ PrivateMessageResponse,
+ GetSiteResponse,
+ Site,
+} from 'lemmy-js-client';
+import { WebSocketService, UserService } from '../services';
+import {
+ wsJsonToRes,
+ fetchLimit,
+ isCommentType,
+ toast,
+ editCommentRes,
+ saveCommentRes,
+ createCommentLikeRes,
+ commentsToFlatNodes,
+ setupTippy,
+} from '../utils';
+import { CommentNodes } from './comment-nodes';
+import { PrivateMessage } from './private-message';
+import { SortSelect } from './sort-select';
+import { i18n } from '../i18next';
+
+enum UnreadOrAll {
+ Unread,
+ All,
+}
+
+enum MessageType {
+ All,
+ Replies,
+ Mentions,
+ Messages,
+}
+
+type ReplyType = Comment | PrivateMessageI;
+
+interface InboxState {
+ unreadOrAll: UnreadOrAll;
+ messageType: MessageType;
+ replies: Comment[];
+ mentions: Comment[];
+ messages: PrivateMessageI[];
+ sort: SortType;
+ page: number;
+ site: Site;
+}
+
+export class Inbox extends Component<any, InboxState> {
+ private subscription: Subscription;
+ private emptyState: InboxState = {
+ unreadOrAll: UnreadOrAll.Unread,
+ messageType: MessageType.All,
+ replies: [],
+ mentions: [],
+ messages: [],
+ sort: SortType.New,
+ page: 1,
+ site: {
+ id: undefined,
+ name: undefined,
+ creator_id: undefined,
+ published: undefined,
+ creator_name: undefined,
+ number_of_users: undefined,
+ number_of_posts: undefined,
+ number_of_comments: undefined,
+ number_of_communities: undefined,
+ enable_downvotes: undefined,
+ open_registration: undefined,
+ enable_nsfw: undefined,
+ },
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handleSortChange = this.handleSortChange.bind(this);
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ this.refetch();
+ WebSocketService.Instance.getSite();
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ get documentTitle(): string {
+ if (this.state.site.name) {
+ return `@${UserService.Instance.user.name} ${i18n.t('inbox')} - ${
+ this.state.site.name
+ }`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle} />
+ <div class="row">
+ <div class="col-12">
+ <h5 class="mb-1">
+ {i18n.t('inbox')}
+ <small>
+ <a
+ href={`/feeds/inbox/${UserService.Instance.auth}.xml`}
+ target="_blank"
+ title="RSS"
+ rel="noopener"
+ >
+ <svg class="icon ml-2 text-muted small">
+ <use xlinkHref="#icon-rss">#</use>
+ </svg>
+ </a>
+ </small>
+ </h5>
+ {this.state.replies.length +
+ this.state.mentions.length +
+ this.state.messages.length >
+ 0 &&
+ this.state.unreadOrAll == UnreadOrAll.Unread && (
+ <ul class="list-inline mb-1 text-muted small font-weight-bold">
+ <li className="list-inline-item">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.markAllAsRead)}
+ >
+ {i18n.t('mark_all_as_read')}
+ </span>
+ </li>
+ </ul>
+ )}
+ {this.selects()}
+ {this.state.messageType == MessageType.All && this.all()}
+ {this.state.messageType == MessageType.Replies && this.replies()}
+ {this.state.messageType == MessageType.Mentions && this.mentions()}
+ {this.state.messageType == MessageType.Messages && this.messages()}
+ {this.paginator()}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ unreadOrAllRadios() {
+ return (
+ <div class="btn-group btn-group-toggle flex-wrap mb-2">
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.unreadOrAll == UnreadOrAll.Unread && 'active'}
+ `}
+ >
+ <input
+ type="radio"
+ value={UnreadOrAll.Unread}
+ checked={this.state.unreadOrAll == UnreadOrAll.Unread}
+ onChange={linkEvent(this, this.handleUnreadOrAllChange)}
+ />
+ {i18n.t('unread')}
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.unreadOrAll == UnreadOrAll.All && 'active'}
+ `}
+ >
+ <input
+ type="radio"
+ value={UnreadOrAll.All}
+ checked={this.state.unreadOrAll == UnreadOrAll.All}
+ onChange={linkEvent(this, this.handleUnreadOrAllChange)}
+ />
+ {i18n.t('all')}
+ </label>
+ </div>
+ );
+ }
+
+ messageTypeRadios() {
+ return (
+ <div class="btn-group btn-group-toggle flex-wrap mb-2">
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.messageType == MessageType.All && 'active'}
+ `}
+ >
+ <input
+ type="radio"
+ value={MessageType.All}
+ checked={this.state.messageType == MessageType.All}
+ onChange={linkEvent(this, this.handleMessageTypeChange)}
+ />
+ {i18n.t('all')}
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.messageType == MessageType.Replies && 'active'}
+ `}
+ >
+ <input
+ type="radio"
+ value={MessageType.Replies}
+ checked={this.state.messageType == MessageType.Replies}
+ onChange={linkEvent(this, this.handleMessageTypeChange)}
+ />
+ {i18n.t('replies')}
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.messageType == MessageType.Mentions && 'active'}
+ `}
+ >
+ <input
+ type="radio"
+ value={MessageType.Mentions}
+ checked={this.state.messageType == MessageType.Mentions}
+ onChange={linkEvent(this, this.handleMessageTypeChange)}
+ />
+ {i18n.t('mentions')}
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.messageType == MessageType.Messages && 'active'}
+ `}
+ >
+ <input
+ type="radio"
+ value={MessageType.Messages}
+ checked={this.state.messageType == MessageType.Messages}
+ onChange={linkEvent(this, this.handleMessageTypeChange)}
+ />
+ {i18n.t('messages')}
+ </label>
+ </div>
+ );
+ }
+
+ selects() {
+ return (
+ <div className="mb-2">
+ <span class="mr-3">{this.unreadOrAllRadios()}</span>
+ <span class="mr-3">{this.messageTypeRadios()}</span>
+ <SortSelect
+ sort={this.state.sort}
+ onChange={this.handleSortChange}
+ hideHot
+ />
+ </div>
+ );
+ }
+
+ combined(): ReplyType[] {
+ return [
+ ...this.state.replies,
+ ...this.state.mentions,
+ ...this.state.messages,
+ ].sort((a, b) => b.published.localeCompare(a.published));
+ }
+
+ all() {
+ return (
+ <div>
+ {this.combined().map(i =>
+ isCommentType(i) ? (
+ <CommentNodes
+ key={i.id}
+ nodes={[{ comment: i }]}
+ noIndent
+ markable
+ showCommunity
+ showContext
+ enableDownvotes={this.state.site.enable_downvotes}
+ />
+ ) : (
+ <PrivateMessage key={i.id} privateMessage={i} />
+ )
+ )}
+ </div>
+ );
+ }
+
+ replies() {
+ return (
+ <div>
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.replies)}
+ noIndent
+ markable
+ showCommunity
+ showContext
+ enableDownvotes={this.state.site.enable_downvotes}
+ />
+ </div>
+ );
+ }
+
+ mentions() {
+ return (
+ <div>
+ {this.state.mentions.map(mention => (
+ <CommentNodes
+ key={mention.id}
+ nodes={[{ comment: mention }]}
+ noIndent
+ markable
+ showCommunity
+ showContext
+ enableDownvotes={this.state.site.enable_downvotes}
+ />
+ ))}
+ </div>
+ );
+ }
+
+ messages() {
+ return (
+ <div>
+ {this.state.messages.map(message => (
+ <PrivateMessage key={message.id} privateMessage={message} />
+ ))}
+ </div>
+ );
+ }
+
+ paginator() {
+ return (
+ <div class="mt-2">
+ {this.state.page > 1 && (
+ <button
+ class="btn btn-secondary mr-1"
+ onClick={linkEvent(this, this.prevPage)}
+ >
+ {i18n.t('prev')}
+ </button>
+ )}
+ {this.unreadCount() > 0 && (
+ <button
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.nextPage)}
+ >
+ {i18n.t('next')}
+ </button>
+ )}
+ </div>
+ );
+ }
+
+ nextPage(i: Inbox) {
+ i.state.page++;
+ i.setState(i.state);
+ i.refetch();
+ }
+
+ prevPage(i: Inbox) {
+ i.state.page--;
+ i.setState(i.state);
+ i.refetch();
+ }
+
+ handleUnreadOrAllChange(i: Inbox, event: any) {
+ i.state.unreadOrAll = Number(event.target.value);
+ i.state.page = 1;
+ i.setState(i.state);
+ i.refetch();
+ }
+
+ handleMessageTypeChange(i: Inbox, event: any) {
+ i.state.messageType = Number(event.target.value);
+ i.state.page = 1;
+ i.setState(i.state);
+ i.refetch();
+ }
+
+ refetch() {
+ let repliesForm: GetRepliesForm = {
+ sort: this.state.sort,
+ unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
+ page: this.state.page,
+ limit: fetchLimit,
+ };
+ WebSocketService.Instance.getReplies(repliesForm);
+
+ let userMentionsForm: GetUserMentionsForm = {
+ sort: this.state.sort,
+ unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
+ page: this.state.page,
+ limit: fetchLimit,
+ };
+ WebSocketService.Instance.getUserMentions(userMentionsForm);
+
+ let privateMessagesForm: GetPrivateMessagesForm = {
+ unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
+ page: this.state.page,
+ limit: fetchLimit,
+ };
+ WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
+ }
+
+ handleSortChange(val: SortType) {
+ this.state.sort = val;
+ this.state.page = 1;
+ this.setState(this.state);
+ this.refetch();
+ }
+
+ markAllAsRead(i: Inbox) {
+ WebSocketService.Instance.markAllAsRead();
+ i.state.replies = [];
+ i.state.mentions = [];
+ i.state.messages = [];
+ i.sendUnreadCount();
+ window.scrollTo(0, 0);
+ i.setState(i.state);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ return;
+ } else if (msg.reconnect) {
+ this.refetch();
+ } else if (res.op == UserOperation.GetReplies) {
+ let data = res.data as GetRepliesResponse;
+ this.state.replies = data.replies;
+ this.sendUnreadCount();
+ window.scrollTo(0, 0);
+ this.setState(this.state);
+ setupTippy();
+ } else if (res.op == UserOperation.GetUserMentions) {
+ let data = res.data as GetUserMentionsResponse;
+ this.state.mentions = data.mentions;
+ this.sendUnreadCount();
+ window.scrollTo(0, 0);
+ this.setState(this.state);
+ setupTippy();
+ } else if (res.op == UserOperation.GetPrivateMessages) {
+ let data = res.data as PrivateMessagesResponse;
+ this.state.messages = data.messages;
+ this.sendUnreadCount();
+ window.scrollTo(0, 0);
+ this.setState(this.state);
+ setupTippy();
+ } else if (res.op == UserOperation.EditPrivateMessage) {
+ let data = res.data as PrivateMessageResponse;
+ let found: PrivateMessageI = this.state.messages.find(
+ m => m.id === data.message.id
+ );
+ if (found) {
+ found.content = data.message.content;
+ found.updated = data.message.updated;
+ }
+ this.setState(this.state);
+ } else if (res.op == UserOperation.DeletePrivateMessage) {
+ let data = res.data as PrivateMessageResponse;
+ let found: PrivateMessageI = this.state.messages.find(
+ m => m.id === data.message.id
+ );
+ if (found) {
+ found.deleted = data.message.deleted;
+ found.updated = data.message.updated;
+ }
+ this.setState(this.state);
+ } else if (res.op == UserOperation.MarkPrivateMessageAsRead) {
+ let data = res.data as PrivateMessageResponse;
+ let found: PrivateMessageI = this.state.messages.find(
+ m => m.id === data.message.id
+ );
+
+ if (found) {
+ found.updated = data.message.updated;
+
+ // If youre in the unread view, just remove it from the list
+ if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
+ this.state.messages = this.state.messages.filter(
+ r => r.id !== data.message.id
+ );
+ } else {
+ let found = this.state.messages.find(c => c.id == data.message.id);
+ found.read = data.message.read;
+ }
+ }
+ this.sendUnreadCount();
+ this.setState(this.state);
+ } else if (res.op == UserOperation.MarkAllAsRead) {
+ // Moved to be instant
+ } else if (
+ res.op == UserOperation.EditComment ||
+ res.op == UserOperation.DeleteComment ||
+ res.op == UserOperation.RemoveComment
+ ) {
+ let data = res.data as CommentResponse;
+ editCommentRes(data, this.state.replies);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.MarkCommentAsRead) {
+ let data = res.data as CommentResponse;
+
+ // If youre in the unread view, just remove it from the list
+ if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
+ this.state.replies = this.state.replies.filter(
+ r => r.id !== data.comment.id
+ );
+ } else {
+ let found = this.state.replies.find(c => c.id == data.comment.id);
+ found.read = data.comment.read;
+ }
+ this.sendUnreadCount();
+ this.setState(this.state);
+ setupTippy();
+ } else if (res.op == UserOperation.MarkUserMentionAsRead) {
+ let data = res.data as UserMentionResponse;
+
+ let found = this.state.mentions.find(c => c.id == data.mention.id);
+ found.content = data.mention.content;
+ found.updated = data.mention.updated;
+ found.removed = data.mention.removed;
+ found.deleted = data.mention.deleted;
+ found.upvotes = data.mention.upvotes;
+ found.downvotes = data.mention.downvotes;
+ found.score = data.mention.score;
+
+ // If youre in the unread view, just remove it from the list
+ if (this.state.unreadOrAll == UnreadOrAll.Unread && data.mention.read) {
+ this.state.mentions = this.state.mentions.filter(
+ r => r.id !== data.mention.id
+ );
+ } else {
+ let found = this.state.mentions.find(c => c.id == data.mention.id);
+ found.read = data.mention.read;
+ }
+ this.sendUnreadCount();
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreateComment) {
+ let data = res.data as CommentResponse;
+
+ if (data.recipient_ids.includes(UserService.Instance.user.id)) {
+ this.state.replies.unshift(data.comment);
+ this.setState(this.state);
+ } else if (data.comment.creator_id == UserService.Instance.user.id) {
+ toast(i18n.t('reply_sent'));
+ }
+ } else if (res.op == UserOperation.CreatePrivateMessage) {
+ let data = res.data as PrivateMessageResponse;
+ if (data.message.recipient_id == UserService.Instance.user.id) {
+ this.state.messages.unshift(data.message);
+ this.setState(this.state);
+ }
+ } else if (res.op == UserOperation.SaveComment) {
+ let data = res.data as CommentResponse;
+ saveCommentRes(data, this.state.replies);
+ this.setState(this.state);
+ setupTippy();
+ } else if (res.op == UserOperation.CreateCommentLike) {
+ let data = res.data as CommentResponse;
+ createCommentLikeRes(data, this.state.replies);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.site = data.site;
+ this.setState(this.state);
+ }
+ }
+
+ sendUnreadCount() {
+ UserService.Instance.unreadCountSub.next(this.unreadCount());
+ }
+
+ unreadCount(): number {
+ return (
+ this.state.replies.filter(r => !r.read).length +
+ this.state.mentions.filter(r => !r.read).length +
+ this.state.messages.filter(
+ r =>
+ UserService.Instance.user &&
+ !r.read &&
+ r.creator_id !== UserService.Instance.user.id
+ ).length
+ );
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ UserOperation,
+ WebSocketJsonResponse,
+ GetSiteResponse,
+} from 'lemmy-js-client';
+import { WebSocketService } from '../services';
+import { wsJsonToRes, toast, isBrowser } from '../utils';
+import { i18n } from '../i18next';
+
+interface InstancesState {
+ loading: boolean;
+ siteRes: GetSiteResponse;
+}
+
+export class Instances extends Component<any, InstancesState> {
+ private subscription: Subscription;
+ private emptyState: InstancesState = {
+ loading: true,
+ siteRes: undefined,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.state = this.emptyState;
+
+ if (isBrowser()) {
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ WebSocketService.Instance.getSite();
+ }
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ get documentTitle(): string {
+ if (this.state.siteRes) {
+ return `${i18n.t('instances')} - ${this.state.siteRes.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle} />
+ {this.state.loading ? (
+ <h5 class="">
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ </h5>
+ ) : (
+ <div>
+ <h5>{i18n.t('linked_instances')}</h5>
+ {this.state.siteRes &&
+ this.state.siteRes.federated_instances.length ? (
+ <ul>
+ {this.state.siteRes.federated_instances.map(i => (
+ <li>
+ <a href={`https://${i}`} target="_blank" rel="noopener">
+ {i}
+ </a>
+ </li>
+ ))}
+ </ul>
+ ) : (
+ <div>{i18n.t('none_found')}</div>
+ )}
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ return;
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.siteRes = data;
+ this.state.loading = false;
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { ListingType } from 'lemmy-js-client';
+import { UserService } from '../services';
+import { randomStr } from '../utils';
+import { i18n } from '../i18next';
+
+interface ListingTypeSelectProps {
+ type_: ListingType;
+ onChange?(val: ListingType): any;
+}
+
+interface ListingTypeSelectState {
+ type_: ListingType;
+}
+
+export class ListingTypeSelect extends Component<
+ ListingTypeSelectProps,
+ ListingTypeSelectState
+> {
+ private id = `listing-type-input-${randomStr()}`;
+
+ private emptyState: ListingTypeSelectState = {
+ type_: this.props.type_,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.state = this.emptyState;
+ }
+
+ static getDerivedStateFromProps(props: any): ListingTypeSelectProps {
+ return {
+ type_: props.type_,
+ };
+ }
+
+ render() {
+ return (
+ <div class="btn-group btn-group-toggle flex-wrap mb-2">
+ <label
+ className={`btn btn-outline-secondary
+ ${this.state.type_ == ListingType.Subscribed && 'active'}
+ ${UserService.Instance.user == undefined ? 'disabled' : 'pointer'}
+ `}
+ >
+ <input
+ id={`${this.id}-subscribed`}
+ type="radio"
+ value={ListingType.Subscribed}
+ checked={this.state.type_ == ListingType.Subscribed}
+ onChange={linkEvent(this, this.handleTypeChange)}
+ disabled={UserService.Instance.user == undefined}
+ />
+ {i18n.t('subscribed')}
+ </label>
+ <label
+ className={`pointer btn btn-outline-secondary ${
+ this.state.type_ == ListingType.All && 'active'
+ }`}
+ >
+ <input
+ id={`${this.id}-all`}
+ type="radio"
+ value={ListingType.All}
+ checked={this.state.type_ == ListingType.All}
+ onChange={linkEvent(this, this.handleTypeChange)}
+ />
+ {i18n.t('all')}
+ </label>
+ </div>
+ );
+ }
+
+ handleTypeChange(i: ListingTypeSelect, event: any) {
+ i.props.onChange(event.target.value);
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ LoginForm,
+ RegisterForm,
+ LoginResponse,
+ UserOperation,
+ PasswordResetForm,
+ GetSiteResponse,
+ GetCaptchaResponse,
+ WebSocketJsonResponse,
+ Site,
+} from 'lemmy-js-client';
+import { WebSocketService, UserService } from '../services';
+import { wsJsonToRes, validEmail, toast } from '../utils';
+import { i18n } from '../i18next';
+
+interface State {
+ loginForm: LoginForm;
+ registerForm: RegisterForm;
+ loginLoading: boolean;
+ registerLoading: boolean;
+ captcha: GetCaptchaResponse;
+ captchaPlaying: boolean;
+ site: Site;
+}
+
+export class Login extends Component<any, State> {
+ private subscription: Subscription;
+
+ emptyState: State = {
+ loginForm: {
+ username_or_email: undefined,
+ password: undefined,
+ },
+ registerForm: {
+ username: undefined,
+ password: undefined,
+ password_verify: undefined,
+ admin: false,
+ show_nsfw: false,
+ captcha_uuid: undefined,
+ captcha_answer: undefined,
+ },
+ loginLoading: false,
+ registerLoading: false,
+ captcha: undefined,
+ captchaPlaying: false,
+ site: {
+ id: undefined,
+ name: undefined,
+ creator_id: undefined,
+ published: undefined,
+ creator_name: undefined,
+ number_of_users: undefined,
+ number_of_posts: undefined,
+ number_of_comments: undefined,
+ number_of_communities: undefined,
+ enable_downvotes: undefined,
+ open_registration: undefined,
+ enable_nsfw: undefined,
+ },
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ WebSocketService.Instance.getSite();
+ WebSocketService.Instance.getCaptcha();
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ get documentTitle(): string {
+ if (this.state.site.name) {
+ return `${i18n.t('login')} - ${this.state.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle} />
+ <div class="row">
+ <div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
+ <div class="col-12 col-lg-6">{this.registerForm()}</div>
+ </div>
+ </div>
+ );
+ }
+
+ loginForm() {
+ return (
+ <div>
+ <form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
+ <h5>{i18n.t('login')}</h5>
+ <div class="form-group row">
+ <label
+ class="col-sm-2 col-form-label"
+ htmlFor="login-email-or-username"
+ >
+ {i18n.t('email_or_username')}
+ </label>
+ <div class="col-sm-10">
+ <input
+ type="text"
+ class="form-control"
+ id="login-email-or-username"
+ value={this.state.loginForm.username_or_email}
+ onInput={linkEvent(this, this.handleLoginUsernameChange)}
+ required
+ minLength={3}
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label" htmlFor="login-password">
+ {i18n.t('password')}
+ </label>
+ <div class="col-sm-10">
+ <input
+ type="password"
+ id="login-password"
+ value={this.state.loginForm.password}
+ onInput={linkEvent(this, this.handleLoginPasswordChange)}
+ class="form-control"
+ required
+ />
+ {validEmail(this.state.loginForm.username_or_email) && (
+ <button
+ type="button"
+ onClick={linkEvent(this, this.handlePasswordReset)}
+ className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
+ >
+ {i18n.t('forgot_password')}
+ </button>
+ )}
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <button type="submit" class="btn btn-secondary">
+ {this.state.loginLoading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ i18n.t('login')
+ )}
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+ }
+
+ registerForm() {
+ return (
+ <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
+ <h5>{i18n.t('sign_up')}</h5>
+
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label" htmlFor="register-username">
+ {i18n.t('username')}
+ </label>
+
+ <div class="col-sm-10">
+ <input
+ type="text"
+ id="register-username"
+ class="form-control"
+ value={this.state.registerForm.username}
+ onInput={linkEvent(this, this.handleRegisterUsernameChange)}
+ required
+ minLength={3}
+ maxLength={20}
+ pattern="[a-zA-Z0-9_]+"
+ />
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label" htmlFor="register-email">
+ {i18n.t('email')}
+ </label>
+ <div class="col-sm-10">
+ <input
+ type="email"
+ id="register-email"
+ class="form-control"
+ placeholder={i18n.t('optional')}
+ value={this.state.registerForm.email}
+ onInput={linkEvent(this, this.handleRegisterEmailChange)}
+ minLength={3}
+ />
+ {!validEmail(this.state.registerForm.email) && (
+ <div class="mt-2 mb-0 alert alert-light" role="alert">
+ <svg class="icon icon-inline mr-2">
+ <use xlinkHref="#icon-alert-triangle"></use>
+ </svg>
+ {i18n.t('no_password_reset')}
+ </div>
+ )}
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label" htmlFor="register-password">
+ {i18n.t('password')}
+ </label>
+ <div class="col-sm-10">
+ <input
+ type="password"
+ id="register-password"
+ value={this.state.registerForm.password}
+ autoComplete="new-password"
+ onInput={linkEvent(this, this.handleRegisterPasswordChange)}
+ class="form-control"
+ required
+ />
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label
+ class="col-sm-2 col-form-label"
+ htmlFor="register-verify-password"
+ >
+ {i18n.t('verify_password')}
+ </label>
+ <div class="col-sm-10">
+ <input
+ type="password"
+ id="register-verify-password"
+ value={this.state.registerForm.password_verify}
+ autoComplete="new-password"
+ onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
+ class="form-control"
+ required
+ />
+ </div>
+ </div>
+
+ {this.state.captcha && (
+ <div class="form-group row">
+ <label class="col-sm-2" htmlFor="register-captcha">
+ <span class="mr-2">{i18n.t('enter_code')}</span>
+ <button
+ type="button"
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.handleRegenCaptcha)}
+ >
+ <svg class="icon icon-refresh-cw">
+ <use xlinkHref="#icon-refresh-cw"></use>
+ </svg>
+ </button>
+ </label>
+ {this.showCaptcha()}
+ <div class="col-sm-6">
+ <input
+ type="text"
+ class="form-control"
+ id="register-captcha"
+ value={this.state.registerForm.captcha_answer}
+ onInput={linkEvent(
+ this,
+ this.handleRegisterCaptchaAnswerChange
+ )}
+ required
+ />
+ </div>
+ </div>
+ )}
+ {this.state.site.enable_nsfw && (
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="register-show-nsfw"
+ type="checkbox"
+ checked={this.state.registerForm.show_nsfw}
+ onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
+ />
+ <label class="form-check-label" htmlFor="register-show-nsfw">
+ {i18n.t('show_nsfw')}
+ </label>
+ </div>
+ </div>
+ </div>
+ )}
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <button type="submit" class="btn btn-secondary">
+ {this.state.registerLoading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ i18n.t('sign_up')
+ )}
+ </button>
+ </div>
+ </div>
+ </form>
+ );
+ }
+
+ showCaptcha() {
+ return (
+ <div class="col-sm-4">
+ {this.state.captcha.ok && (
+ <>
+ <img
+ class="rounded-top img-fluid"
+ src={this.captchaPngSrc()}
+ style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
+ />
+ {this.state.captcha.ok.wav && (
+ <button
+ class="rounded-bottom btn btn-sm btn-secondary btn-block"
+ style="border-top-right-radius: 0; border-top-left-radius: 0;"
+ title={i18n.t('play_captcha_audio')}
+ onClick={linkEvent(this, this.handleCaptchaPlay)}
+ type="button"
+ disabled={this.state.captchaPlaying}
+ >
+ <svg class="icon icon-play">
+ <use xlinkHref="#icon-play"></use>
+ </svg>
+ </button>
+ )}
+ </>
+ )}
+ </div>
+ );
+ }
+
+ handleLoginSubmit(i: Login, event: any) {
+ event.preventDefault();
+ i.state.loginLoading = true;
+ i.setState(i.state);
+ WebSocketService.Instance.login(i.state.loginForm);
+ }
+
+ handleLoginUsernameChange(i: Login, event: any) {
+ i.state.loginForm.username_or_email = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleLoginPasswordChange(i: Login, event: any) {
+ i.state.loginForm.password = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleRegisterSubmit(i: Login, event: any) {
+ event.preventDefault();
+ i.state.registerLoading = true;
+ i.setState(i.state);
+ WebSocketService.Instance.register(i.state.registerForm);
+ }
+
+ handleRegisterUsernameChange(i: Login, event: any) {
+ i.state.registerForm.username = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleRegisterEmailChange(i: Login, event: any) {
+ i.state.registerForm.email = event.target.value;
+ if (i.state.registerForm.email == '') {
+ i.state.registerForm.email = undefined;
+ }
+ i.setState(i.state);
+ }
+
+ handleRegisterPasswordChange(i: Login, event: any) {
+ i.state.registerForm.password = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleRegisterPasswordVerifyChange(i: Login, event: any) {
+ i.state.registerForm.password_verify = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleRegisterShowNsfwChange(i: Login, event: any) {
+ i.state.registerForm.show_nsfw = event.target.checked;
+ i.setState(i.state);
+ }
+
+ handleRegisterCaptchaAnswerChange(i: Login, event: any) {
+ i.state.registerForm.captcha_answer = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleRegenCaptcha(_i: Login, _event: any) {
+ event.preventDefault();
+ WebSocketService.Instance.getCaptcha();
+ }
+
+ handlePasswordReset(i: Login) {
+ event.preventDefault();
+ let resetForm: PasswordResetForm = {
+ email: i.state.loginForm.username_or_email,
+ };
+ WebSocketService.Instance.passwordReset(resetForm);
+ }
+
+ handleCaptchaPlay(i: Login) {
+ event.preventDefault();
+ let snd = new Audio('data:audio/wav;base64,' + i.state.captcha.ok.wav);
+ snd.play();
+ i.state.captchaPlaying = true;
+ i.setState(i.state);
+ snd.addEventListener('ended', () => {
+ snd.currentTime = 0;
+ i.state.captchaPlaying = false;
+ i.setState(this.state);
+ });
+ }
+
+ captchaPngSrc() {
+ return `data:image/png;base64,${this.state.captcha.ok.png}`;
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ this.state = this.emptyState;
+ this.state.registerForm.captcha_answer = undefined;
+ // Refetch another captcha
+ WebSocketService.Instance.getCaptcha();
+ this.setState(this.state);
+ return;
+ } else {
+ if (res.op == UserOperation.Login) {
+ let data = res.data as LoginResponse;
+ this.state = this.emptyState;
+ this.setState(this.state);
+ UserService.Instance.login(data);
+ WebSocketService.Instance.userJoin();
+ toast(i18n.t('logged_in'));
+ this.props.history.push('/');
+ } else if (res.op == UserOperation.Register) {
+ let data = res.data as LoginResponse;
+ this.state = this.emptyState;
+ this.setState(this.state);
+ UserService.Instance.login(data);
+ WebSocketService.Instance.userJoin();
+ this.props.history.push('/communities');
+ } else if (res.op == UserOperation.GetCaptcha) {
+ let data = res.data as GetCaptchaResponse;
+ if (data.ok) {
+ this.state.captcha = data;
+ this.state.registerForm.captcha_uuid = data.ok.uuid;
+ this.setState(this.state);
+ }
+ } else if (res.op == UserOperation.PasswordReset) {
+ toast(i18n.t('reset_password_mail_sent'));
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.site = data.site;
+ this.setState(this.state);
+ }
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Link } from 'inferno-router';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ UserOperation,
+ CommunityUser,
+ GetFollowedCommunitiesResponse,
+ ListCommunitiesForm,
+ ListCommunitiesResponse,
+ Community,
+ SortType,
+ GetSiteResponse,
+ ListingType,
+ SiteResponse,
+ GetPostsResponse,
+ PostResponse,
+ Post,
+ GetPostsForm,
+ Comment,
+ GetCommentsForm,
+ GetCommentsResponse,
+ CommentResponse,
+ AddAdminResponse,
+ BanUserResponse,
+ WebSocketJsonResponse,
+} from 'lemmy-js-client';
+import { DataType } from '../interfaces';
+import { WebSocketService, UserService } from '../services';
+import { PostListings } from './post-listings';
+import { CommentNodes } from './comment-nodes';
+import { SortSelect } from './sort-select';
+import { ListingTypeSelect } from './listing-type-select';
+import { DataTypeSelect } from './data-type-select';
+import { SiteForm } from './site-form';
+import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
+import { BannerIconHeader } from './banner-icon-header';
+import {
+ wsJsonToRes,
+ repoUrl,
+ mdToHtml,
+ fetchLimit,
+ toast,
+ getListingTypeFromProps,
+ getPageFromProps,
+ getSortTypeFromProps,
+ getDataTypeFromProps,
+ editCommentRes,
+ saveCommentRes,
+ createCommentLikeRes,
+ createPostLikeFindRes,
+ editPostFindRes,
+ commentsToFlatNodes,
+ setupTippy,
+ favIconUrl,
+ notifyPost,
+ isBrowser,
+} from '../utils';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
+
+interface MainState {
+ subscribedCommunities: CommunityUser[];
+ trendingCommunities: Community[];
+ siteRes: GetSiteResponse;
+ showEditSite: boolean;
+ loading: boolean;
+ posts: Post[];
+ comments: Comment[];
+ listingType: ListingType;
+ dataType: DataType;
+ sort: SortType;
+ page: number;
+}
+
+interface MainProps {
+ listingType: ListingType;
+ dataType: DataType;
+ sort: SortType;
+ page: number;
+}
+
+interface UrlParams {
+ listingType?: ListingType;
+ dataType?: string;
+ sort?: SortType;
+ page?: number;
+}
+
+export class Main extends Component<any, MainState> {
+ private subscription: Subscription;
+ private emptyState: MainState = {
+ subscribedCommunities: [],
+ trendingCommunities: [],
+ siteRes: {
+ site: {
+ id: null,
+ name: null,
+ creator_id: null,
+ creator_name: null,
+ published: null,
+ number_of_users: null,
+ number_of_posts: null,
+ number_of_comments: null,
+ number_of_communities: null,
+ enable_downvotes: null,
+ open_registration: null,
+ enable_nsfw: null,
+ icon: null,
+ banner: null,
+ creator_preferred_username: null,
+ },
+ admins: [],
+ banned: [],
+ online: null,
+ version: null,
+ federated_instances: null,
+ },
+ showEditSite: false,
+ loading: true,
+ posts: [],
+ comments: [],
+ listingType: getListingTypeFromProps(this.props),
+ dataType: getDataTypeFromProps(this.props),
+ sort: getSortTypeFromProps(this.props),
+ page: getPageFromProps(this.props),
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handleEditCancel = this.handleEditCancel.bind(this);
+ this.handleSortChange = this.handleSortChange.bind(this);
+ this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
+ this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
+
+ if (isBrowser()) {
+ // TODO
+ /* this.subscription = WebSocketService.Instance.subject */
+ /* .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) */
+ /* .subscribe( */
+ /* msg => this.parseMessage(msg), */
+ /* err => console.error(err), */
+ /* () => console.log('complete') */
+ /* ); */
+ /* WebSocketService.Instance.getSite(); */
+ /* if (UserService.Instance.user) { */
+ /* WebSocketService.Instance.getFollowedCommunities(); */
+ /* } */
+ /* let listCommunitiesForm: ListCommunitiesForm = { */
+ /* sort: SortType.Hot, */
+ /* limit: 6, */
+ /* }; */
+ /* WebSocketService.Instance.listCommunities(listCommunitiesForm); */
+ /* this.fetchData(); */
+ }
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ /* static getDerivedStateFromProps(props: any): MainProps { */
+ /* return { */
+ /* listingType: getListingTypeFromProps(props), */
+ /* dataType: getDataTypeFromProps(props), */
+ /* sort: getSortTypeFromProps(props), */
+ /* page: getPageFromProps(props), */
+ /* }; */
+ /* } */
+
+ /* componentDidUpdate(_: any, lastState: MainState) { */
+ /* if ( */
+ /* lastState.listingType !== this.state.listingType || */
+ /* lastState.dataType !== this.state.dataType || */
+ /* lastState.sort !== this.state.sort || */
+ /* lastState.page !== this.state.page */
+ /* ) { */
+ /* this.setState({ loading: true }); */
+ /* this.fetchData(); */
+ /* } */
+ /* } */
+
+ get documentTitle(): string {
+ if (this.state.siteRes.site.name) {
+ return `${this.state.siteRes.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ get favIcon(): string {
+ return this.state.siteRes.site.icon
+ ? this.state.siteRes.site.icon
+ : favIconUrl;
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <h1 className={`text-warning`}>u stink main</h1>
+ <Helmet title={this.documentTitle}>
+ <link
+ id="favicon"
+ rel="icon"
+ type="image/x-icon"
+ href={this.favIcon}
+ />
+ </Helmet>
+ <div class="row">
+ <main role="main" class="col-12 col-md-8">
+ {this.posts()}
+ </main>
+ <aside class="col-12 col-md-4">{this.mySidebar()}</aside>
+ </div>
+ </div>
+ );
+ }
+
+ mySidebar() {
+ return (
+ <div>
+ {!this.state.loading && (
+ <div>
+ <div class="card bg-transparent border-secondary mb-3">
+ <div class="card-header bg-transparent border-secondary">
+ <div class="mb-2">
+ {this.siteName()}
+ {this.adminButtons()}
+ </div>
+ <BannerIconHeader banner={this.state.siteRes.site.banner} />
+ </div>
+ <div class="card-body">
+ {this.trendingCommunities()}
+ {this.createCommunityButton()}
+ {/*
+ {this.subscribedCommunities()}
+ */}
+ </div>
+ </div>
+
+ <div class="card bg-transparent border-secondary mb-3">
+ <div class="card-body">{this.sidebar()}</div>
+ </div>
+
+ <div class="card bg-transparent border-secondary">
+ <div class="card-body">{this.landing()}</div>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ createCommunityButton() {
+ return (
+ <Link class="btn btn-secondary btn-block" to="/create_community">
+ {i18n.t('create_a_community')}
+ </Link>
+ );
+ }
+
+ trendingCommunities() {
+ return (
+ <div>
+ <h5>
+ <T i18nKey="trending_communities">
+ #
+ <Link class="text-body" to="/communities">
+ #
+ </Link>
+ </T>
+ </h5>
+ <ul class="list-inline">
+ {this.state.trendingCommunities.map(community => (
+ <li class="list-inline-item">
+ <CommunityLink community={community} />
+ </li>
+ ))}
+ </ul>
+ </div>
+ );
+ }
+
+ subscribedCommunities() {
+ return (
+ UserService.Instance.user &&
+ this.state.subscribedCommunities.length > 0 && (
+ <div>
+ <h5>
+ <T i18nKey="subscribed_to_communities">
+ #
+ <Link class="text-body" to="/communities">
+ #
+ </Link>
+ </T>
+ </h5>
+ <ul class="list-inline">
+ {this.state.subscribedCommunities.map(community => (
+ <li class="list-inline-item">
+ <CommunityLink
+ community={{
+ name: community.community_name,
+ id: community.community_id,
+ local: community.community_local,
+ actor_id: community.community_actor_id,
+ icon: community.community_icon,
+ }}
+ />
+ </li>
+ ))}
+ </ul>
+ </div>
+ )
+ );
+ }
+
+ sidebar() {
+ return (
+ <div>
+ {!this.state.showEditSite ? (
+ this.siteInfo()
+ ) : (
+ <SiteForm
+ site={this.state.siteRes.site}
+ onCancel={this.handleEditCancel}
+ />
+ )}
+ </div>
+ );
+ }
+
+ updateUrl(paramUpdates: UrlParams) {
+ const listingTypeStr = paramUpdates.listingType || this.state.listingType;
+ const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
+ const sortStr = paramUpdates.sort || this.state.sort;
+ const page = paramUpdates.page || this.state.page;
+ this.props.history.push(
+ `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
+ );
+ }
+
+ siteInfo() {
+ return (
+ <div>
+ {this.state.siteRes.site.description && this.siteDescription()}
+ {this.badges()}
+ {this.admins()}
+ </div>
+ );
+ }
+
+ siteName() {
+ return <h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>;
+ }
+
+ admins() {
+ return (
+ <ul class="mt-1 list-inline small mb-0">
+ <li class="list-inline-item">{i18n.t('admins')}:</li>
+ {this.state.siteRes.admins.map(admin => (
+ <li class="list-inline-item">
+ <UserListing
+ user={{
+ name: admin.name,
+ preferred_username: admin.preferred_username,
+ avatar: admin.avatar,
+ local: admin.local,
+ actor_id: admin.actor_id,
+ id: admin.id,
+ }}
+ />
+ </li>
+ ))}
+ </ul>
+ );
+ }
+
+ badges() {
+ return (
+ <ul class="my-2 list-inline">
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_online', { count: this.state.siteRes.online })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_users', {
+ count: this.state.siteRes.site.number_of_users,
+ })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_communities', {
+ count: this.state.siteRes.site.number_of_communities,
+ })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_posts', {
+ count: this.state.siteRes.site.number_of_posts,
+ })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_comments', {
+ count: this.state.siteRes.site.number_of_comments,
+ })}
+ </li>
+ <li className="list-inline-item">
+ <Link className="badge badge-light" to="/modlog">
+ {i18n.t('modlog')}
+ </Link>
+ </li>
+ </ul>
+ );
+ }
+
+ adminButtons() {
+ return (
+ this.canAdmin && (
+ <ul class="list-inline mb-1 text-muted font-weight-bold">
+ <li className="list-inline-item-action">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleEditClick)}
+ data-tippy-content={i18n.t('edit')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-edit"></use>
+ </svg>
+ </span>
+ </li>
+ </ul>
+ )
+ );
+ }
+
+ siteDescription() {
+ return (
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(this.state.siteRes.site.description)}
+ />
+ );
+ }
+
+ landing() {
+ return (
+ <>
+ <h5>
+ {i18n.t('powered_by')}
+ <svg class="icon mx-2">
+ <use xlinkHref="#icon-mouse">#</use>
+ </svg>
+ <a href={repoUrl}>
+ Lemmy<sup>beta</sup>
+ </a>
+ </h5>
+ <p class="mb-0">
+ <T i18nKey="landing_0">
+ #
+ <a href="https://en.wikipedia.org/wiki/Social_network_aggregation">
+ #
+ </a>
+ <a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
+ <br class="big"></br>
+ <code>#</code>
+ <br></br>
+ <b>#</b>
+ <br class="big"></br>
+ <a href={repoUrl}>#</a>
+ <br class="big"></br>
+ <a href="https://www.rust-lang.org">#</a>
+ <a href="https://actix.rs/">#</a>
+ <a href="https://infernojs.org">#</a>
+ <a href="https://www.typescriptlang.org/">#</a>
+ <br class="big"></br>
+ <a href="https://github.com/LemmyNet/lemmy/graphs/contributors?type=a">
+ #
+ </a>
+ </T>
+ </p>
+ </>
+ );
+ }
+
+ posts() {
+ return (
+ <div class="main-content-wrapper">
+ {this.state.loading ? (
+ <h5>
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ </h5>
+ ) : (
+ <div>
+ {this.selects()}
+ {this.listings()}
+ {this.paginator()}
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ listings() {
+ return this.state.dataType == DataType.Post ? (
+ <PostListings
+ posts={this.state.posts}
+ showCommunity
+ removeDuplicates
+ sort={this.state.sort}
+ enableDownvotes={this.state.siteRes.site.enable_downvotes}
+ enableNsfw={this.state.siteRes.site.enable_nsfw}
+ />
+ ) : (
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.comments)}
+ noIndent
+ showCommunity
+ sortType={this.state.sort}
+ showContext
+ enableDownvotes={this.state.siteRes.site.enable_downvotes}
+ />
+ );
+ }
+
+ selects() {
+ return (
+ <div className="mb-3">
+ <span class="mr-3">
+ <DataTypeSelect
+ type_={this.state.dataType}
+ onChange={this.handleDataTypeChange}
+ />
+ </span>
+ <span class="mr-3">
+ <ListingTypeSelect
+ type_={this.state.listingType}
+ onChange={this.handleListingTypeChange}
+ />
+ </span>
+ <span class="mr-2">
+ <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
+ </span>
+ {this.state.listingType == ListingType.All && (
+ <a
+ href={`/feeds/all.xml?sort=${this.state.sort}`}
+ target="_blank"
+ rel="noopener"
+ title="RSS"
+ >
+ <svg class="icon text-muted small">
+ <use xlinkHref="#icon-rss">#</use>
+ </svg>
+ </a>
+ )}
+ {UserService.Instance.user &&
+ this.state.listingType == ListingType.Subscribed && (
+ <a
+ href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${this.state.sort}`}
+ target="_blank"
+ title="RSS"
+ rel="noopener"
+ >
+ <svg class="icon text-muted small">
+ <use xlinkHref="#icon-rss">#</use>
+ </svg>
+ </a>
+ )}
+ </div>
+ );
+ }
+
+ paginator() {
+ return (
+ <div class="my-2">
+ {this.state.page > 1 && (
+ <button
+ class="btn btn-secondary mr-1"
+ onClick={linkEvent(this, this.prevPage)}
+ >
+ {i18n.t('prev')}
+ </button>
+ )}
+ {this.state.posts.length > 0 && (
+ <button
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.nextPage)}
+ >
+ {i18n.t('next')}
+ </button>
+ )}
+ </div>
+ );
+ }
+
+ get canAdmin(): boolean {
+ return (
+ UserService.Instance.user &&
+ this.state.siteRes.admins
+ .map(a => a.id)
+ .includes(UserService.Instance.user.id)
+ );
+ }
+
+ handleEditClick(i: Main) {
+ i.state.showEditSite = true;
+ i.setState(i.state);
+ }
+
+ handleEditCancel() {
+ this.state.showEditSite = false;
+ this.setState(this.state);
+ }
+
+ nextPage(i: Main) {
+ i.updateUrl({ page: i.state.page + 1 });
+ window.scrollTo(0, 0);
+ }
+
+ prevPage(i: Main) {
+ i.updateUrl({ page: i.state.page - 1 });
+ window.scrollTo(0, 0);
+ }
+
+ handleSortChange(val: SortType) {
+ this.updateUrl({ sort: val, page: 1 });
+ window.scrollTo(0, 0);
+ }
+
+ handleListingTypeChange(val: ListingType) {
+ this.updateUrl({ listingType: val, page: 1 });
+ window.scrollTo(0, 0);
+ }
+
+ handleDataTypeChange(val: DataType) {
+ this.updateUrl({ dataType: DataType[val], page: 1 });
+ window.scrollTo(0, 0);
+ }
+
+ fetchData() {
+ if (this.state.dataType == DataType.Post) {
+ let getPostsForm: GetPostsForm = {
+ page: this.state.page,
+ limit: fetchLimit,
+ sort: this.state.sort,
+ type_: this.state.listingType,
+ };
+ WebSocketService.Instance.getPosts(getPostsForm);
+ } else {
+ let getCommentsForm: GetCommentsForm = {
+ page: this.state.page,
+ limit: fetchLimit,
+ sort: this.state.sort,
+ type_: this.state.listingType,
+ };
+ WebSocketService.Instance.getComments(getCommentsForm);
+ }
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ return;
+ } else if (msg.reconnect) {
+ this.fetchData();
+ } else if (res.op == UserOperation.GetFollowedCommunities) {
+ let data = res.data as GetFollowedCommunitiesResponse;
+ this.state.subscribedCommunities = data.communities;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.ListCommunities) {
+ let data = res.data as ListCommunitiesResponse;
+ this.state.trendingCommunities = data.communities;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+
+ // This means it hasn't been set up yet
+ if (!data.site) {
+ this.context.router.history.push('/setup');
+ }
+ this.state.siteRes.admins = data.admins;
+ this.state.siteRes.site = data.site;
+ this.state.siteRes.banned = data.banned;
+ this.state.siteRes.online = data.online;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.EditSite) {
+ let data = res.data as SiteResponse;
+ this.state.siteRes.site = data.site;
+ this.state.showEditSite = false;
+ this.setState(this.state);
+ toast(i18n.t('site_saved'));
+ } else if (res.op == UserOperation.GetPosts) {
+ let data = res.data as GetPostsResponse;
+ this.state.posts = data.posts;
+ this.state.loading = false;
+ this.setState(this.state);
+ setupTippy();
+ } else if (res.op == UserOperation.CreatePost) {
+ let data = res.data as PostResponse;
+
+ // If you're on subscribed, only push it if you're subscribed.
+ if (this.state.listingType == ListingType.Subscribed) {
+ if (
+ this.state.subscribedCommunities
+ .map(c => c.community_id)
+ .includes(data.post.community_id)
+ ) {
+ this.state.posts.unshift(data.post);
+ notifyPost(data.post, this.context.router);
+ }
+ } else {
+ // NSFW posts
+ let nsfw = data.post.nsfw || data.post.community_nsfw;
+
+ // Don't push the post if its nsfw, and don't have that setting on
+ if (
+ !nsfw ||
+ (nsfw &&
+ UserService.Instance.user &&
+ UserService.Instance.user.show_nsfw)
+ ) {
+ this.state.posts.unshift(data.post);
+ notifyPost(data.post, this.context.router);
+ }
+ }
+ this.setState(this.state);
+ } else if (res.op == UserOperation.EditPost) {
+ let data = res.data as PostResponse;
+ editPostFindRes(data, this.state.posts);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreatePostLike) {
+ let data = res.data as PostResponse;
+ createPostLikeFindRes(data, this.state.posts);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.AddAdmin) {
+ let data = res.data as AddAdminResponse;
+ this.state.siteRes.admins = data.admins;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.BanUser) {
+ let data = res.data as BanUserResponse;
+ let found = this.state.siteRes.banned.find(u => (u.id = data.user.id));
+
+ // Remove the banned if its found in the list, and the action is an unban
+ if (found && !data.banned) {
+ this.state.siteRes.banned = this.state.siteRes.banned.filter(
+ i => i.id !== data.user.id
+ );
+ } else {
+ this.state.siteRes.banned.push(data.user);
+ }
+
+ this.state.posts
+ .filter(p => p.creator_id == data.user.id)
+ .forEach(p => (p.banned = data.banned));
+
+ this.setState(this.state);
+ } else if (res.op == UserOperation.GetComments) {
+ let data = res.data as GetCommentsResponse;
+ this.state.comments = data.comments;
+ this.state.loading = false;
+ this.setState(this.state);
+ } else if (
+ res.op == UserOperation.EditComment ||
+ res.op == UserOperation.DeleteComment ||
+ res.op == UserOperation.RemoveComment
+ ) {
+ let data = res.data as CommentResponse;
+ editCommentRes(data, this.state.comments);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreateComment) {
+ let data = res.data as CommentResponse;
+
+ // Necessary since it might be a user reply
+ if (data.recipient_ids.length == 0) {
+ // If you're on subscribed, only push it if you're subscribed.
+ if (this.state.listingType == ListingType.Subscribed) {
+ if (
+ this.state.subscribedCommunities
+ .map(c => c.community_id)
+ .includes(data.comment.community_id)
+ ) {
+ this.state.comments.unshift(data.comment);
+ }
+ } else {
+ this.state.comments.unshift(data.comment);
+ }
+ this.setState(this.state);
+ }
+ } else if (res.op == UserOperation.SaveComment) {
+ let data = res.data as CommentResponse;
+ saveCommentRes(data, this.state.comments);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreateCommentLike) {
+ let data = res.data as CommentResponse;
+ createCommentLikeRes(data, this.state.comments);
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Prompt } from 'inferno-router';
+import {
+ mdToHtml,
+ randomStr,
+ markdownHelpUrl,
+ toast,
+ setupTribute,
+ pictrsDeleteToast,
+ setupTippy,
+} from '../utils';
+import { UserService } from '../services';
+import autosize from 'autosize';
+import Tribute from 'tributejs/src/Tribute.js';
+import { i18n } from '../i18next';
+
+interface MarkdownTextAreaProps {
+ initialContent: string;
+ finished?: boolean;
+ buttonTitle?: string;
+ replyType?: boolean;
+ focus?: boolean;
+ disabled?: boolean;
+ maxLength?: number;
+ onSubmit?(msg: { val: string; formId: string }): any;
+ onContentChange?(val: string): any;
+ onReplyCancel?(): any;
+ hideNavigationWarnings?: boolean;
+}
+
+interface MarkdownTextAreaState {
+ content: string;
+ previewMode: boolean;
+ loading: boolean;
+ imageLoading: boolean;
+}
+
+export class MarkdownTextArea extends Component<
+ MarkdownTextAreaProps,
+ MarkdownTextAreaState
+> {
+ private id = `comment-textarea-${randomStr()}`;
+ private formId = `comment-form-${randomStr()}`;
+ private tribute: Tribute;
+ private emptyState: MarkdownTextAreaState = {
+ content: this.props.initialContent,
+ previewMode: false,
+ loading: false,
+ imageLoading: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ // TODO
+ /* this.tribute = setupTribute(); */
+ this.state = this.emptyState;
+ }
+
+ componentDidMount() {
+ let textarea: any = document.getElementById(this.id);
+ if (textarea) {
+ autosize(textarea);
+ this.tribute.attach(textarea);
+ textarea.addEventListener('tribute-replaced', () => {
+ this.state.content = textarea.value;
+ this.setState(this.state);
+ autosize.update(textarea);
+ });
+
+ this.quoteInsert();
+
+ if (this.props.focus) {
+ textarea.focus();
+ }
+
+ // TODO this is slow for some reason
+ setupTippy();
+ }
+ }
+
+ componentDidUpdate() {
+ if (!this.props.hideNavigationWarnings && this.state.content) {
+ window.onbeforeunload = () => true;
+ } else {
+ window.onbeforeunload = undefined;
+ }
+ }
+
+ componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
+ if (nextProps.finished) {
+ this.state.previewMode = false;
+ this.state.loading = false;
+ this.state.content = '';
+ this.setState(this.state);
+ if (this.props.replyType) {
+ this.props.onReplyCancel();
+ }
+
+ let textarea: any = document.getElementById(this.id);
+ let form: any = document.getElementById(this.formId);
+ form.reset();
+ setTimeout(() => autosize.update(textarea), 10);
+ this.setState(this.state);
+ }
+ }
+
+ componentWillUnmount() {
+ window.onbeforeunload = null;
+ }
+
+ render() {
+ return (
+ <form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}>
+ <Prompt
+ when={!this.props.hideNavigationWarnings && this.state.content}
+ message={i18n.t('block_leaving')}
+ />
+ <div class="form-group row">
+ <div className={`col-sm-12`}>
+ <textarea
+ id={this.id}
+ className={`form-control ${this.state.previewMode && 'd-none'}`}
+ value={this.state.content}
+ onInput={linkEvent(this, this.handleContentChange)}
+ onPaste={linkEvent(this, this.handleImageUploadPaste)}
+ required
+ disabled={this.props.disabled}
+ rows={2}
+ maxLength={this.props.maxLength || 10000}
+ />
+ {this.state.previewMode && (
+ <div
+ className="card bg-transparent border-secondary card-body md-div"
+ dangerouslySetInnerHTML={mdToHtml(this.state.content)}
+ />
+ )}
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-sm-12 d-flex flex-wrap">
+ {this.props.buttonTitle && (
+ <button
+ type="submit"
+ class="btn btn-sm btn-secondary mr-2"
+ disabled={this.props.disabled || this.state.loading}
+ >
+ {this.state.loading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ <span>{this.props.buttonTitle}</span>
+ )}
+ </button>
+ )}
+ {this.props.replyType && (
+ <button
+ type="button"
+ class="btn btn-sm btn-secondary mr-2"
+ onClick={linkEvent(this, this.handleReplyCancel)}
+ >
+ {i18n.t('cancel')}
+ </button>
+ )}
+ {this.state.content && (
+ <button
+ className={`btn btn-sm btn-secondary mr-2 ${
+ this.state.previewMode && 'active'
+ }`}
+ onClick={linkEvent(this, this.handlePreviewToggle)}
+ >
+ {i18n.t('preview')}
+ </button>
+ )}
+ {/* A flex expander */}
+ <div class="flex-grow-1"></div>
+ <button
+ class="btn btn-sm text-muted"
+ data-tippy-content={i18n.t('bold')}
+ onClick={linkEvent(this, this.handleInsertBold)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-bold"></use>
+ </svg>
+ </button>
+ <button
+ class="btn btn-sm text-muted"
+ data-tippy-content={i18n.t('italic')}
+ onClick={linkEvent(this, this.handleInsertItalic)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-italic"></use>
+ </svg>
+ </button>
+ <button
+ class="btn btn-sm text-muted"
+ data-tippy-content={i18n.t('link')}
+ onClick={linkEvent(this, this.handleInsertLink)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-link"></use>
+ </svg>
+ </button>
+ <form class="btn btn-sm text-muted font-weight-bold">
+ <label
+ htmlFor={`file-upload-${this.id}`}
+ className={`mb-0 ${UserService.Instance.user && 'pointer'}`}
+ data-tippy-content={i18n.t('upload_image')}
+ >
+ {this.state.imageLoading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-image"></use>
+ </svg>
+ )}
+ </label>
+ <input
+ id={`file-upload-${this.id}`}
+ type="file"
+ accept="image/*,video/*"
+ name="file"
+ class="d-none"
+ disabled={!UserService.Instance.user}
+ onChange={linkEvent(this, this.handleImageUpload)}
+ />
+ </form>
+ <button
+ class="btn btn-sm text-muted"
+ data-tippy-content={i18n.t('header')}
+ onClick={linkEvent(this, this.handleInsertHeader)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-header"></use>
+ </svg>
+ </button>
+ <button
+ class="btn btn-sm text-muted"
+ data-tippy-content={i18n.t('strikethrough')}
+ onClick={linkEvent(this, this.handleInsertStrikethrough)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-strikethrough"></use>
+ </svg>
+ </button>
+ <button
+ class="btn btn-sm text-muted"
+ data-tippy-content={i18n.t('quote')}
+ onClick={linkEvent(this, this.handleInsertQuote)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-format_quote"></use>
+ </svg>
+ </button>
+ <button
+ class="btn btn-sm text-muted"
+ data-tippy-content={i18n.t('list')}
+ onClick={linkEvent(this, this.handleInsertList)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-list"></use>
+ </svg>
+ </button>
+ <button
+ class="btn btn-sm text-muted"
+ data-tippy-content={i18n.t('code')}
+ onClick={linkEvent(this, this.handleInsertCode)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-code"></use>
+ </svg>
+ </button>
+ <button
+ class="btn btn-sm text-muted"
+ data-tippy-content={i18n.t('subscript')}
+ onClick={linkEvent(this, this.handleInsertSubscript)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-subscript"></use>
+ </svg>
+ </button>
+ <button
+ class="btn btn-sm text-muted"
+ data-tippy-content={i18n.t('superscript')}
+ onClick={linkEvent(this, this.handleInsertSuperscript)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-superscript"></use>
+ </svg>
+ </button>
+ <button
+ class="btn btn-sm text-muted"
+ data-tippy-content={i18n.t('spoiler')}
+ onClick={linkEvent(this, this.handleInsertSpoiler)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-alert-triangle"></use>
+ </svg>
+ </button>
+ <a
+ href={markdownHelpUrl}
+ target="_blank"
+ class="btn btn-sm text-muted font-weight-bold"
+ title={i18n.t('formatting_help')}
+ rel="noopener"
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-help-circle"></use>
+ </svg>
+ </a>
+ </div>
+ </div>
+ </form>
+ );
+ }
+
+ handleImageUploadPaste(i: MarkdownTextArea, event: any) {
+ let image = event.clipboardData.files[0];
+ if (image) {
+ i.handleImageUpload(i, image);
+ }
+ }
+
+ handleImageUpload(i: MarkdownTextArea, event: any) {
+ let file: any;
+ if (event.target) {
+ event.preventDefault();
+ file = event.target.files[0];
+ } else {
+ file = event;
+ }
+
+ const imageUploadUrl = `/pictrs/image`;
+ const formData = new FormData();
+ formData.append('images[]', file);
+
+ i.state.imageLoading = true;
+ i.setState(i.state);
+
+ fetch(imageUploadUrl, {
+ method: 'POST',
+ body: formData,
+ })
+ .then(res => res.json())
+ .then(res => {
+ console.log('pictrs upload:');
+ console.log(res);
+ if (res.msg == 'ok') {
+ let hash = res.files[0].file;
+ let url = `${window.location.origin}/pictrs/image/${hash}`;
+ let deleteToken = res.files[0].delete_token;
+ let deleteUrl = `${window.location.origin}/pictrs/image/delete/${deleteToken}/${hash}`;
+ let imageMarkdown = `![](${url})`;
+ let content = i.state.content;
+ content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
+ i.state.content = content;
+ i.state.imageLoading = false;
+ i.setState(i.state);
+ let textarea: any = document.getElementById(i.id);
+ autosize.update(textarea);
+ pictrsDeleteToast(
+ i18n.t('click_to_delete_picture'),
+ i18n.t('picture_deleted'),
+ deleteUrl
+ );
+ } else {
+ i.state.imageLoading = false;
+ i.setState(i.state);
+ toast(JSON.stringify(res), 'danger');
+ }
+ })
+ .catch(error => {
+ i.state.imageLoading = false;
+ i.setState(i.state);
+ toast(error, 'danger');
+ });
+ }
+
+ handleContentChange(i: MarkdownTextArea, event: any) {
+ i.state.content = event.target.value;
+ i.setState(i.state);
+ if (i.props.onContentChange) {
+ i.props.onContentChange(i.state.content);
+ }
+ }
+
+ handlePreviewToggle(i: MarkdownTextArea, event: any) {
+ event.preventDefault();
+ i.state.previewMode = !i.state.previewMode;
+ i.setState(i.state);
+ }
+
+ handleSubmit(i: MarkdownTextArea, event: any) {
+ event.preventDefault();
+ i.state.loading = true;
+ i.setState(i.state);
+ let msg = { val: i.state.content, formId: i.formId };
+ i.props.onSubmit(msg);
+ }
+
+ handleReplyCancel(i: MarkdownTextArea) {
+ i.props.onReplyCancel();
+ }
+
+ handleInsertLink(i: MarkdownTextArea, event: any) {
+ event.preventDefault();
+ if (!i.state.content) {
+ i.state.content = '';
+ }
+ let textarea: any = document.getElementById(i.id);
+ let start: number = textarea.selectionStart;
+ let end: number = textarea.selectionEnd;
+
+ if (start !== end) {
+ let selectedText = i.state.content.substring(start, end);
+ i.state.content = `${i.state.content.substring(
+ 0,
+ start
+ )} [${selectedText}]() ${i.state.content.substring(end)}`;
+ textarea.focus();
+ setTimeout(() => (textarea.selectionEnd = end + 4), 10);
+ } else {
+ i.state.content += '[]()';
+ textarea.focus();
+ setTimeout(() => (textarea.selectionEnd -= 1), 10);
+ }
+ i.setState(i.state);
+ }
+
+ simpleSurround(chars: string) {
+ this.simpleSurroundBeforeAfter(chars, chars);
+ }
+
+ simpleSurroundBeforeAfter(beforeChars: string, afterChars: string) {
+ if (!this.state.content) {
+ this.state.content = '';
+ }
+ let textarea: any = document.getElementById(this.id);
+ let start: number = textarea.selectionStart;
+ let end: number = textarea.selectionEnd;
+
+ if (start !== end) {
+ let selectedText = this.state.content.substring(start, end);
+ this.state.content = `${this.state.content.substring(
+ 0,
+ start - 1
+ )} ${beforeChars}${selectedText}${afterChars} ${this.state.content.substring(
+ end + 1
+ )}`;
+ } else {
+ this.state.content += `${beforeChars}___${afterChars}`;
+ }
+ this.setState(this.state);
+ setTimeout(() => {
+ autosize.update(textarea);
+ }, 10);
+ }
+
+ handleInsertBold(i: MarkdownTextArea, event: any) {
+ event.preventDefault();
+ i.simpleSurround('**');
+ }
+
+ handleInsertItalic(i: MarkdownTextArea, event: any) {
+ event.preventDefault();
+ i.simpleSurround('*');
+ }
+
+ handleInsertCode(i: MarkdownTextArea, event: any) {
+ event.preventDefault();
+ i.simpleSurround('`');
+ }
+
+ handleInsertStrikethrough(i: MarkdownTextArea, event: any) {
+ event.preventDefault();
+ i.simpleSurround('~~');
+ }
+
+ handleInsertList(i: MarkdownTextArea, event: any) {
+ event.preventDefault();
+ i.simpleInsert('-');
+ }
+
+ handleInsertQuote(i: MarkdownTextArea, event: any) {
+ event.preventDefault();
+ i.simpleInsert('>');
+ }
+
+ handleInsertHeader(i: MarkdownTextArea, event: any) {
+ event.preventDefault();
+ i.simpleInsert('#');
+ }
+
+ handleInsertSubscript(i: MarkdownTextArea, event: any) {
+ event.preventDefault();
+ i.simpleSurround('~');
+ }
+
+ handleInsertSuperscript(i: MarkdownTextArea, event: any) {
+ event.preventDefault();
+ i.simpleSurround('^');
+ }
+
+ simpleInsert(chars: string) {
+ if (!this.state.content) {
+ this.state.content = `${chars} `;
+ } else {
+ this.state.content += `\n${chars} `;
+ }
+
+ let textarea: any = document.getElementById(this.id);
+ textarea.focus();
+ setTimeout(() => {
+ autosize.update(textarea);
+ }, 10);
+ this.setState(this.state);
+ }
+
+ handleInsertSpoiler(i: MarkdownTextArea, event: any) {
+ event.preventDefault();
+ let beforeChars = `\n::: spoiler ${i18n.t('spoiler')}\n`;
+ let afterChars = '\n:::\n';
+ i.simpleSurroundBeforeAfter(beforeChars, afterChars);
+ }
+
+ quoteInsert() {
+ let textarea: any = document.getElementById(this.id);
+ let selectedText = window.getSelection().toString();
+ if (selectedText) {
+ let quotedText =
+ selectedText
+ .split('\n')
+ .map(t => `> ${t}`)
+ .join('\n') + '\n\n';
+ this.state.content = quotedText;
+ this.setState(this.state);
+ // Not sure why this needs a delay
+ setTimeout(() => autosize.update(textarea), 10);
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Link } from 'inferno-router';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ UserOperation,
+ GetModlogForm,
+ GetModlogResponse,
+ ModRemovePost,
+ ModLockPost,
+ ModStickyPost,
+ ModRemoveComment,
+ ModRemoveCommunity,
+ ModBanFromCommunity,
+ ModBan,
+ ModAddCommunity,
+ ModAdd,
+ WebSocketJsonResponse,
+ GetSiteResponse,
+ Site,
+} from 'lemmy-js-client';
+import { WebSocketService } from '../services';
+import { wsJsonToRes, addTypeInfo, fetchLimit, toast } from '../utils';
+import { MomentTime } from './moment-time';
+import moment from 'moment';
+import { i18n } from '../i18next';
+
+interface ModlogState {
+ combined: {
+ type_: string;
+ data:
+ | ModRemovePost
+ | ModLockPost
+ | ModStickyPost
+ | ModRemoveCommunity
+ | ModAdd
+ | ModBan;
+ }[];
+ communityId?: number;
+ communityName?: string;
+ page: number;
+ site: Site;
+ loading: boolean;
+}
+
+export class Modlog extends Component<any, ModlogState> {
+ private subscription: Subscription;
+ private emptyState: ModlogState = {
+ combined: [],
+ page: 1,
+ loading: true,
+ site: undefined,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.state.communityId = this.props.match.params.community_id
+ ? Number(this.props.match.params.community_id)
+ : undefined;
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ this.refetch();
+ WebSocketService.Instance.getSite();
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ setCombined(res: GetModlogResponse) {
+ let removed_posts = addTypeInfo(res.removed_posts, 'removed_posts');
+ let locked_posts = addTypeInfo(res.locked_posts, 'locked_posts');
+ let stickied_posts = addTypeInfo(res.stickied_posts, 'stickied_posts');
+ let removed_comments = addTypeInfo(
+ res.removed_comments,
+ 'removed_comments'
+ );
+ let removed_communities = addTypeInfo(
+ res.removed_communities,
+ 'removed_communities'
+ );
+ let banned_from_community = addTypeInfo(
+ res.banned_from_community,
+ 'banned_from_community'
+ );
+ let added_to_community = addTypeInfo(
+ res.added_to_community,
+ 'added_to_community'
+ );
+ let added = addTypeInfo(res.added, 'added');
+ let banned = addTypeInfo(res.banned, 'banned');
+ this.state.combined = [];
+
+ this.state.combined.push(...removed_posts);
+ this.state.combined.push(...locked_posts);
+ this.state.combined.push(...stickied_posts);
+ this.state.combined.push(...removed_comments);
+ this.state.combined.push(...removed_communities);
+ this.state.combined.push(...banned_from_community);
+ this.state.combined.push(...added_to_community);
+ this.state.combined.push(...added);
+ this.state.combined.push(...banned);
+
+ if (this.state.communityId && this.state.combined.length > 0) {
+ this.state.communityName = (this.state.combined[0]
+ .data as ModRemovePost).community_name;
+ }
+
+ // Sort them by time
+ this.state.combined.sort((a, b) =>
+ b.data.when_.localeCompare(a.data.when_)
+ );
+
+ this.setState(this.state);
+ }
+
+ combined() {
+ return (
+ <tbody>
+ {this.state.combined.map(i => (
+ <tr>
+ <td>
+ <MomentTime data={i.data} />
+ </td>
+ <td>
+ <Link to={`/u/${i.data.mod_user_name}`}>
+ {i.data.mod_user_name}
+ </Link>
+ </td>
+ <td>
+ {i.type_ == 'removed_posts' && (
+ <>
+ {(i.data as ModRemovePost).removed ? 'Removed' : 'Restored'}
+ <span>
+ {' '}
+ Post{' '}
+ <Link to={`/post/${(i.data as ModRemovePost).post_id}`}>
+ {(i.data as ModRemovePost).post_name}
+ </Link>
+ </span>
+ <div>
+ {(i.data as ModRemovePost).reason &&
+ ` reason: ${(i.data as ModRemovePost).reason}`}
+ </div>
+ </>
+ )}
+ {i.type_ == 'locked_posts' && (
+ <>
+ {(i.data as ModLockPost).locked ? 'Locked' : 'Unlocked'}
+ <span>
+ {' '}
+ Post{' '}
+ <Link to={`/post/${(i.data as ModLockPost).post_id}`}>
+ {(i.data as ModLockPost).post_name}
+ </Link>
+ </span>
+ </>
+ )}
+ {i.type_ == 'stickied_posts' && (
+ <>
+ {(i.data as ModStickyPost).stickied
+ ? 'Stickied'
+ : 'Unstickied'}
+ <span>
+ {' '}
+ Post{' '}
+ <Link to={`/post/${(i.data as ModStickyPost).post_id}`}>
+ {(i.data as ModStickyPost).post_name}
+ </Link>
+ </span>
+ </>
+ )}
+ {i.type_ == 'removed_comments' && (
+ <>
+ {(i.data as ModRemoveComment).removed
+ ? 'Removed'
+ : 'Restored'}
+ <span>
+ {' '}
+ Comment{' '}
+ <Link
+ to={`/post/${
+ (i.data as ModRemoveComment).post_id
+ }/comment/${(i.data as ModRemoveComment).comment_id}`}
+ >
+ {(i.data as ModRemoveComment).comment_content}
+ </Link>
+ </span>
+ <span>
+ {' '}
+ by{' '}
+ <Link
+ to={`/u/${
+ (i.data as ModRemoveComment).comment_user_name
+ }`}
+ >
+ {(i.data as ModRemoveComment).comment_user_name}
+ </Link>
+ </span>
+ <div>
+ {(i.data as ModRemoveComment).reason &&
+ ` reason: ${(i.data as ModRemoveComment).reason}`}
+ </div>
+ </>
+ )}
+ {i.type_ == 'removed_communities' && (
+ <>
+ {(i.data as ModRemoveCommunity).removed
+ ? 'Removed'
+ : 'Restored'}
+ <span>
+ {' '}
+ Community{' '}
+ <Link
+ to={`/c/${(i.data as ModRemoveCommunity).community_name}`}
+ >
+ {(i.data as ModRemoveCommunity).community_name}
+ </Link>
+ </span>
+ <div>
+ {(i.data as ModRemoveCommunity).reason &&
+ ` reason: ${(i.data as ModRemoveCommunity).reason}`}
+ </div>
+ <div>
+ {(i.data as ModRemoveCommunity).expires &&
+ ` expires: ${moment
+ .utc((i.data as ModRemoveCommunity).expires)
+ .fromNow()}`}
+ </div>
+ </>
+ )}
+ {i.type_ == 'banned_from_community' && (
+ <>
+ <span>
+ {(i.data as ModBanFromCommunity).banned
+ ? 'Banned '
+ : 'Unbanned '}{' '}
+ </span>
+ <span>
+ <Link
+ to={`/u/${
+ (i.data as ModBanFromCommunity).other_user_name
+ }`}
+ >
+ {(i.data as ModBanFromCommunity).other_user_name}
+ </Link>
+ </span>
+ <span> from the community </span>
+ <span>
+ <Link
+ to={`/c/${
+ (i.data as ModBanFromCommunity).community_name
+ }`}
+ >
+ {(i.data as ModBanFromCommunity).community_name}
+ </Link>
+ </span>
+ <div>
+ {(i.data as ModBanFromCommunity).reason &&
+ ` reason: ${(i.data as ModBanFromCommunity).reason}`}
+ </div>
+ <div>
+ {(i.data as ModBanFromCommunity).expires &&
+ ` expires: ${moment
+ .utc((i.data as ModBanFromCommunity).expires)
+ .fromNow()}`}
+ </div>
+ </>
+ )}
+ {i.type_ == 'added_to_community' && (
+ <>
+ <span>
+ {(i.data as ModAddCommunity).removed
+ ? 'Removed '
+ : 'Appointed '}{' '}
+ </span>
+ <span>
+ <Link
+ to={`/u/${(i.data as ModAddCommunity).other_user_name}`}
+ >
+ {(i.data as ModAddCommunity).other_user_name}
+ </Link>
+ </span>
+ <span> as a mod to the community </span>
+ <span>
+ <Link
+ to={`/c/${(i.data as ModAddCommunity).community_name}`}
+ >
+ {(i.data as ModAddCommunity).community_name}
+ </Link>
+ </span>
+ </>
+ )}
+ {i.type_ == 'banned' && (
+ <>
+ <span>
+ {(i.data as ModBan).banned ? 'Banned ' : 'Unbanned '}{' '}
+ </span>
+ <span>
+ <Link to={`/u/${(i.data as ModBan).other_user_name}`}>
+ {(i.data as ModBan).other_user_name}
+ </Link>
+ </span>
+ <div>
+ {(i.data as ModBan).reason &&
+ ` reason: ${(i.data as ModBan).reason}`}
+ </div>
+ <div>
+ {(i.data as ModBan).expires &&
+ ` expires: ${moment
+ .utc((i.data as ModBan).expires)
+ .fromNow()}`}
+ </div>
+ </>
+ )}
+ {i.type_ == 'added' && (
+ <>
+ <span>
+ {(i.data as ModAdd).removed ? 'Removed ' : 'Appointed '}{' '}
+ </span>
+ <span>
+ <Link to={`/u/${(i.data as ModAdd).other_user_name}`}>
+ {(i.data as ModAdd).other_user_name}
+ </Link>
+ </span>
+ <span> as an admin </span>
+ </>
+ )}
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ );
+ }
+
+ get documentTitle(): string {
+ if (this.state.site) {
+ return `Modlog - ${this.state.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle} />
+ {this.state.loading ? (
+ <h5 class="">
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ </h5>
+ ) : (
+ <div>
+ <h5>
+ {this.state.communityName && (
+ <Link
+ className="text-body"
+ to={`/c/${this.state.communityName}`}
+ >
+ /c/{this.state.communityName}{' '}
+ </Link>
+ )}
+ <span>{i18n.t('modlog')}</span>
+ </h5>
+ <div class="table-responsive">
+ <table id="modlog_table" class="table table-sm table-hover">
+ <thead class="pointer">
+ <tr>
+ <th> {i18n.t('time')}</th>
+ <th>{i18n.t('mod')}</th>
+ <th>{i18n.t('action')}</th>
+ </tr>
+ </thead>
+ {this.combined()}
+ </table>
+ {this.paginator()}
+ </div>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ paginator() {
+ return (
+ <div class="mt-2">
+ {this.state.page > 1 && (
+ <button
+ class="btn btn-secondary mr-1"
+ onClick={linkEvent(this, this.prevPage)}
+ >
+ {i18n.t('prev')}
+ </button>
+ )}
+ <button
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.nextPage)}
+ >
+ {i18n.t('next')}
+ </button>
+ </div>
+ );
+ }
+
+ nextPage(i: Modlog) {
+ i.state.page++;
+ i.setState(i.state);
+ i.refetch();
+ }
+
+ prevPage(i: Modlog) {
+ i.state.page--;
+ i.setState(i.state);
+ i.refetch();
+ }
+
+ refetch() {
+ let modlogForm: GetModlogForm = {
+ community_id: this.state.communityId,
+ page: this.state.page,
+ limit: fetchLimit,
+ };
+ WebSocketService.Instance.getModlog(modlogForm);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ return;
+ } else if (res.op == UserOperation.GetModlog) {
+ let data = res.data as GetModlogResponse;
+ this.state.loading = false;
+ window.scrollTo(0, 0);
+ this.setCombined(data);
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.site = data.site;
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import moment from 'moment';
+import { getMomentLanguage, capitalizeFirstLetter } from '../utils';
+import { i18n } from '../i18next';
+
+interface MomentTimeProps {
+ data: {
+ published?: string;
+ when_?: string;
+ updated?: string;
+ };
+ showAgo?: boolean;
+}
+
+export class MomentTime extends Component<MomentTimeProps, any> {
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ let lang = getMomentLanguage();
+
+ moment.locale(lang);
+ }
+
+ render() {
+ if (this.props.data.updated) {
+ return (
+ <span
+ data-tippy-content={`${capitalizeFirstLetter(
+ i18n.t('modified')
+ )} ${this.format(this.props.data.updated)}`}
+ className="font-italics pointer unselectable"
+ >
+ <svg class="icon icon-inline mr-1">
+ <use xlinkHref="#icon-edit-2"></use>
+ </svg>
+ {moment.utc(this.props.data.updated).fromNow(!this.props.showAgo)}
+ </span>
+ );
+ } else {
+ let str = this.props.data.published || this.props.data.when_;
+ return (
+ <span
+ className="pointer unselectable"
+ data-tippy-content={this.format(str)}
+ >
+ {moment.utc(str).fromNow(!this.props.showAgo)}
+ </span>
+ );
+ }
+ }
+
+ format(input: string): string {
+ return moment.utc(input).local().format('LLLL');
+ }
+}
--- /dev/null
+import { Component, linkEvent, createRef, RefObject } from 'inferno';
+import { Link } from 'inferno-router';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { WebSocketService, UserService } from '../services';
+import {
+ UserOperation,
+ GetRepliesForm,
+ GetRepliesResponse,
+ GetUserMentionsForm,
+ GetUserMentionsResponse,
+ GetPrivateMessagesForm,
+ PrivateMessagesResponse,
+ SortType,
+ GetSiteResponse,
+ Comment,
+ CommentResponse,
+ PrivateMessage,
+ PrivateMessageResponse,
+ WebSocketJsonResponse,
+} from 'lemmy-js-client';
+import {
+ wsJsonToRes,
+ pictrsAvatarThumbnail,
+ showAvatars,
+ fetchLimit,
+ toast,
+ setTheme,
+ getLanguage,
+ notifyComment,
+ notifyPrivateMessage,
+ isBrowser,
+} from '../utils';
+import { i18n } from '../i18next';
+
+interface NavbarState {
+ isLoggedIn: boolean;
+ expanded: boolean;
+ replies: Comment[];
+ mentions: Comment[];
+ messages: PrivateMessage[];
+ unreadCount: number;
+ searchParam: string;
+ toggleSearch: boolean;
+ siteLoading: boolean;
+ siteRes: GetSiteResponse;
+ onSiteBanner?(url: string): any;
+}
+
+export class Navbar extends Component<any, NavbarState> {
+ private wsSub: Subscription;
+ private userSub: Subscription;
+ private unreadCountSub: Subscription;
+ private searchTextField: RefObject<HTMLInputElement>;
+ emptyState: NavbarState = {
+ isLoggedIn: false,
+ unreadCount: 0,
+ replies: [],
+ mentions: [],
+ messages: [],
+ expanded: false,
+ siteRes: {
+ site: {
+ id: null,
+ name: null,
+ creator_id: null,
+ creator_name: null,
+ published: null,
+ number_of_users: null,
+ number_of_posts: null,
+ number_of_comments: null,
+ number_of_communities: null,
+ enable_downvotes: null,
+ open_registration: null,
+ enable_nsfw: null,
+ icon: null,
+ banner: null,
+ creator_preferred_username: null,
+ },
+ my_user: null,
+ admins: [],
+ banned: [],
+ online: null,
+ version: null,
+ federated_instances: null,
+ },
+ searchParam: '',
+ toggleSearch: false,
+ siteLoading: true,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.state = this.emptyState;
+
+ if (isBrowser()) {
+ this.wsSub = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ WebSocketService.Instance.getSite();
+
+ this.searchTextField = createRef();
+ }
+ }
+
+ componentDidMount() {
+ if (isBrowser()) {
+ // Subscribe to jwt changes
+ this.userSub = UserService.Instance.jwtSub.subscribe(res => {
+ // A login
+ if (res !== undefined) {
+ this.requestNotificationPermission();
+ } else {
+ this.state.isLoggedIn = false;
+ }
+ WebSocketService.Instance.getSite();
+ this.setState(this.state);
+ });
+
+ // Subscribe to unread count changes
+ this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(
+ res => {
+ this.setState({ unreadCount: res });
+ }
+ );
+ }
+ }
+
+ handleSearchParam(i: Navbar, event: any) {
+ i.state.searchParam = event.target.value;
+ i.setState(i.state);
+ }
+
+ updateUrl() {
+ /* const searchParam = this.state.searchParam; */
+ /* this.setState({ searchParam: '' }); */
+ /* this.setState({ toggleSearch: false }); */
+ /* if (searchParam === '') { */
+ /* this.context.router.history.push(`/search/`); */
+ /* } else { */
+ /* this.context.router.history.push( */
+ /* `/search/q/${searchParam}/type/All/sort/TopAll/page/1` */
+ /* ); */
+ /* } */
+ }
+
+ handleSearchSubmit(i: Navbar, event: any) {
+ event.preventDefault();
+ i.updateUrl();
+ }
+
+ handleSearchBtn(i: Navbar, event: any) {
+ event.preventDefault();
+ i.setState({ toggleSearch: true });
+
+ i.searchTextField.current.focus();
+ const offsetWidth = i.searchTextField.current.offsetWidth;
+ if (i.state.searchParam && offsetWidth > 100) {
+ i.updateUrl();
+ }
+ }
+
+ handleSearchBlur(i: Navbar, event: any) {
+ if (!(event.relatedTarget && event.relatedTarget.name !== 'search-btn')) {
+ i.state.toggleSearch = false;
+ i.setState(i.state);
+ }
+ }
+
+ render() {
+ return this.navbar();
+ }
+
+ componentWillUnmount() {
+ this.wsSub.unsubscribe();
+ this.userSub.unsubscribe();
+ this.unreadCountSub.unsubscribe();
+ }
+
+ // TODO class active corresponding to current page
+ navbar() {
+ let user = UserService.Instance.user;
+ let expandedClass = `${!this.state.expanded && 'collapse'} navbar-collapse`;
+
+ return (
+ <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
+ <div class="container">
+ {!this.state.siteLoading ? (
+ <Link
+ title={this.state.siteRes.version}
+ class="d-flex align-items-center navbar-brand mr-md-3"
+ to="/"
+ >
+ {this.state.siteRes.site.icon && showAvatars() && (
+ <img
+ src={pictrsAvatarThumbnail(this.state.siteRes.site.icon)}
+ height="32"
+ width="32"
+ class="rounded-circle mr-2"
+ />
+ )}
+ {this.state.siteRes.site.name}
+ </Link>
+ ) : (
+ <div class="navbar-item">
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ </div>
+ )}
+ {this.state.isLoggedIn && (
+ <Link
+ class="ml-auto p-0 navbar-toggler nav-link border-0"
+ to="/inbox"
+ title={i18n.t('inbox')}
+ >
+ <svg class="icon">
+ <use xlinkHref="#icon-bell"></use>
+ </svg>
+ {this.state.unreadCount > 0 && (
+ <span class="mx-1 badge badge-light">
+ {this.state.unreadCount}
+ </span>
+ )}
+ </Link>
+ )}
+ <button
+ class="navbar-toggler border-0 p-1"
+ type="button"
+ aria-label="menu"
+ onClick={linkEvent(this, this.expandNavbar)}
+ data-tippy-content={i18n.t('expand_here')}
+ >
+ <span class="navbar-toggler-icon"></span>
+ </button>
+ {/* TODO this isn't working
+ className={`${!this.state.expanded && 'collapse'
+ } navbar-collapse`}
+ */}
+ {!this.state.siteLoading && (
+ <div class="navbar-collapse">
+ <ul class="navbar-nav my-2 mr-auto">
+ <li class="nav-item">
+ <Link
+ class="nav-link"
+ to="/communities"
+ title={i18n.t('communities')}
+ >
+ {i18n.t('communities')}
+ </Link>
+ </li>
+ <li class="nav-item">
+ <Link
+ class="nav-link"
+ to={{
+ pathname: '/create_post',
+ state: { prevPath: this.currentLocation },
+ }}
+ title={i18n.t('create_post')}
+ >
+ {i18n.t('create_post')}
+ </Link>
+ </li>
+ <li class="nav-item">
+ <Link
+ class="nav-link"
+ to="/create_community"
+ title={i18n.t('create_community')}
+ >
+ {i18n.t('create_community')}
+ </Link>
+ </li>
+ <li className="nav-item">
+ <Link
+ class="nav-link"
+ to="/sponsors"
+ title={i18n.t('donate_to_lemmy')}
+ >
+ <svg class="icon">
+ <use xlinkHref="#icon-coffee"></use>
+ </svg>
+ </Link>
+ </li>
+ </ul>
+ <ul class="navbar-nav my-2">
+ {this.canAdmin && (
+ <li className="nav-item">
+ <Link
+ class="nav-link"
+ to={`/admin`}
+ title={i18n.t('admin_settings')}
+ >
+ <svg class="icon">
+ <use xlinkHref="#icon-settings"></use>
+ </svg>
+ </Link>
+ </li>
+ )}
+ </ul>
+ {!this.context.router.history.location.pathname.match(
+ /^\/search/
+ ) && (
+ <form
+ class="form-inline"
+ onSubmit={linkEvent(this, this.handleSearchSubmit)}
+ >
+ {/* TODO No idea why, but this class here fails
+ class={`form-control mr-0 search-input ${
+ this.state.toggleSearch ? 'show-input' : 'hide-input'
+ }`}
+
+ */}
+ <input
+ onInput={linkEvent(this, this.handleSearchParam)}
+ value={this.state.searchParam}
+ type="text"
+ placeholder={i18n.t('search')}
+ onBlur={linkEvent(this, this.handleSearchBlur)}
+ ></input>
+ <button
+ name="search-btn"
+ onClick={linkEvent(this, this.handleSearchBtn)}
+ class="px-1 btn btn-link"
+ style="color: var(--gray)"
+ >
+ <svg class="icon">
+ <use xlinkHref="#icon-search"></use>
+ </svg>
+ </button>
+ </form>
+ )}
+ {this.state.isLoggedIn ? (
+ <>
+ <ul class="navbar-nav my-2">
+ <li className="nav-item">
+ <Link
+ class="nav-link"
+ to="/inbox"
+ title={i18n.t('inbox')}
+ >
+ <svg class="icon">
+ <use xlinkHref="#icon-bell"></use>
+ </svg>
+ {this.state.unreadCount > 0 && (
+ <span class="ml-1 badge badge-light">
+ {this.state.unreadCount}
+ </span>
+ )}
+ </Link>
+ </li>
+ </ul>
+ <ul class="navbar-nav">
+ <li className="nav-item">
+ <Link
+ class="nav-link"
+ to={`/u/${user.name}`}
+ title={i18n.t('settings')}
+ >
+ <span>
+ {user.avatar && showAvatars() && (
+ <img
+ src={pictrsAvatarThumbnail(user.avatar)}
+ height="32"
+ width="32"
+ class="rounded-circle mr-2"
+ />
+ )}
+ {user.preferred_username
+ ? user.preferred_username
+ : user.name}
+ </span>
+ </Link>
+ </li>
+ </ul>
+ </>
+ ) : (
+ <ul class="navbar-nav my-2">
+ <li className="ml-2 nav-item">
+ <Link
+ class="btn btn-success"
+ to="/login"
+ title={i18n.t('login_sign_up')}
+ >
+ {i18n.t('login_sign_up')}
+ </Link>
+ </li>
+ </ul>
+ )}
+ </div>
+ )}
+ </div>
+ </nav>
+ );
+ }
+
+ expandNavbar(i: Navbar) {
+ i.state.expanded = !i.state.expanded;
+ i.setState(i.state);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+ console.log(res);
+ if (msg.error) {
+ if (msg.error == 'not_logged_in') {
+ UserService.Instance.logout();
+ location.reload();
+ }
+ return;
+ } else if (msg.reconnect) {
+ this.fetchUnreads();
+ } else if (res.op == UserOperation.GetReplies) {
+ let data = res.data as GetRepliesResponse;
+ let unreadReplies = data.replies.filter(r => !r.read);
+
+ this.state.replies = unreadReplies;
+ this.state.unreadCount = this.calculateUnreadCount();
+ this.setState(this.state);
+ this.sendUnreadCount();
+ } else if (res.op == UserOperation.GetUserMentions) {
+ let data = res.data as GetUserMentionsResponse;
+ let unreadMentions = data.mentions.filter(r => !r.read);
+
+ this.state.mentions = unreadMentions;
+ this.state.unreadCount = this.calculateUnreadCount();
+ this.setState(this.state);
+ this.sendUnreadCount();
+ } else if (res.op == UserOperation.GetPrivateMessages) {
+ let data = res.data as PrivateMessagesResponse;
+ let unreadMessages = data.messages.filter(r => !r.read);
+
+ this.state.messages = unreadMessages;
+ this.state.unreadCount = this.calculateUnreadCount();
+ this.setState(this.state);
+ this.sendUnreadCount();
+ } else if (res.op == UserOperation.CreateComment) {
+ let data = res.data as CommentResponse;
+
+ if (this.state.isLoggedIn) {
+ if (data.recipient_ids.includes(UserService.Instance.user.id)) {
+ this.state.replies.push(data.comment);
+ this.state.unreadCount++;
+ this.setState(this.state);
+ this.sendUnreadCount();
+ notifyComment(data.comment, this.context.router);
+ }
+ }
+ } else if (res.op == UserOperation.CreatePrivateMessage) {
+ let data = res.data as PrivateMessageResponse;
+
+ if (this.state.isLoggedIn) {
+ if (data.message.recipient_id == UserService.Instance.user.id) {
+ this.state.messages.push(data.message);
+ this.state.unreadCount++;
+ this.setState(this.state);
+ this.sendUnreadCount();
+ notifyPrivateMessage(data.message, this.context.router);
+ }
+ }
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+
+ this.state.siteRes = data;
+
+ // The login
+ if (data.my_user) {
+ UserService.Instance.user = data.my_user;
+ WebSocketService.Instance.userJoin();
+ // On the first load, check the unreads
+ if (this.state.isLoggedIn == false) {
+ this.requestNotificationPermission();
+ this.fetchUnreads();
+ setTheme(data.my_user.theme, true);
+ i18n.changeLanguage(getLanguage());
+ }
+ this.state.isLoggedIn = true;
+ }
+ }
+
+ this.state.siteLoading = false;
+ this.setState(this.state);
+ }
+
+ fetchUnreads() {
+ console.log('Fetching unreads...');
+ let repliesForm: GetRepliesForm = {
+ sort: SortType.New,
+ unread_only: true,
+ page: 1,
+ limit: fetchLimit,
+ };
+
+ let userMentionsForm: GetUserMentionsForm = {
+ sort: SortType.New,
+ unread_only: true,
+ page: 1,
+ limit: fetchLimit,
+ };
+
+ let privateMessagesForm: GetPrivateMessagesForm = {
+ unread_only: true,
+ page: 1,
+ limit: fetchLimit,
+ };
+
+ if (this.currentLocation !== '/inbox') {
+ WebSocketService.Instance.getReplies(repliesForm);
+ WebSocketService.Instance.getUserMentions(userMentionsForm);
+ WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
+ }
+ }
+
+ get currentLocation() {
+ return this.context.router.history.location.pathname;
+ }
+
+ sendUnreadCount() {
+ UserService.Instance.unreadCountSub.next(this.state.unreadCount);
+ }
+
+ calculateUnreadCount(): number {
+ return (
+ this.state.replies.filter(r => !r.read).length +
+ this.state.mentions.filter(r => !r.read).length +
+ this.state.messages.filter(r => !r.read).length
+ );
+ }
+
+ get canAdmin(): boolean {
+ return (
+ UserService.Instance.user &&
+ this.state.siteRes.admins
+ .map(a => a.id)
+ .includes(UserService.Instance.user.id)
+ );
+ }
+
+ requestNotificationPermission() {
+ if (UserService.Instance.user) {
+ document.addEventListener('DOMContentLoaded', function () {
+ if (!Notification) {
+ toast(i18n.t('notifications_error'), 'danger');
+ return;
+ }
+
+ if (Notification.permission !== 'granted')
+ Notification.requestPermission();
+ });
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ UserOperation,
+ LoginResponse,
+ PasswordChangeForm,
+ WebSocketJsonResponse,
+ GetSiteResponse,
+ Site,
+} from 'lemmy-js-client';
+import { WebSocketService, UserService } from '../services';
+import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
+import { i18n } from '../i18next';
+
+interface State {
+ passwordChangeForm: PasswordChangeForm;
+ loading: boolean;
+ site: Site;
+}
+
+export class PasswordChange extends Component<any, State> {
+ private subscription: Subscription;
+
+ emptyState: State = {
+ passwordChangeForm: {
+ token: this.props.match.params.token,
+ password: undefined,
+ password_verify: undefined,
+ },
+ loading: false,
+ site: undefined,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+ WebSocketService.Instance.getSite();
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ get documentTitle(): string {
+ if (this.state.site) {
+ return `${i18n.t('password_change')} - ${this.state.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle} />
+ <div class="row">
+ <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+ <h5>{i18n.t('password_change')}</h5>
+ {this.passwordChangeForm()}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ passwordChangeForm() {
+ return (
+ <form onSubmit={linkEvent(this, this.handlePasswordChangeSubmit)}>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">
+ {i18n.t('new_password')}
+ </label>
+ <div class="col-sm-10">
+ <input
+ type="password"
+ value={this.state.passwordChangeForm.password}
+ onInput={linkEvent(this, this.handlePasswordChange)}
+ class="form-control"
+ required
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">
+ {i18n.t('verify_password')}
+ </label>
+ <div class="col-sm-10">
+ <input
+ type="password"
+ value={this.state.passwordChangeForm.password_verify}
+ onInput={linkEvent(this, this.handleVerifyPasswordChange)}
+ class="form-control"
+ required
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <button type="submit" class="btn btn-secondary">
+ {this.state.loading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ capitalizeFirstLetter(i18n.t('save'))
+ )}
+ </button>
+ </div>
+ </div>
+ </form>
+ );
+ }
+
+ handlePasswordChange(i: PasswordChange, event: any) {
+ i.state.passwordChangeForm.password = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleVerifyPasswordChange(i: PasswordChange, event: any) {
+ i.state.passwordChangeForm.password_verify = event.target.value;
+ i.setState(i.state);
+ }
+
+ handlePasswordChangeSubmit(i: PasswordChange, event: any) {
+ event.preventDefault();
+ i.state.loading = true;
+ i.setState(i.state);
+
+ WebSocketService.Instance.passwordChange(i.state.passwordChangeForm);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ this.state.loading = false;
+ this.setState(this.state);
+ return;
+ } else if (res.op == UserOperation.PasswordChange) {
+ let data = res.data as LoginResponse;
+ this.state = this.emptyState;
+ this.setState(this.state);
+ UserService.Instance.login(data);
+ this.props.history.push('/');
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.site = data.site;
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Prompt } from 'inferno-router';
+import { PostListings } from './post-listings';
+import { MarkdownTextArea } from './markdown-textarea';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ PostForm as PostFormI,
+ PostFormParams,
+ Post,
+ PostResponse,
+ UserOperation,
+ Community,
+ ListCommunitiesResponse,
+ ListCommunitiesForm,
+ SortType,
+ SearchForm,
+ SearchType,
+ SearchResponse,
+ WebSocketJsonResponse,
+} from 'lemmy-js-client';
+import { WebSocketService, UserService } from '../services';
+import {
+ wsJsonToRes,
+ getPageTitle,
+ validURL,
+ capitalizeFirstLetter,
+ archiveUrl,
+ debounce,
+ isImage,
+ toast,
+ randomStr,
+ setupTippy,
+ hostname,
+ pictrsDeleteToast,
+ validTitle,
+} from '../utils';
+import Choices from 'choices.js';
+import { i18n } from '../i18next';
+
+const MAX_POST_TITLE_LENGTH = 200;
+
+interface PostFormProps {
+ post?: Post; // If a post is given, that means this is an edit
+ params?: PostFormParams;
+ onCancel?(): any;
+ onCreate?(id: number): any;
+ onEdit?(post: Post): any;
+ enableNsfw: boolean;
+ enableDownvotes: boolean;
+}
+
+interface PostFormState {
+ postForm: PostFormI;
+ communities: Community[];
+ loading: boolean;
+ imageLoading: boolean;
+ previewMode: boolean;
+ suggestedTitle: string;
+ suggestedPosts: Post[];
+ crossPosts: Post[];
+}
+
+export class PostForm extends Component<PostFormProps, PostFormState> {
+ private id = `post-form-${randomStr()}`;
+ private subscription: Subscription;
+ private choices: Choices;
+ private emptyState: PostFormState = {
+ postForm: {
+ name: null,
+ nsfw: false,
+ auth: null,
+ community_id: null,
+ },
+ communities: [],
+ loading: false,
+ imageLoading: false,
+ previewMode: false,
+ suggestedTitle: undefined,
+ suggestedPosts: [],
+ crossPosts: [],
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
+ this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
+ this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
+
+ this.state = this.emptyState;
+
+ if (this.props.post) {
+ this.state.postForm = {
+ body: this.props.post.body,
+ // NOTE: debouncing breaks both these for some reason, unless you use defaultValue
+ name: this.props.post.name,
+ community_id: this.props.post.community_id,
+ edit_id: this.props.post.id,
+ url: this.props.post.url,
+ nsfw: this.props.post.nsfw,
+ auth: null,
+ };
+ }
+
+ if (this.props.params) {
+ this.state.postForm.name = this.props.params.name;
+ if (this.props.params.url) {
+ this.state.postForm.url = this.props.params.url;
+ }
+ if (this.props.params.body) {
+ this.state.postForm.body = this.props.params.body;
+ }
+ }
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ let listCommunitiesForm: ListCommunitiesForm = {
+ sort: SortType.TopAll,
+ limit: 9999,
+ };
+
+ WebSocketService.Instance.listCommunities(listCommunitiesForm);
+ }
+
+ componentDidMount() {
+ setupTippy();
+ }
+
+ componentDidUpdate() {
+ if (
+ !this.state.loading &&
+ (this.state.postForm.name ||
+ this.state.postForm.url ||
+ this.state.postForm.body)
+ ) {
+ window.onbeforeunload = () => true;
+ } else {
+ window.onbeforeunload = undefined;
+ }
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ /* this.choices && this.choices.destroy(); */
+ window.onbeforeunload = null;
+ }
+
+ render() {
+ return (
+ <div>
+ <Prompt
+ when={
+ !this.state.loading &&
+ (this.state.postForm.name ||
+ this.state.postForm.url ||
+ this.state.postForm.body)
+ }
+ message={i18n.t('block_leaving')}
+ />
+ <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label" htmlFor="post-url">
+ {i18n.t('url')}
+ </label>
+ <div class="col-sm-10">
+ <input
+ type="url"
+ id="post-url"
+ class="form-control"
+ value={this.state.postForm.url}
+ onInput={linkEvent(this, this.handlePostUrlChange)}
+ onPaste={linkEvent(this, this.handleImageUploadPaste)}
+ />
+ {this.state.suggestedTitle && (
+ <div
+ class="mt-1 text-muted small font-weight-bold pointer"
+ onClick={linkEvent(this, this.copySuggestedTitle)}
+ >
+ {i18n.t('copy_suggested_title', {
+ title: this.state.suggestedTitle,
+ })}
+ </div>
+ )}
+ <form>
+ <label
+ htmlFor="file-upload"
+ className={`${
+ UserService.Instance.user && 'pointer'
+ } d-inline-block float-right text-muted font-weight-bold`}
+ data-tippy-content={i18n.t('upload_image')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-image"></use>
+ </svg>
+ </label>
+ <input
+ id="file-upload"
+ type="file"
+ accept="image/*,video/*"
+ name="file"
+ class="d-none"
+ disabled={!UserService.Instance.user}
+ onChange={linkEvent(this, this.handleImageUpload)}
+ />
+ </form>
+ {validURL(this.state.postForm.url) && (
+ <a
+ href={`${archiveUrl}/?run=1&url=${encodeURIComponent(
+ this.state.postForm.url
+ )}`}
+ target="_blank"
+ class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
+ rel="noopener"
+ >
+ {i18n.t('archive_link')}
+ </a>
+ )}
+ {this.state.imageLoading && (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ )}
+ {isImage(this.state.postForm.url) && (
+ <img src={this.state.postForm.url} class="img-fluid" />
+ )}
+ {this.state.crossPosts.length > 0 && (
+ <>
+ <div class="my-1 text-muted small font-weight-bold">
+ {i18n.t('cross_posts')}
+ </div>
+ <PostListings
+ showCommunity
+ posts={this.state.crossPosts}
+ enableDownvotes={this.props.enableDownvotes}
+ enableNsfw={this.props.enableNsfw}
+ />
+ </>
+ )}
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label" htmlFor="post-title">
+ {i18n.t('title')}
+ </label>
+ <div class="col-sm-10">
+ <textarea
+ value={this.state.postForm.name}
+ id="post-title"
+ onInput={linkEvent(this, this.handlePostNameChange)}
+ class={`form-control ${
+ !validTitle(this.state.postForm.name) && 'is-invalid'
+ }`}
+ required
+ rows={2}
+ minLength={3}
+ maxLength={MAX_POST_TITLE_LENGTH}
+ />
+ {!validTitle(this.state.postForm.name) && (
+ <div class="invalid-feedback">
+ {i18n.t('invalid_post_title')}
+ </div>
+ )}
+ {this.state.suggestedPosts.length > 0 && (
+ <>
+ <div class="my-1 text-muted small font-weight-bold">
+ {i18n.t('related_posts')}
+ </div>
+ <PostListings
+ posts={this.state.suggestedPosts}
+ enableDownvotes={this.props.enableDownvotes}
+ enableNsfw={this.props.enableNsfw}
+ />
+ </>
+ )}
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label" htmlFor={this.id}>
+ {i18n.t('body')}
+ </label>
+ <div class="col-sm-10">
+ <MarkdownTextArea
+ initialContent={this.state.postForm.body}
+ onContentChange={this.handlePostBodyChange}
+ />
+ </div>
+ </div>
+ {!this.props.post && (
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label" htmlFor="post-community">
+ {i18n.t('community')}
+ </label>
+ <div class="col-sm-10">
+ <select
+ class="form-control"
+ id="post-community"
+ value={this.state.postForm.community_id}
+ onInput={linkEvent(this, this.handlePostCommunityChange)}
+ >
+ <option>{i18n.t('select_a_community')}</option>
+ {this.state.communities.map(community => (
+ <option value={community.id}>
+ {community.local
+ ? community.name
+ : `${hostname(community.actor_id)}/${community.name}`}
+ </option>
+ ))}
+ </select>
+ </div>
+ </div>
+ )}
+ {this.props.enableNsfw && (
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="post-nsfw"
+ type="checkbox"
+ checked={this.state.postForm.nsfw}
+ onChange={linkEvent(this, this.handlePostNsfwChange)}
+ />
+ <label class="form-check-label" htmlFor="post-nsfw">
+ {i18n.t('nsfw')}
+ </label>
+ </div>
+ </div>
+ </div>
+ )}
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <button
+ disabled={
+ !this.state.postForm.community_id || this.state.loading
+ }
+ type="submit"
+ class="btn btn-secondary mr-2"
+ >
+ {this.state.loading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : this.props.post ? (
+ capitalizeFirstLetter(i18n.t('save'))
+ ) : (
+ capitalizeFirstLetter(i18n.t('create'))
+ )}
+ </button>
+ {this.props.post && (
+ <button
+ type="button"
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.handleCancel)}
+ >
+ {i18n.t('cancel')}
+ </button>
+ )}
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+ }
+
+ handlePostSubmit(i: PostForm, event: any) {
+ event.preventDefault();
+
+ // Coerce empty url string to undefined
+ if (i.state.postForm.url && i.state.postForm.url === '') {
+ i.state.postForm.url = undefined;
+ }
+
+ if (i.props.post) {
+ WebSocketService.Instance.editPost(i.state.postForm);
+ } else {
+ WebSocketService.Instance.createPost(i.state.postForm);
+ }
+ i.state.loading = true;
+ i.setState(i.state);
+ }
+
+ copySuggestedTitle(i: PostForm) {
+ i.state.postForm.name = i.state.suggestedTitle.substring(
+ 0,
+ MAX_POST_TITLE_LENGTH
+ );
+ i.state.suggestedTitle = undefined;
+ i.setState(i.state);
+ }
+
+ handlePostUrlChange(i: PostForm, event: any) {
+ i.state.postForm.url = event.target.value;
+ i.setState(i.state);
+ i.fetchPageTitle();
+ }
+
+ fetchPageTitle() {
+ if (validURL(this.state.postForm.url)) {
+ let form: SearchForm = {
+ q: this.state.postForm.url,
+ type_: SearchType.Url,
+ sort: SortType.TopAll,
+ page: 1,
+ limit: 6,
+ };
+
+ WebSocketService.Instance.search(form);
+
+ // Fetch the page title
+ getPageTitle(this.state.postForm.url).then(d => {
+ this.state.suggestedTitle = d;
+ this.setState(this.state);
+ });
+ } else {
+ this.state.suggestedTitle = undefined;
+ this.state.crossPosts = [];
+ }
+ }
+
+ handlePostNameChange(i: PostForm, event: any) {
+ i.state.postForm.name = event.target.value;
+ i.setState(i.state);
+ i.fetchSimilarPosts();
+ }
+
+ fetchSimilarPosts() {
+ let form: SearchForm = {
+ q: this.state.postForm.name,
+ type_: SearchType.Posts,
+ sort: SortType.TopAll,
+ community_id: this.state.postForm.community_id,
+ page: 1,
+ limit: 6,
+ };
+
+ if (this.state.postForm.name !== '') {
+ WebSocketService.Instance.search(form);
+ } else {
+ this.state.suggestedPosts = [];
+ }
+
+ this.setState(this.state);
+ }
+
+ handlePostBodyChange(val: string) {
+ this.state.postForm.body = val;
+ this.setState(this.state);
+ }
+
+ handlePostCommunityChange(i: PostForm, event: any) {
+ i.state.postForm.community_id = Number(event.target.value);
+ i.setState(i.state);
+ }
+
+ handlePostNsfwChange(i: PostForm, event: any) {
+ i.state.postForm.nsfw = event.target.checked;
+ i.setState(i.state);
+ }
+
+ handleCancel(i: PostForm) {
+ i.props.onCancel();
+ }
+
+ handlePreviewToggle(i: PostForm, event: any) {
+ event.preventDefault();
+ i.state.previewMode = !i.state.previewMode;
+ i.setState(i.state);
+ }
+
+ handleImageUploadPaste(i: PostForm, event: any) {
+ let image = event.clipboardData.files[0];
+ if (image) {
+ i.handleImageUpload(i, image);
+ }
+ }
+
+ handleImageUpload(i: PostForm, event: any) {
+ let file: any;
+ if (event.target) {
+ event.preventDefault();
+ file = event.target.files[0];
+ } else {
+ file = event;
+ }
+
+ const imageUploadUrl = `/pictrs/image`;
+ const formData = new FormData();
+ formData.append('images[]', file);
+
+ i.state.imageLoading = true;
+ i.setState(i.state);
+
+ fetch(imageUploadUrl, {
+ method: 'POST',
+ body: formData,
+ })
+ .then(res => res.json())
+ .then(res => {
+ console.log('pictrs upload:');
+ console.log(res);
+ if (res.msg == 'ok') {
+ let hash = res.files[0].file;
+ let url = `${window.location.origin}/pictrs/image/${hash}`;
+ let deleteToken = res.files[0].delete_token;
+ let deleteUrl = `${window.location.origin}/pictrs/image/delete/${deleteToken}/${hash}`;
+ i.state.postForm.url = url;
+ i.state.imageLoading = false;
+ i.setState(i.state);
+ pictrsDeleteToast(
+ i18n.t('click_to_delete_picture'),
+ i18n.t('picture_deleted'),
+ deleteUrl
+ );
+ } else {
+ i.state.imageLoading = false;
+ i.setState(i.state);
+ toast(JSON.stringify(res), 'danger');
+ }
+ })
+ .catch(error => {
+ i.state.imageLoading = false;
+ i.setState(i.state);
+ toast(error, 'danger');
+ });
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ this.state.loading = false;
+ this.setState(this.state);
+ return;
+ } else if (res.op == UserOperation.ListCommunities) {
+ let data = res.data as ListCommunitiesResponse;
+ this.state.communities = data.communities;
+ if (this.props.post) {
+ this.state.postForm.community_id = this.props.post.community_id;
+ } else if (this.props.params && this.props.params.community) {
+ let foundCommunityId = data.communities.find(
+ r => r.name == this.props.params.community
+ ).id;
+ this.state.postForm.community_id = foundCommunityId;
+ } else {
+ // By default, the null valued 'Select a Community'
+ }
+ this.setState(this.state);
+
+ // Set up select searching
+ let selectId: any = document.getElementById('post-community');
+ if (selectId) {
+ // TODO
+ /* this.choices = new Choices(selectId, { */
+ /* shouldSort: false, */
+ /* classNames: { */
+ /* containerOuter: 'choices', */
+ /* containerInner: 'choices__inner bg-secondary border-0', */
+ /* input: 'form-control', */
+ /* inputCloned: 'choices__input--cloned', */
+ /* list: 'choices__list', */
+ /* listItems: 'choices__list--multiple', */
+ /* listSingle: 'choices__list--single', */
+ /* listDropdown: 'choices__list--dropdown', */
+ /* item: 'choices__item bg-secondary', */
+ /* itemSelectable: 'choices__item--selectable', */
+ /* itemDisabled: 'choices__item--disabled', */
+ /* itemChoice: 'choices__item--choice', */
+ /* placeholder: 'choices__placeholder', */
+ /* group: 'choices__group', */
+ /* groupHeading: 'choices__heading', */
+ /* button: 'choices__button', */
+ /* activeState: 'is-active', */
+ /* focusState: 'is-focused', */
+ /* openState: 'is-open', */
+ /* disabledState: 'is-disabled', */
+ /* highlightedState: 'text-info', */
+ /* selectedState: 'text-info', */
+ /* flippedState: 'is-flipped', */
+ /* loadingState: 'is-loading', */
+ /* noResults: 'has-no-results', */
+ /* noChoices: 'has-no-choices', */
+ /* }, */
+ /* }); */
+ this.choices.passedElement.element.addEventListener(
+ 'choice',
+ (e: any) => {
+ this.state.postForm.community_id = Number(e.detail.choice.value);
+ this.setState(this.state);
+ },
+ false
+ );
+ }
+ } else if (res.op == UserOperation.CreatePost) {
+ let data = res.data as PostResponse;
+ if (data.post.creator_id == UserService.Instance.user.id) {
+ this.state.loading = false;
+ this.props.onCreate(data.post.id);
+ }
+ } else if (res.op == UserOperation.EditPost) {
+ let data = res.data as PostResponse;
+ if (data.post.creator_id == UserService.Instance.user.id) {
+ this.state.loading = false;
+ this.props.onEdit(data.post);
+ }
+ } else if (res.op == UserOperation.Search) {
+ let data = res.data as SearchResponse;
+
+ if (data.type_ == SearchType[SearchType.Posts]) {
+ this.state.suggestedPosts = data.posts;
+ } else if (data.type_ == SearchType[SearchType.Url]) {
+ this.state.crossPosts = data.posts;
+ }
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import { WebSocketService, UserService } from '../services';
+import {
+ Post,
+ CreatePostLikeForm,
+ DeletePostForm,
+ RemovePostForm,
+ LockPostForm,
+ StickyPostForm,
+ SavePostForm,
+ CommunityUser,
+ UserView,
+ BanFromCommunityForm,
+ BanUserForm,
+ AddModToCommunityForm,
+ AddAdminForm,
+ TransferSiteForm,
+ TransferCommunityForm,
+} from 'lemmy-js-client';
+import { BanType } from '../interfaces';
+import { MomentTime } from './moment-time';
+import { PostForm } from './post-form';
+import { IFramelyCard } from './iframely-card';
+import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
+import {
+ md,
+ mdToHtml,
+ canMod,
+ isMod,
+ isImage,
+ isVideo,
+ getUnixTime,
+ pictrsImage,
+ setupTippy,
+ hostname,
+ previewLines,
+} from '../utils';
+import { i18n } from '../i18next';
+
+interface PostListingState {
+ showEdit: boolean;
+ showRemoveDialog: boolean;
+ removeReason: string;
+ showBanDialog: boolean;
+ removeData: boolean;
+ banReason: string;
+ banExpires: string;
+ banType: BanType;
+ showConfirmTransferSite: boolean;
+ showConfirmTransferCommunity: boolean;
+ imageExpanded: boolean;
+ viewSource: boolean;
+ showAdvanced: boolean;
+ my_vote: number;
+ score: number;
+ upvotes: number;
+ downvotes: number;
+}
+
+interface PostListingProps {
+ post: Post;
+ showCommunity?: boolean;
+ showBody?: boolean;
+ moderators?: CommunityUser[];
+ admins?: UserView[];
+ enableDownvotes: boolean;
+ enableNsfw: boolean;
+}
+
+export class PostListing extends Component<PostListingProps, PostListingState> {
+ private emptyState: PostListingState = {
+ showEdit: false,
+ showRemoveDialog: false,
+ removeReason: null,
+ showBanDialog: false,
+ removeData: null,
+ banReason: null,
+ banExpires: null,
+ banType: BanType.Community,
+ showConfirmTransferSite: false,
+ showConfirmTransferCommunity: false,
+ imageExpanded: false,
+ viewSource: false,
+ showAdvanced: false,
+ my_vote: this.props.post.my_vote,
+ score: this.props.post.score,
+ upvotes: this.props.post.upvotes,
+ downvotes: this.props.post.downvotes,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handlePostLike = this.handlePostLike.bind(this);
+ this.handlePostDisLike = this.handlePostDisLike.bind(this);
+ this.handleEditPost = this.handleEditPost.bind(this);
+ this.handleEditCancel = this.handleEditCancel.bind(this);
+ }
+
+ componentWillReceiveProps(nextProps: PostListingProps) {
+ this.state.my_vote = nextProps.post.my_vote;
+ this.state.upvotes = nextProps.post.upvotes;
+ this.state.downvotes = nextProps.post.downvotes;
+ this.state.score = nextProps.post.score;
+ if (this.props.post.id !== nextProps.post.id) {
+ this.state.imageExpanded = false;
+ }
+ this.setState(this.state);
+ }
+
+ render() {
+ return (
+ <div class="">
+ {!this.state.showEdit ? (
+ <>
+ {this.listing()}
+ {this.body()}
+ </>
+ ) : (
+ <div class="col-12">
+ <PostForm
+ post={this.props.post}
+ onEdit={this.handleEditPost}
+ onCancel={this.handleEditCancel}
+ enableNsfw={this.props.enableNsfw}
+ enableDownvotes={this.props.enableDownvotes}
+ />
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ body() {
+ return (
+ <div class="row">
+ <div class="col-12">
+ {this.props.post.url &&
+ this.props.showBody &&
+ this.props.post.embed_title && (
+ <IFramelyCard post={this.props.post} />
+ )}
+ {this.props.showBody && this.props.post.body && (
+ <>
+ {this.state.viewSource ? (
+ <pre>{this.props.post.body}</pre>
+ ) : (
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(this.props.post.body)}
+ />
+ )}
+ </>
+ )}
+ </div>
+ </div>
+ );
+ }
+
+ imgThumb(src: string) {
+ let post = this.props.post;
+ return (
+ <img
+ className={`img-fluid thumbnail rounded ${
+ post.nsfw || post.community_nsfw ? 'img-blur' : ''
+ }`}
+ src={src}
+ />
+ );
+ }
+
+ getImage(thumbnail: boolean = false) {
+ let post = this.props.post;
+ if (isImage(post.url)) {
+ if (post.url.includes('pictrs')) {
+ return pictrsImage(post.url, thumbnail);
+ } else if (post.thumbnail_url) {
+ return pictrsImage(post.thumbnail_url, thumbnail);
+ } else {
+ return post.url;
+ }
+ } else if (post.thumbnail_url) {
+ return pictrsImage(post.thumbnail_url, thumbnail);
+ }
+ }
+
+ thumbnail() {
+ let post = this.props.post;
+
+ if (isImage(post.url)) {
+ return (
+ <div
+ class="float-right text-body pointer d-inline-block position-relative"
+ data-tippy-content={i18n.t('expand_here')}
+ onClick={linkEvent(this, this.handleImageExpandClick)}
+ >
+ {this.imgThumb(this.getImage(true))}
+ <svg class="icon mini-overlay">
+ <use xlinkHref="#icon-image"></use>
+ </svg>
+ </div>
+ );
+ } else if (post.thumbnail_url) {
+ return (
+ <a
+ class="float-right text-body d-inline-block position-relative"
+ href={post.url}
+ target="_blank"
+ rel="noopener"
+ title={post.url}
+ >
+ {this.imgThumb(this.getImage(true))}
+ <svg class="icon mini-overlay">
+ <use xlinkHref="#icon-external-link"></use>
+ </svg>
+ </a>
+ );
+ } else if (post.url) {
+ if (isVideo(post.url)) {
+ return (
+ <div class="embed-responsive embed-responsive-16by9">
+ <video
+ playsinline
+ muted
+ loop
+ controls
+ class="embed-responsive-item"
+ >
+ <source src={post.url} type="video/mp4" />
+ </video>
+ </div>
+ );
+ } else {
+ return (
+ <a
+ className="text-body"
+ href={post.url}
+ target="_blank"
+ title={post.url}
+ rel="noopener"
+ >
+ <div class="thumbnail rounded bg-light d-flex justify-content-center">
+ <svg class="icon d-flex align-items-center">
+ <use xlinkHref="#icon-external-link"></use>
+ </svg>
+ </div>
+ </a>
+ );
+ }
+ } else {
+ return (
+ <Link
+ className="text-body"
+ to={`/post/${post.id}`}
+ title={i18n.t('comments')}
+ >
+ <div class="thumbnail rounded bg-light d-flex justify-content-center">
+ <svg class="icon d-flex align-items-center">
+ <use xlinkHref="#icon-message-square"></use>
+ </svg>
+ </div>
+ </Link>
+ );
+ }
+ }
+
+ createdLine() {
+ let post = this.props.post;
+ return (
+ <ul class="list-inline mb-1 text-muted small">
+ <li className="list-inline-item">
+ <UserListing
+ user={{
+ name: post.creator_name,
+ preferred_username: post.creator_preferred_username,
+ avatar: post.creator_avatar,
+ id: post.creator_id,
+ local: post.creator_local,
+ actor_id: post.creator_actor_id,
+ published: post.creator_published,
+ }}
+ />
+
+ {this.isMod && (
+ <span className="mx-1 badge badge-light">{i18n.t('mod')}</span>
+ )}
+ {this.isAdmin && (
+ <span className="mx-1 badge badge-light">{i18n.t('admin')}</span>
+ )}
+ {(post.banned_from_community || post.banned) && (
+ <span className="mx-1 badge badge-danger">{i18n.t('banned')}</span>
+ )}
+ {this.props.showCommunity && (
+ <span>
+ <span class="mx-1"> {i18n.t('to')} </span>
+ <CommunityLink
+ community={{
+ name: post.community_name,
+ id: post.community_id,
+ local: post.community_local,
+ actor_id: post.community_actor_id,
+ icon: post.community_icon,
+ }}
+ />
+ </span>
+ )}
+ </li>
+ <li className="list-inline-item">•</li>
+ {post.url && !(hostname(post.url) == window.location.hostname) && (
+ <>
+ <li className="list-inline-item">
+ <a
+ className="text-muted font-italic"
+ href={post.url}
+ target="_blank"
+ title={post.url}
+ rel="noopener"
+ >
+ {hostname(post.url)}
+ </a>
+ </li>
+ <li className="list-inline-item">•</li>
+ </>
+ )}
+ <li className="list-inline-item">
+ <span>
+ <MomentTime data={post} />
+ </span>
+ </li>
+ {post.body && (
+ <>
+ <li className="list-inline-item">•</li>
+ <li className="list-inline-item">
+ {/* Using a link with tippy doesn't work on touch devices unfortunately */}
+ <Link
+ className="text-muted"
+ data-tippy-content={md.render(previewLines(post.body))}
+ data-tippy-allowHtml={true}
+ to={`/post/${post.id}`}
+ >
+ <svg class="mr-1 icon icon-inline">
+ <use xlinkHref="#icon-book-open"></use>
+ </svg>
+ </Link>
+ </li>
+ </>
+ )}
+ </ul>
+ );
+ }
+
+ voteBar() {
+ return (
+ <div className={`vote-bar col-1 pr-0 small text-center`}>
+ <button
+ className={`btn-animate btn btn-link p-0 ${
+ this.state.my_vote == 1 ? 'text-info' : 'text-muted'
+ }`}
+ onClick={linkEvent(this, this.handlePostLike)}
+ data-tippy-content={i18n.t('upvote')}
+ >
+ <svg class="icon upvote">
+ <use xlinkHref="#icon-arrow-up1"></use>
+ </svg>
+ </button>
+ <div
+ class={`unselectable pointer font-weight-bold text-muted px-1`}
+ data-tippy-content={this.pointsTippy}
+ >
+ {this.state.score}
+ </div>
+ {this.props.enableDownvotes && (
+ <button
+ className={`btn-animate btn btn-link p-0 ${
+ this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
+ }`}
+ onClick={linkEvent(this, this.handlePostDisLike)}
+ data-tippy-content={i18n.t('downvote')}
+ >
+ <svg class="icon downvote">
+ <use xlinkHref="#icon-arrow-down1"></use>
+ </svg>
+ </button>
+ )}
+ </div>
+ );
+ }
+
+ postTitleLine() {
+ let post = this.props.post;
+ return (
+ <div className="post-title overflow-hidden">
+ <h5>
+ {this.props.showBody && post.url ? (
+ <a
+ className={!post.stickied ? 'text-body' : 'text-primary'}
+ href={post.url}
+ target="_blank"
+ title={post.url}
+ rel="noopener"
+ >
+ {post.name}
+ </a>
+ ) : (
+ <Link
+ className={!post.stickied ? 'text-body' : 'text-primary'}
+ to={`/post/${post.id}`}
+ title={i18n.t('comments')}
+ >
+ {post.name}
+ </Link>
+ )}
+ {(isImage(post.url) || this.props.post.thumbnail_url) && (
+ <>
+ {!this.state.imageExpanded ? (
+ <span
+ class="text-monospace unselectable pointer ml-2 text-muted small"
+ data-tippy-content={i18n.t('expand_here')}
+ onClick={linkEvent(this, this.handleImageExpandClick)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-plus-square"></use>
+ </svg>
+ </span>
+ ) : (
+ <span>
+ <span
+ class="text-monospace unselectable pointer ml-2 text-muted small"
+ onClick={linkEvent(this, this.handleImageExpandClick)}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-minus-square"></use>
+ </svg>
+ </span>
+ <div>
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleImageExpandClick)}
+ >
+ <img
+ class="img-fluid img-expanded"
+ src={this.getImage()}
+ />
+ </span>
+ </div>
+ </span>
+ )}
+ </>
+ )}
+ {post.removed && (
+ <small className="ml-2 text-muted font-italic">
+ {i18n.t('removed')}
+ </small>
+ )}
+ {post.deleted && (
+ <small
+ className="unselectable pointer ml-2 text-muted font-italic"
+ data-tippy-content={i18n.t('deleted')}
+ >
+ <svg class={`icon icon-inline text-danger`}>
+ <use xlinkHref="#icon-trash"></use>
+ </svg>
+ </small>
+ )}
+ {post.locked && (
+ <small
+ className="unselectable pointer ml-2 text-muted font-italic"
+ data-tippy-content={i18n.t('locked')}
+ >
+ <svg class={`icon icon-inline text-danger`}>
+ <use xlinkHref="#icon-lock"></use>
+ </svg>
+ </small>
+ )}
+ {post.stickied && (
+ <small
+ className="unselectable pointer ml-2 text-muted font-italic"
+ data-tippy-content={i18n.t('stickied')}
+ >
+ <svg class={`icon icon-inline text-primary`}>
+ <use xlinkHref="#icon-pin"></use>
+ </svg>
+ </small>
+ )}
+ {post.nsfw && (
+ <small className="ml-2 text-muted font-italic">
+ {i18n.t('nsfw')}
+ </small>
+ )}
+ </h5>
+ </div>
+ );
+ }
+
+ commentsLine(showVotes: boolean = false) {
+ let post = this.props.post;
+ return (
+ <ul class="d-flex align-items-center list-inline mb-1 text-muted small">
+ <li className="list-inline-item">
+ <Link
+ className="text-muted"
+ title={i18n.t('number_of_comments', {
+ count: post.number_of_comments,
+ })}
+ to={`/post/${post.id}`}
+ >
+ <svg class="mr-1 icon icon-inline">
+ <use xlinkHref="#icon-message-square"></use>
+ </svg>
+ {i18n.t('number_of_comments', {
+ count: post.number_of_comments,
+ })}
+ </Link>
+ </li>
+ {(showVotes || this.state.upvotes !== this.state.score) && (
+ <>
+ <span
+ class="unselectable pointer ml-3"
+ data-tippy-content={this.pointsTippy}
+ >
+ <li className="list-inline-item">
+ <a
+ className={`btn-animate btn btn-link p-0 ${
+ this.state.my_vote == 1 ? 'text-info' : 'text-muted'
+ }`}
+ onClick={linkEvent(this, this.handlePostLike)}
+ >
+ <svg class="small icon icon-inline mx-1">
+ <use xlinkHref="#icon-arrow-up1"></use>
+ </svg>
+ {this.state.upvotes}
+ </a>
+ </li>
+ <li className="list-inline-item">
+ <a
+ className={`btn-animate btn btn-link p-0 ${
+ this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
+ }`}
+ onClick={linkEvent(this, this.handlePostDisLike)}
+ >
+ <svg class="small icon icon-inline mx-1">
+ <use xlinkHref="#icon-arrow-down1"></use>
+ </svg>
+ {this.state.downvotes}
+ </a>
+ </li>
+ </span>
+ </>
+ )}
+ </ul>
+ );
+ }
+
+ duplicatesLine() {
+ return (
+ this.props.post.duplicates && (
+ <ul class="list-inline mb-1 small text-muted">
+ <>
+ <li className="list-inline-item mr-2">
+ {i18n.t('cross_posted_to')}
+ </li>
+ {this.props.post.duplicates.map(post => (
+ <li className="list-inline-item mr-2">
+ <Link to={`/post/${post.id}`}>{post.community_name}</Link>
+ </li>
+ ))}
+ </>
+ </ul>
+ )
+ );
+ }
+
+ postActions() {
+ let post = this.props.post;
+ return (
+ <ul class="list-inline mb-1 text-muted font-weight-bold">
+ {UserService.Instance.user && (
+ <>
+ {this.props.showBody && (
+ <>
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleSavePostClick)}
+ data-tippy-content={
+ post.saved ? i18n.t('unsave') : i18n.t('save')
+ }
+ >
+ <svg
+ class={`icon icon-inline ${post.saved && 'text-warning'}`}
+ >
+ <use xlinkHref="#icon-star"></use>
+ </svg>
+ </button>
+ </li>
+ <li className="list-inline-item">
+ <Link
+ class="btn btn-link btn-animate text-muted"
+ to={`/create_post${this.crossPostParams}`}
+ title={i18n.t('cross_post')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-copy"></use>
+ </svg>
+ </Link>
+ </li>
+ </>
+ )}
+ {this.myPost && this.props.showBody && (
+ <>
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleEditClick)}
+ data-tippy-content={i18n.t('edit')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-edit"></use>
+ </svg>
+ </button>
+ </li>
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleDeleteClick)}
+ data-tippy-content={
+ !post.deleted ? i18n.t('delete') : i18n.t('restore')
+ }
+ >
+ <svg
+ class={`icon icon-inline ${
+ post.deleted && 'text-danger'
+ }`}
+ >
+ <use xlinkHref="#icon-trash"></use>
+ </svg>
+ </button>
+ </li>
+ </>
+ )}
+
+ {!this.state.showAdvanced && this.props.showBody ? (
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleShowAdvanced)}
+ data-tippy-content={i18n.t('more')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-more-vertical"></use>
+ </svg>
+ </button>
+ </li>
+ ) : (
+ <>
+ {this.props.showBody && post.body && (
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleViewSource)}
+ data-tippy-content={i18n.t('view_source')}
+ >
+ <svg
+ class={`icon icon-inline ${
+ this.state.viewSource && 'text-success'
+ }`}
+ >
+ <use xlinkHref="#icon-file-text"></use>
+ </svg>
+ </button>
+ </li>
+ )}
+ {this.canModOnSelf && (
+ <>
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleModLock)}
+ data-tippy-content={
+ post.locked ? i18n.t('unlock') : i18n.t('lock')
+ }
+ >
+ <svg
+ class={`icon icon-inline ${
+ post.locked && 'text-danger'
+ }`}
+ >
+ <use xlinkHref="#icon-lock"></use>
+ </svg>
+ </button>
+ </li>
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleModSticky)}
+ data-tippy-content={
+ post.stickied ? i18n.t('unsticky') : i18n.t('sticky')
+ }
+ >
+ <svg
+ class={`icon icon-inline ${
+ post.stickied && 'text-success'
+ }`}
+ >
+ <use xlinkHref="#icon-pin"></use>
+ </svg>
+ </button>
+ </li>
+ </>
+ )}
+ {/* Mods can ban from community, and appoint as mods to community */}
+ {(this.canMod || this.canAdmin) && (
+ <li className="list-inline-item">
+ {!post.removed ? (
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleModRemoveShow)}
+ >
+ {i18n.t('remove')}
+ </span>
+ ) : (
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleModRemoveSubmit)}
+ >
+ {i18n.t('restore')}
+ </span>
+ )}
+ </li>
+ )}
+ {this.canMod && (
+ <>
+ {!this.isMod && (
+ <li className="list-inline-item">
+ {!post.banned_from_community ? (
+ <span
+ class="pointer"
+ onClick={linkEvent(
+ this,
+ this.handleModBanFromCommunityShow
+ )}
+ >
+ {i18n.t('ban')}
+ </span>
+ ) : (
+ <span
+ class="pointer"
+ onClick={linkEvent(
+ this,
+ this.handleModBanFromCommunitySubmit
+ )}
+ >
+ {i18n.t('unban')}
+ </span>
+ )}
+ </li>
+ )}
+ {!post.banned_from_community && post.creator_local && (
+ <li className="list-inline-item">
+ <span
+ class="pointer"
+ onClick={linkEvent(
+ this,
+ this.handleAddModToCommunity
+ )}
+ >
+ {this.isMod
+ ? i18n.t('remove_as_mod')
+ : i18n.t('appoint_as_mod')}
+ </span>
+ </li>
+ )}
+ </>
+ )}
+ {/* Community creators and admins can transfer community to another mod */}
+ {(this.amCommunityCreator || this.canAdmin) &&
+ this.isMod &&
+ post.creator_local && (
+ <li className="list-inline-item">
+ {!this.state.showConfirmTransferCommunity ? (
+ <span
+ class="pointer"
+ onClick={linkEvent(
+ this,
+ this.handleShowConfirmTransferCommunity
+ )}
+ >
+ {i18n.t('transfer_community')}
+ </span>
+ ) : (
+ <>
+ <span class="d-inline-block mr-1">
+ {i18n.t('are_you_sure')}
+ </span>
+ <span
+ class="pointer d-inline-block mr-1"
+ onClick={linkEvent(
+ this,
+ this.handleTransferCommunity
+ )}
+ >
+ {i18n.t('yes')}
+ </span>
+ <span
+ class="pointer d-inline-block"
+ onClick={linkEvent(
+ this,
+ this.handleCancelShowConfirmTransferCommunity
+ )}
+ >
+ {i18n.t('no')}
+ </span>
+ </>
+ )}
+ </li>
+ )}
+ {/* Admins can ban from all, and appoint other admins */}
+ {this.canAdmin && (
+ <>
+ {!this.isAdmin && (
+ <li className="list-inline-item">
+ {!post.banned ? (
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleModBanShow)}
+ >
+ {i18n.t('ban_from_site')}
+ </span>
+ ) : (
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleModBanSubmit)}
+ >
+ {i18n.t('unban_from_site')}
+ </span>
+ )}
+ </li>
+ )}
+ {!post.banned && post.creator_local && (
+ <li className="list-inline-item">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleAddAdmin)}
+ >
+ {this.isAdmin
+ ? i18n.t('remove_as_admin')
+ : i18n.t('appoint_as_admin')}
+ </span>
+ </li>
+ )}
+ </>
+ )}
+ {/* Site Creator can transfer to another admin */}
+ {this.amSiteCreator && this.isAdmin && (
+ <li className="list-inline-item">
+ {!this.state.showConfirmTransferSite ? (
+ <span
+ class="pointer"
+ onClick={linkEvent(
+ this,
+ this.handleShowConfirmTransferSite
+ )}
+ >
+ {i18n.t('transfer_site')}
+ </span>
+ ) : (
+ <>
+ <span class="d-inline-block mr-1">
+ {i18n.t('are_you_sure')}
+ </span>
+ <span
+ class="pointer d-inline-block mr-1"
+ onClick={linkEvent(this, this.handleTransferSite)}
+ >
+ {i18n.t('yes')}
+ </span>
+ <span
+ class="pointer d-inline-block"
+ onClick={linkEvent(
+ this,
+ this.handleCancelShowConfirmTransferSite
+ )}
+ >
+ {i18n.t('no')}
+ </span>
+ </>
+ )}
+ </li>
+ )}
+ </>
+ )}
+ </>
+ )}
+ </ul>
+ );
+ }
+
+ removeAndBanDialogs() {
+ let post = this.props.post;
+ return (
+ <>
+ {this.state.showRemoveDialog && (
+ <form
+ class="form-inline"
+ onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
+ >
+ <input
+ type="text"
+ class="form-control mr-2"
+ placeholder={i18n.t('reason')}
+ value={this.state.removeReason}
+ onInput={linkEvent(this, this.handleModRemoveReasonChange)}
+ />
+ <button type="submit" class="btn btn-secondary">
+ {i18n.t('remove_post')}
+ </button>
+ </form>
+ )}
+ {this.state.showBanDialog && (
+ <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
+ <div class="form-group row">
+ <label class="col-form-label" htmlFor="post-listing-reason">
+ {i18n.t('reason')}
+ </label>
+ <input
+ type="text"
+ id="post-listing-reason"
+ class="form-control mr-2"
+ placeholder={i18n.t('reason')}
+ value={this.state.banReason}
+ onInput={linkEvent(this, this.handleModBanReasonChange)}
+ />
+ <div class="form-group">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="mod-ban-remove-data"
+ type="checkbox"
+ checked={this.state.removeData}
+ onChange={linkEvent(this, this.handleModRemoveDataChange)}
+ />
+ <label class="form-check-label" htmlFor="mod-ban-remove-data">
+ {i18n.t('remove_posts_comments')}
+ </label>
+ </div>
+ </div>
+ </div>
+ {/* TODO hold off on expires until later */}
+ {/* <div class="form-group row"> */}
+ {/* <label class="col-form-label">Expires</label> */}
+ {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
+ {/* </div> */}
+ <div class="form-group row">
+ <button type="submit" class="btn btn-secondary">
+ {i18n.t('ban')} {post.creator_name}
+ </button>
+ </div>
+ </form>
+ )}
+ </>
+ );
+ }
+
+ mobileThumbnail() {
+ return this.props.post.thumbnail_url || isImage(this.props.post.url) ? (
+ <div class="row">
+ <div className={`${this.state.imageExpanded ? 'col-12' : 'col-8'}`}>
+ {this.postTitleLine()}
+ </div>
+ <div class="col-4">
+ {/* Post body prev or thumbnail */}
+ {!this.state.imageExpanded && this.thumbnail()}
+ </div>
+ </div>
+ ) : (
+ this.postTitleLine()
+ );
+ }
+
+ showMobilePreview() {
+ return (
+ this.props.post.body &&
+ !this.props.showBody && (
+ <div
+ className="md-div mb-1"
+ dangerouslySetInnerHTML={{
+ __html: md.render(previewLines(this.props.post.body)),
+ }}
+ />
+ )
+ );
+ }
+
+ listing() {
+ return (
+ <>
+ {/* The mobile view*/}
+ <div class="d-block d-sm-none">
+ <div class="row">
+ <div class="col-12">
+ {this.createdLine()}
+
+ {/* If it has a thumbnail, do a right aligned thumbnail */}
+ {this.mobileThumbnail()}
+
+ {/* Show a preview of the post body */}
+ {this.showMobilePreview()}
+
+ {this.commentsLine(true)}
+ {this.duplicatesLine()}
+ {this.postActions()}
+ {this.removeAndBanDialogs()}
+ </div>
+ </div>
+ </div>
+
+ {/* The larger view*/}
+ <div class="d-none d-sm-block">
+ <div class="row">
+ {this.voteBar()}
+ {!this.state.imageExpanded && (
+ <div class="col-sm-2 pr-0">
+ <div class="">{this.thumbnail()}</div>
+ </div>
+ )}
+ <div
+ class={`${
+ this.state.imageExpanded ? 'col-12' : 'col-12 col-sm-9'
+ }`}
+ >
+ <div class="row">
+ <div className="col-12">
+ {this.postTitleLine()}
+ {this.createdLine()}
+ {this.commentsLine()}
+ {this.duplicatesLine()}
+ {this.postActions()}
+ {this.removeAndBanDialogs()}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </>
+ );
+ }
+
+ private get myPost(): boolean {
+ return (
+ UserService.Instance.user &&
+ this.props.post.creator_id == UserService.Instance.user.id
+ );
+ }
+
+ get isMod(): boolean {
+ return (
+ this.props.moderators &&
+ isMod(
+ this.props.moderators.map(m => m.user_id),
+ this.props.post.creator_id
+ )
+ );
+ }
+
+ get isAdmin(): boolean {
+ return (
+ this.props.admins &&
+ isMod(
+ this.props.admins.map(a => a.id),
+ this.props.post.creator_id
+ )
+ );
+ }
+
+ get canMod(): boolean {
+ if (this.props.admins && this.props.moderators) {
+ let adminsThenMods = this.props.admins
+ .map(a => a.id)
+ .concat(this.props.moderators.map(m => m.user_id));
+
+ return canMod(
+ UserService.Instance.user,
+ adminsThenMods,
+ this.props.post.creator_id
+ );
+ } else {
+ return false;
+ }
+ }
+
+ get canModOnSelf(): boolean {
+ if (this.props.admins && this.props.moderators) {
+ let adminsThenMods = this.props.admins
+ .map(a => a.id)
+ .concat(this.props.moderators.map(m => m.user_id));
+
+ return canMod(
+ UserService.Instance.user,
+ adminsThenMods,
+ this.props.post.creator_id,
+ true
+ );
+ } else {
+ return false;
+ }
+ }
+
+ get canAdmin(): boolean {
+ return (
+ this.props.admins &&
+ canMod(
+ UserService.Instance.user,
+ this.props.admins.map(a => a.id),
+ this.props.post.creator_id
+ )
+ );
+ }
+
+ get amCommunityCreator(): boolean {
+ return (
+ this.props.moderators &&
+ UserService.Instance.user &&
+ this.props.post.creator_id != UserService.Instance.user.id &&
+ UserService.Instance.user.id == this.props.moderators[0].user_id
+ );
+ }
+
+ get amSiteCreator(): boolean {
+ return (
+ this.props.admins &&
+ UserService.Instance.user &&
+ this.props.post.creator_id != UserService.Instance.user.id &&
+ UserService.Instance.user.id == this.props.admins[0].id
+ );
+ }
+
+ handlePostLike(i: PostListing) {
+ if (!UserService.Instance.user) {
+ this.context.router.history.push(`/login`);
+ }
+
+ let new_vote = i.state.my_vote == 1 ? 0 : 1;
+
+ if (i.state.my_vote == 1) {
+ i.state.score--;
+ i.state.upvotes--;
+ } else if (i.state.my_vote == -1) {
+ i.state.downvotes--;
+ i.state.upvotes++;
+ i.state.score += 2;
+ } else {
+ i.state.upvotes++;
+ i.state.score++;
+ }
+
+ i.state.my_vote = new_vote;
+
+ let form: CreatePostLikeForm = {
+ post_id: i.props.post.id,
+ score: i.state.my_vote,
+ };
+
+ WebSocketService.Instance.likePost(form);
+ i.setState(i.state);
+ setupTippy();
+ }
+
+ handlePostDisLike(i: PostListing) {
+ if (!UserService.Instance.user) {
+ this.context.router.history.push(`/login`);
+ }
+
+ let new_vote = i.state.my_vote == -1 ? 0 : -1;
+
+ if (i.state.my_vote == 1) {
+ i.state.score -= 2;
+ i.state.upvotes--;
+ i.state.downvotes++;
+ } else if (i.state.my_vote == -1) {
+ i.state.downvotes--;
+ i.state.score++;
+ } else {
+ i.state.downvotes++;
+ i.state.score--;
+ }
+
+ i.state.my_vote = new_vote;
+
+ let form: CreatePostLikeForm = {
+ post_id: i.props.post.id,
+ score: i.state.my_vote,
+ };
+
+ WebSocketService.Instance.likePost(form);
+ i.setState(i.state);
+ setupTippy();
+ }
+
+ handleEditClick(i: PostListing) {
+ i.state.showEdit = true;
+ i.setState(i.state);
+ }
+
+ handleEditCancel() {
+ this.state.showEdit = false;
+ this.setState(this.state);
+ }
+
+ // The actual editing is done in the recieve for post
+ handleEditPost() {
+ this.state.showEdit = false;
+ this.setState(this.state);
+ }
+
+ handleDeleteClick(i: PostListing) {
+ let deleteForm: DeletePostForm = {
+ edit_id: i.props.post.id,
+ deleted: !i.props.post.deleted,
+ auth: null,
+ };
+ WebSocketService.Instance.deletePost(deleteForm);
+ }
+
+ handleSavePostClick(i: PostListing) {
+ let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
+ let form: SavePostForm = {
+ post_id: i.props.post.id,
+ save: saved,
+ };
+
+ WebSocketService.Instance.savePost(form);
+ }
+
+ get crossPostParams(): string {
+ let params = `?title=${this.props.post.name}`;
+ let post = this.props.post;
+
+ if (post.url) {
+ params += `&url=${encodeURIComponent(post.url)}`;
+ }
+ if (this.props.post.body) {
+ params += `&body=${this.props.post.body}`;
+ }
+ return params;
+ }
+
+ handleModRemoveShow(i: PostListing) {
+ i.state.showRemoveDialog = true;
+ i.setState(i.state);
+ }
+
+ handleModRemoveReasonChange(i: PostListing, event: any) {
+ i.state.removeReason = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleModRemoveDataChange(i: PostListing, event: any) {
+ i.state.removeData = event.target.checked;
+ i.setState(i.state);
+ }
+
+ handleModRemoveSubmit(i: PostListing) {
+ event.preventDefault();
+ let form: RemovePostForm = {
+ edit_id: i.props.post.id,
+ removed: !i.props.post.removed,
+ reason: i.state.removeReason,
+ auth: null,
+ };
+ WebSocketService.Instance.removePost(form);
+
+ i.state.showRemoveDialog = false;
+ i.setState(i.state);
+ }
+
+ handleModLock(i: PostListing) {
+ let form: LockPostForm = {
+ edit_id: i.props.post.id,
+ locked: !i.props.post.locked,
+ auth: null,
+ };
+ WebSocketService.Instance.lockPost(form);
+ }
+
+ handleModSticky(i: PostListing) {
+ let form: StickyPostForm = {
+ edit_id: i.props.post.id,
+ stickied: !i.props.post.stickied,
+ auth: null,
+ };
+ WebSocketService.Instance.stickyPost(form);
+ }
+
+ handleModBanFromCommunityShow(i: PostListing) {
+ i.state.showBanDialog = true;
+ i.state.banType = BanType.Community;
+ i.setState(i.state);
+ }
+
+ handleModBanShow(i: PostListing) {
+ i.state.showBanDialog = true;
+ i.state.banType = BanType.Site;
+ i.setState(i.state);
+ }
+
+ handleModBanReasonChange(i: PostListing, event: any) {
+ i.state.banReason = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleModBanExpiresChange(i: PostListing, event: any) {
+ i.state.banExpires = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleModBanFromCommunitySubmit(i: PostListing) {
+ i.state.banType = BanType.Community;
+ i.setState(i.state);
+ i.handleModBanBothSubmit(i);
+ }
+
+ handleModBanSubmit(i: PostListing) {
+ i.state.banType = BanType.Site;
+ i.setState(i.state);
+ i.handleModBanBothSubmit(i);
+ }
+
+ handleModBanBothSubmit(i: PostListing) {
+ event.preventDefault();
+
+ if (i.state.banType == BanType.Community) {
+ // If its an unban, restore all their data
+ let ban = !i.props.post.banned_from_community;
+ if (ban == false) {
+ i.state.removeData = false;
+ }
+ let form: BanFromCommunityForm = {
+ user_id: i.props.post.creator_id,
+ community_id: i.props.post.community_id,
+ ban,
+ remove_data: i.state.removeData,
+ reason: i.state.banReason,
+ expires: getUnixTime(i.state.banExpires),
+ };
+ WebSocketService.Instance.banFromCommunity(form);
+ } else {
+ // If its an unban, restore all their data
+ let ban = !i.props.post.banned;
+ if (ban == false) {
+ i.state.removeData = false;
+ }
+ let form: BanUserForm = {
+ user_id: i.props.post.creator_id,
+ ban,
+ remove_data: i.state.removeData,
+ reason: i.state.banReason,
+ expires: getUnixTime(i.state.banExpires),
+ };
+ WebSocketService.Instance.banUser(form);
+ }
+
+ i.state.showBanDialog = false;
+ i.setState(i.state);
+ }
+
+ handleAddModToCommunity(i: PostListing) {
+ let form: AddModToCommunityForm = {
+ user_id: i.props.post.creator_id,
+ community_id: i.props.post.community_id,
+ added: !i.isMod,
+ };
+ WebSocketService.Instance.addModToCommunity(form);
+ i.setState(i.state);
+ }
+
+ handleAddAdmin(i: PostListing) {
+ let form: AddAdminForm = {
+ user_id: i.props.post.creator_id,
+ added: !i.isAdmin,
+ };
+ WebSocketService.Instance.addAdmin(form);
+ i.setState(i.state);
+ }
+
+ handleShowConfirmTransferCommunity(i: PostListing) {
+ i.state.showConfirmTransferCommunity = true;
+ i.setState(i.state);
+ }
+
+ handleCancelShowConfirmTransferCommunity(i: PostListing) {
+ i.state.showConfirmTransferCommunity = false;
+ i.setState(i.state);
+ }
+
+ handleTransferCommunity(i: PostListing) {
+ let form: TransferCommunityForm = {
+ community_id: i.props.post.community_id,
+ user_id: i.props.post.creator_id,
+ };
+ WebSocketService.Instance.transferCommunity(form);
+ i.state.showConfirmTransferCommunity = false;
+ i.setState(i.state);
+ }
+
+ handleShowConfirmTransferSite(i: PostListing) {
+ i.state.showConfirmTransferSite = true;
+ i.setState(i.state);
+ }
+
+ handleCancelShowConfirmTransferSite(i: PostListing) {
+ i.state.showConfirmTransferSite = false;
+ i.setState(i.state);
+ }
+
+ handleTransferSite(i: PostListing) {
+ let form: TransferSiteForm = {
+ user_id: i.props.post.creator_id,
+ };
+ WebSocketService.Instance.transferSite(form);
+ i.state.showConfirmTransferSite = false;
+ i.setState(i.state);
+ }
+
+ handleImageExpandClick(i: PostListing) {
+ i.state.imageExpanded = !i.state.imageExpanded;
+ i.setState(i.state);
+ }
+
+ handleViewSource(i: PostListing) {
+ i.state.viewSource = !i.state.viewSource;
+ i.setState(i.state);
+ }
+
+ handleShowAdvanced(i: PostListing) {
+ i.state.showAdvanced = !i.state.showAdvanced;
+ i.setState(i.state);
+ setupTippy();
+ }
+
+ get pointsTippy(): string {
+ let points = i18n.t('number_of_points', {
+ count: this.state.score,
+ });
+
+ let upvotes = i18n.t('number_of_upvotes', {
+ count: this.state.upvotes,
+ });
+
+ let downvotes = i18n.t('number_of_downvotes', {
+ count: this.state.downvotes,
+ });
+
+ return `${points} • ${upvotes} • ${downvotes}`;
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import { Link } from 'inferno-router';
+import { Post, SortType } from 'lemmy-js-client';
+import { postSort } from '../utils';
+import { PostListing } from './post-listing';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
+
+interface PostListingsProps {
+ posts: Post[];
+ showCommunity?: boolean;
+ removeDuplicates?: boolean;
+ sort?: SortType;
+ enableDownvotes: boolean;
+ enableNsfw: boolean;
+}
+
+export class PostListings extends Component<PostListingsProps, any> {
+ constructor(props: any, context: any) {
+ super(props, context);
+ }
+
+ render() {
+ return (
+ <div>
+ {this.props.posts.length > 0 ? (
+ this.outer().map(post => (
+ <>
+ <PostListing
+ post={post}
+ showCommunity={this.props.showCommunity}
+ enableDownvotes={this.props.enableDownvotes}
+ enableNsfw={this.props.enableNsfw}
+ />
+ <hr class="my-3" />
+ </>
+ ))
+ ) : (
+ <>
+ <div>{i18n.t('no_posts')}</div>
+ {this.props.showCommunity !== undefined && (
+ <T i18nKey="subscribe_to_communities">
+ #<Link to="/communities">#</Link>
+ </T>
+ )}
+ </>
+ )}
+ </div>
+ );
+ }
+
+ outer(): Post[] {
+ let out = this.props.posts;
+ if (this.props.removeDuplicates) {
+ out = this.removeDuplicates(out);
+ }
+
+ if (this.props.sort !== undefined) {
+ postSort(out, this.props.sort, this.props.showCommunity == undefined);
+ }
+
+ return out;
+ }
+
+ removeDuplicates(posts: Post[]): Post[] {
+ // A map from post url to list of posts (dupes)
+ let urlMap = new Map<string, Post[]>();
+
+ // Loop over the posts, find ones with same urls
+ for (let post of posts) {
+ if (
+ post.url &&
+ !post.deleted &&
+ !post.removed &&
+ !post.community_deleted &&
+ !post.community_removed
+ ) {
+ if (!urlMap.get(post.url)) {
+ urlMap.set(post.url, [post]);
+ } else {
+ urlMap.get(post.url).push(post);
+ }
+ }
+ }
+
+ // Sort by oldest
+ // Remove the ones that have no length
+ for (let e of urlMap.entries()) {
+ if (e[1].length == 1) {
+ urlMap.delete(e[0]);
+ } else {
+ e[1].sort((a, b) => a.published.localeCompare(b.published));
+ }
+ }
+
+ for (let i = 0; i < posts.length; i++) {
+ let post = posts[i];
+ if (post.url) {
+ let found = urlMap.get(post.url);
+ if (found) {
+ // If its the oldest, add
+ if (post.id == found[0].id) {
+ post.duplicates = found.slice(1);
+ }
+ // Otherwise, delete it
+ else {
+ posts.splice(i--, 1);
+ }
+ }
+ }
+ }
+
+ return posts;
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ UserOperation,
+ Community,
+ Post as PostI,
+ GetPostResponse,
+ PostResponse,
+ Comment,
+ MarkCommentAsReadForm,
+ CommentResponse,
+ CommunityUser,
+ CommunityResponse,
+ CommentNode as CommentNodeI,
+ BanFromCommunityResponse,
+ BanUserResponse,
+ AddModToCommunityResponse,
+ AddAdminResponse,
+ SearchType,
+ SortType,
+ SearchForm,
+ GetPostForm,
+ SearchResponse,
+ GetSiteResponse,
+ GetCommunityResponse,
+ WebSocketJsonResponse,
+} from 'lemmy-js-client';
+import { CommentSortType, CommentViewType } from '../interfaces';
+import { WebSocketService, UserService } from '../services';
+import {
+ wsJsonToRes,
+ toast,
+ editCommentRes,
+ saveCommentRes,
+ createCommentLikeRes,
+ createPostLikeRes,
+ commentsToFlatNodes,
+ setupTippy,
+ favIconUrl,
+} from '../utils';
+import { PostListing } from './post-listing';
+import { Sidebar } from './sidebar';
+import { CommentForm } from './comment-form';
+import { CommentNodes } from './comment-nodes';
+import autosize from 'autosize';
+import { i18n } from '../i18next';
+
+interface PostState {
+ post: PostI;
+ comments: Comment[];
+ commentSort: CommentSortType;
+ commentViewType: CommentViewType;
+ community: Community;
+ moderators: CommunityUser[];
+ online: number;
+ scrolled?: boolean;
+ scrolled_comment_id?: number;
+ loading: boolean;
+ crossPosts: PostI[];
+ siteRes: GetSiteResponse;
+}
+
+export class Post extends Component<any, PostState> {
+ private subscription: Subscription;
+ private emptyState: PostState = {
+ post: null,
+ comments: [],
+ commentSort: CommentSortType.Hot,
+ commentViewType: CommentViewType.Tree,
+ community: null,
+ moderators: [],
+ online: null,
+ scrolled: false,
+ loading: true,
+ crossPosts: [],
+ siteRes: {
+ admins: [],
+ banned: [],
+ site: {
+ id: undefined,
+ name: undefined,
+ creator_id: undefined,
+ published: undefined,
+ creator_name: undefined,
+ number_of_users: undefined,
+ number_of_posts: undefined,
+ number_of_comments: undefined,
+ number_of_communities: undefined,
+ enable_downvotes: undefined,
+ open_registration: undefined,
+ enable_nsfw: undefined,
+ icon: undefined,
+ banner: undefined,
+ },
+ online: null,
+ version: null,
+ federated_instances: undefined,
+ },
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ let postId = Number(this.props.match.params.id);
+ if (this.props.match.params.comment_id) {
+ this.state.scrolled_comment_id = this.props.match.params.comment_id;
+ }
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ let form: GetPostForm = {
+ id: postId,
+ };
+ WebSocketService.Instance.getPost(form);
+ WebSocketService.Instance.getSite();
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ componentDidMount() {
+ autosize(document.querySelectorAll('textarea'));
+ }
+
+ componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
+ if (
+ this.state.scrolled_comment_id &&
+ !this.state.scrolled &&
+ lastState.comments.length > 0
+ ) {
+ var elmnt = document.getElementById(
+ `comment-${this.state.scrolled_comment_id}`
+ );
+ elmnt.scrollIntoView();
+ elmnt.classList.add('mark');
+ this.state.scrolled = true;
+ this.markScrolledAsRead(this.state.scrolled_comment_id);
+ }
+
+ // Necessary if you are on a post and you click another post (same route)
+ if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
+ // Couldnt get a refresh working. This does for now.
+ location.reload();
+
+ // let currentId = this.props.match.params.id;
+ // WebSocketService.Instance.getPost(currentId);
+ // this.context.router.history.push('/sponsors');
+ // this.context.refresh();
+ // this.context.router.history.push(_lastProps.location.pathname);
+ }
+ }
+
+ markScrolledAsRead(commentId: number) {
+ let found = this.state.comments.find(c => c.id == commentId);
+ let parent = this.state.comments.find(c => found.parent_id == c.id);
+ let parent_user_id = parent
+ ? parent.creator_id
+ : this.state.post.creator_id;
+
+ if (
+ UserService.Instance.user &&
+ UserService.Instance.user.id == parent_user_id
+ ) {
+ let form: MarkCommentAsReadForm = {
+ edit_id: found.id,
+ read: true,
+ auth: null,
+ };
+ WebSocketService.Instance.markCommentAsRead(form);
+ UserService.Instance.unreadCountSub.next(
+ UserService.Instance.unreadCountSub.value - 1
+ );
+ }
+ }
+
+ get documentTitle(): string {
+ if (this.state.post) {
+ return `${this.state.post.name} - ${this.state.siteRes.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ get favIcon(): string {
+ return this.state.siteRes.site.icon
+ ? this.state.siteRes.site.icon
+ : favIconUrl;
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle}>
+ <link
+ id="favicon"
+ rel="icon"
+ type="image/x-icon"
+ href={this.favIcon}
+ />
+ </Helmet>
+ {this.state.loading ? (
+ <h5>
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ </h5>
+ ) : (
+ <div class="row">
+ <div class="col-12 col-md-8 mb-3">
+ <PostListing
+ post={this.state.post}
+ showBody
+ showCommunity
+ moderators={this.state.moderators}
+ admins={this.state.siteRes.admins}
+ enableDownvotes={this.state.siteRes.site.enable_downvotes}
+ enableNsfw={this.state.siteRes.site.enable_nsfw}
+ />
+ <div className="mb-2" />
+ <CommentForm
+ postId={this.state.post.id}
+ disabled={this.state.post.locked}
+ />
+ {this.state.comments.length > 0 && this.sortRadios()}
+ {this.state.commentViewType == CommentViewType.Tree &&
+ this.commentsTree()}
+ {this.state.commentViewType == CommentViewType.Chat &&
+ this.commentsFlat()}
+ </div>
+ <div class="col-12 col-sm-12 col-md-4">{this.sidebar()}</div>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ sortRadios() {
+ return (
+ <>
+ <div class="btn-group btn-group-toggle flex-wrap mr-3 mb-2">
+ <label
+ className={`btn btn-outline-secondary pointer ${
+ this.state.commentSort === CommentSortType.Hot && 'active'
+ }`}
+ >
+ {i18n.t('hot')}
+ <input
+ type="radio"
+ value={CommentSortType.Hot}
+ checked={this.state.commentSort === CommentSortType.Hot}
+ onChange={linkEvent(this, this.handleCommentSortChange)}
+ />
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer ${
+ this.state.commentSort === CommentSortType.Top && 'active'
+ }`}
+ >
+ {i18n.t('top')}
+ <input
+ type="radio"
+ value={CommentSortType.Top}
+ checked={this.state.commentSort === CommentSortType.Top}
+ onChange={linkEvent(this, this.handleCommentSortChange)}
+ />
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer ${
+ this.state.commentSort === CommentSortType.New && 'active'
+ }`}
+ >
+ {i18n.t('new')}
+ <input
+ type="radio"
+ value={CommentSortType.New}
+ checked={this.state.commentSort === CommentSortType.New}
+ onChange={linkEvent(this, this.handleCommentSortChange)}
+ />
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer ${
+ this.state.commentSort === CommentSortType.Old && 'active'
+ }`}
+ >
+ {i18n.t('old')}
+ <input
+ type="radio"
+ value={CommentSortType.Old}
+ checked={this.state.commentSort === CommentSortType.Old}
+ onChange={linkEvent(this, this.handleCommentSortChange)}
+ />
+ </label>
+ </div>
+ <div class="btn-group btn-group-toggle flex-wrap mb-2">
+ <label
+ className={`btn btn-outline-secondary pointer ${
+ this.state.commentViewType === CommentViewType.Chat && 'active'
+ }`}
+ >
+ {i18n.t('chat')}
+ <input
+ type="radio"
+ value={CommentViewType.Chat}
+ checked={this.state.commentViewType === CommentViewType.Chat}
+ onChange={linkEvent(this, this.handleCommentViewTypeChange)}
+ />
+ </label>
+ </div>
+ </>
+ );
+ }
+
+ commentsFlat() {
+ return (
+ <div>
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.comments)}
+ noIndent
+ locked={this.state.post.locked}
+ moderators={this.state.moderators}
+ admins={this.state.siteRes.admins}
+ postCreatorId={this.state.post.creator_id}
+ showContext
+ enableDownvotes={this.state.siteRes.site.enable_downvotes}
+ sort={this.state.commentSort}
+ />
+ </div>
+ );
+ }
+
+ sidebar() {
+ return (
+ <div class="mb-3">
+ <Sidebar
+ community={this.state.community}
+ moderators={this.state.moderators}
+ admins={this.state.siteRes.admins}
+ online={this.state.online}
+ enableNsfw={this.state.siteRes.site.enable_nsfw}
+ showIcon
+ />
+ </div>
+ );
+ }
+
+ handleCommentSortChange(i: Post, event: any) {
+ i.state.commentSort = Number(event.target.value);
+ i.state.commentViewType = CommentViewType.Tree;
+ i.setState(i.state);
+ }
+
+ handleCommentViewTypeChange(i: Post, event: any) {
+ i.state.commentViewType = Number(event.target.value);
+ i.state.commentSort = CommentSortType.New;
+ i.setState(i.state);
+ }
+
+ buildCommentsTree(): CommentNodeI[] {
+ let map = new Map<number, CommentNodeI>();
+ for (let comment of this.state.comments) {
+ let node: CommentNodeI = {
+ comment: comment,
+ children: [],
+ };
+ map.set(comment.id, { ...node });
+ }
+ let tree: CommentNodeI[] = [];
+ for (let comment of this.state.comments) {
+ let child = map.get(comment.id);
+ if (comment.parent_id) {
+ let parent_ = map.get(comment.parent_id);
+ parent_.children.push(child);
+ } else {
+ tree.push(child);
+ }
+
+ this.setDepth(child);
+ }
+
+ return tree;
+ }
+
+ setDepth(node: CommentNodeI, i: number = 0): void {
+ for (let child of node.children) {
+ child.comment.depth = i;
+ this.setDepth(child, i + 1);
+ }
+ }
+
+ commentsTree() {
+ let nodes = this.buildCommentsTree();
+ return (
+ <div>
+ <CommentNodes
+ nodes={nodes}
+ locked={this.state.post.locked}
+ moderators={this.state.moderators}
+ admins={this.state.siteRes.admins}
+ postCreatorId={this.state.post.creator_id}
+ sort={this.state.commentSort}
+ enableDownvotes={this.state.siteRes.site.enable_downvotes}
+ />
+ </div>
+ );
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ return;
+ } else if (msg.reconnect) {
+ WebSocketService.Instance.getPost({
+ id: Number(this.props.match.params.id),
+ });
+ } else if (res.op == UserOperation.GetPost) {
+ let data = res.data as GetPostResponse;
+ this.state.post = data.post;
+ this.state.comments = data.comments;
+ this.state.community = data.community;
+ this.state.moderators = data.moderators;
+ this.state.online = data.online;
+ this.state.loading = false;
+
+ // Get cross-posts
+ if (this.state.post.url) {
+ let form: SearchForm = {
+ q: this.state.post.url,
+ type_: SearchType.Url,
+ sort: SortType.TopAll,
+ page: 1,
+ limit: 6,
+ };
+ WebSocketService.Instance.search(form);
+ }
+
+ this.setState(this.state);
+ setupTippy();
+ } else if (res.op == UserOperation.CreateComment) {
+ let data = res.data as CommentResponse;
+
+ // Necessary since it might be a user reply
+ if (data.recipient_ids.length == 0) {
+ this.state.comments.unshift(data.comment);
+ this.setState(this.state);
+ }
+ } else if (
+ res.op == UserOperation.EditComment ||
+ res.op == UserOperation.DeleteComment ||
+ res.op == UserOperation.RemoveComment
+ ) {
+ let data = res.data as CommentResponse;
+ editCommentRes(data, this.state.comments);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.SaveComment) {
+ let data = res.data as CommentResponse;
+ saveCommentRes(data, this.state.comments);
+ this.setState(this.state);
+ setupTippy();
+ } else if (res.op == UserOperation.CreateCommentLike) {
+ let data = res.data as CommentResponse;
+ createCommentLikeRes(data, this.state.comments);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreatePostLike) {
+ let data = res.data as PostResponse;
+ createPostLikeRes(data, this.state.post);
+ this.setState(this.state);
+ } else if (
+ res.op == UserOperation.EditPost ||
+ res.op == UserOperation.DeletePost ||
+ res.op == UserOperation.RemovePost ||
+ res.op == UserOperation.LockPost ||
+ res.op == UserOperation.StickyPost
+ ) {
+ let data = res.data as PostResponse;
+ this.state.post = data.post;
+ this.setState(this.state);
+ setupTippy();
+ } else if (res.op == UserOperation.SavePost) {
+ let data = res.data as PostResponse;
+ this.state.post = data.post;
+ this.setState(this.state);
+ setupTippy();
+ } else if (
+ res.op == UserOperation.EditCommunity ||
+ res.op == UserOperation.DeleteCommunity ||
+ res.op == UserOperation.RemoveCommunity
+ ) {
+ let data = res.data as CommunityResponse;
+ this.state.community = data.community;
+ this.state.post.community_id = data.community.id;
+ this.state.post.community_name = data.community.name;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.FollowCommunity) {
+ let data = res.data as CommunityResponse;
+ this.state.community.subscribed = data.community.subscribed;
+ this.state.community.number_of_subscribers =
+ data.community.number_of_subscribers;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.BanFromCommunity) {
+ let data = res.data as BanFromCommunityResponse;
+ this.state.comments
+ .filter(c => c.creator_id == data.user.id)
+ .forEach(c => (c.banned_from_community = data.banned));
+ if (this.state.post.creator_id == data.user.id) {
+ this.state.post.banned_from_community = data.banned;
+ }
+ this.setState(this.state);
+ } else if (res.op == UserOperation.AddModToCommunity) {
+ let data = res.data as AddModToCommunityResponse;
+ this.state.moderators = data.moderators;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.BanUser) {
+ let data = res.data as BanUserResponse;
+ this.state.comments
+ .filter(c => c.creator_id == data.user.id)
+ .forEach(c => (c.banned = data.banned));
+ if (this.state.post.creator_id == data.user.id) {
+ this.state.post.banned = data.banned;
+ }
+ this.setState(this.state);
+ } else if (res.op == UserOperation.AddAdmin) {
+ let data = res.data as AddAdminResponse;
+ this.state.siteRes.admins = data.admins;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.Search) {
+ let data = res.data as SearchResponse;
+ this.state.crossPosts = data.posts.filter(
+ p => p.id != Number(this.props.match.params.id)
+ );
+ if (this.state.crossPosts.length) {
+ this.state.post.duplicates = this.state.crossPosts;
+ }
+ this.setState(this.state);
+ } else if (
+ res.op == UserOperation.TransferSite ||
+ res.op == UserOperation.GetSite
+ ) {
+ let data = res.data as GetSiteResponse;
+ this.state.siteRes = data;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.TransferCommunity) {
+ let data = res.data as GetCommunityResponse;
+ this.state.community = data.community;
+ this.state.moderators = data.moderators;
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Prompt } from 'inferno-router';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ PrivateMessageForm as PrivateMessageFormI,
+ EditPrivateMessageForm,
+ PrivateMessageFormParams,
+ PrivateMessage,
+ PrivateMessageResponse,
+ UserView,
+ UserOperation,
+ UserDetailsResponse,
+ GetUserDetailsForm,
+ SortType,
+ WebSocketJsonResponse,
+} from 'lemmy-js-client';
+import { WebSocketService } from '../services';
+import {
+ capitalizeFirstLetter,
+ wsJsonToRes,
+ toast,
+ setupTippy,
+} from '../utils';
+import { UserListing } from './user-listing';
+import { MarkdownTextArea } from './markdown-textarea';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
+
+interface PrivateMessageFormProps {
+ privateMessage?: PrivateMessage; // If a pm is given, that means this is an edit
+ params?: PrivateMessageFormParams;
+ onCancel?(): any;
+ onCreate?(message: PrivateMessage): any;
+ onEdit?(message: PrivateMessage): any;
+}
+
+interface PrivateMessageFormState {
+ privateMessageForm: PrivateMessageFormI;
+ recipient: UserView;
+ loading: boolean;
+ previewMode: boolean;
+ showDisclaimer: boolean;
+}
+
+export class PrivateMessageForm extends Component<
+ PrivateMessageFormProps,
+ PrivateMessageFormState
+> {
+ private subscription: Subscription;
+ private emptyState: PrivateMessageFormState = {
+ privateMessageForm: {
+ content: null,
+ recipient_id: null,
+ },
+ recipient: null,
+ loading: false,
+ previewMode: false,
+ showDisclaimer: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ this.handleContentChange = this.handleContentChange.bind(this);
+
+ if (this.props.privateMessage) {
+ this.state.privateMessageForm = {
+ content: this.props.privateMessage.content,
+ recipient_id: this.props.privateMessage.recipient_id,
+ };
+ }
+
+ if (this.props.params) {
+ this.state.privateMessageForm.recipient_id = this.props.params.recipient_id;
+ let form: GetUserDetailsForm = {
+ user_id: this.state.privateMessageForm.recipient_id,
+ sort: SortType.New,
+ saved_only: false,
+ };
+ WebSocketService.Instance.getUserDetails(form);
+ }
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+ }
+
+ componentDidMount() {
+ setupTippy();
+ }
+
+ componentDidUpdate() {
+ if (!this.state.loading && this.state.privateMessageForm.content) {
+ window.onbeforeunload = () => true;
+ } else {
+ window.onbeforeunload = undefined;
+ }
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ window.onbeforeunload = null;
+ }
+
+ render() {
+ return (
+ <div>
+ <Prompt
+ when={!this.state.loading && this.state.privateMessageForm.content}
+ message={i18n.t('block_leaving')}
+ />
+ <form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
+ {!this.props.privateMessage && (
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">
+ {capitalizeFirstLetter(i18n.t('to'))}
+ </label>
+
+ {this.state.recipient && (
+ <div class="col-sm-10 form-control-plaintext">
+ <UserListing
+ user={{
+ name: this.state.recipient.name,
+ preferred_username: this.state.recipient
+ .preferred_username,
+ avatar: this.state.recipient.avatar,
+ id: this.state.recipient.id,
+ local: this.state.recipient.local,
+ actor_id: this.state.recipient.actor_id,
+ }}
+ />
+ </div>
+ )}
+ </div>
+ )}
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">
+ {i18n.t('message')}
+ <span
+ onClick={linkEvent(this, this.handleShowDisclaimer)}
+ class="ml-2 pointer text-danger"
+ data-tippy-content={i18n.t('disclaimer')}
+ >
+ <svg class={`icon icon-inline`}>
+ <use xlinkHref="#icon-alert-triangle"></use>
+ </svg>
+ </span>
+ </label>
+ <div class="col-sm-10">
+ <MarkdownTextArea
+ initialContent={this.state.privateMessageForm.content}
+ onContentChange={this.handleContentChange}
+ />
+ </div>
+ </div>
+
+ {this.state.showDisclaimer && (
+ <div class="form-group row">
+ <div class="offset-sm-2 col-sm-10">
+ <div class="alert alert-danger" role="alert">
+ <T i18nKey="private_message_disclaimer">
+ #
+ <a
+ class="alert-link"
+ target="_blank"
+ rel="noopener"
+ href="https://element.io/get-started"
+ >
+ #
+ </a>
+ </T>
+ </div>
+ </div>
+ </div>
+ )}
+ <div class="form-group row">
+ <div class="offset-sm-2 col-sm-10">
+ <button
+ type="submit"
+ class="btn btn-secondary mr-2"
+ disabled={this.state.loading}
+ >
+ {this.state.loading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : this.props.privateMessage ? (
+ capitalizeFirstLetter(i18n.t('save'))
+ ) : (
+ capitalizeFirstLetter(i18n.t('send_message'))
+ )}
+ </button>
+ {this.props.privateMessage && (
+ <button
+ type="button"
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.handleCancel)}
+ >
+ {i18n.t('cancel')}
+ </button>
+ )}
+ <ul class="d-inline-block float-right list-inline mb-1 text-muted font-weight-bold">
+ <li class="list-inline-item"></li>
+ </ul>
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+ }
+
+ handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
+ event.preventDefault();
+ if (i.props.privateMessage) {
+ let editForm: EditPrivateMessageForm = {
+ edit_id: i.props.privateMessage.id,
+ content: i.state.privateMessageForm.content,
+ };
+ WebSocketService.Instance.editPrivateMessage(editForm);
+ } else {
+ WebSocketService.Instance.createPrivateMessage(
+ i.state.privateMessageForm
+ );
+ }
+ i.state.loading = true;
+ i.setState(i.state);
+ }
+
+ handleRecipientChange(i: PrivateMessageForm, event: any) {
+ i.state.recipient = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleContentChange(val: string) {
+ this.state.privateMessageForm.content = val;
+ this.setState(this.state);
+ }
+
+ handleCancel(i: PrivateMessageForm) {
+ i.props.onCancel();
+ }
+
+ handlePreviewToggle(i: PrivateMessageForm, event: any) {
+ event.preventDefault();
+ i.state.previewMode = !i.state.previewMode;
+ i.setState(i.state);
+ }
+
+ handleShowDisclaimer(i: PrivateMessageForm) {
+ i.state.showDisclaimer = !i.state.showDisclaimer;
+ i.setState(i.state);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ this.state.loading = false;
+ this.setState(this.state);
+ return;
+ } else if (
+ res.op == UserOperation.EditPrivateMessage ||
+ res.op == UserOperation.DeletePrivateMessage ||
+ res.op == UserOperation.MarkPrivateMessageAsRead
+ ) {
+ let data = res.data as PrivateMessageResponse;
+ this.state.loading = false;
+ this.props.onEdit(data.message);
+ } else if (res.op == UserOperation.GetUserDetails) {
+ let data = res.data as UserDetailsResponse;
+ this.state.recipient = data.user;
+ this.state.privateMessageForm.recipient_id = data.user.id;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreatePrivateMessage) {
+ let data = res.data as PrivateMessageResponse;
+ this.state.loading = false;
+ this.props.onCreate(data.message);
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import {
+ PrivateMessage as PrivateMessageI,
+ DeletePrivateMessageForm,
+ MarkPrivateMessageAsReadForm,
+} from 'lemmy-js-client';
+import { WebSocketService, UserService } from '../services';
+import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
+import { MomentTime } from './moment-time';
+import { PrivateMessageForm } from './private-message-form';
+import { UserListing, UserOther } from './user-listing';
+import { i18n } from '../i18next';
+
+interface PrivateMessageState {
+ showReply: boolean;
+ showEdit: boolean;
+ collapsed: boolean;
+ viewSource: boolean;
+}
+
+interface PrivateMessageProps {
+ privateMessage: PrivateMessageI;
+}
+
+export class PrivateMessage extends Component<
+ PrivateMessageProps,
+ PrivateMessageState
+> {
+ private emptyState: PrivateMessageState = {
+ showReply: false,
+ showEdit: false,
+ collapsed: false,
+ viewSource: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handleReplyCancel = this.handleReplyCancel.bind(this);
+ this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
+ this
+ );
+ this.handlePrivateMessageEdit = this.handlePrivateMessageEdit.bind(this);
+ }
+
+ get mine(): boolean {
+ return (
+ UserService.Instance.user &&
+ UserService.Instance.user.id == this.props.privateMessage.creator_id
+ );
+ }
+
+ render() {
+ let message = this.props.privateMessage;
+ let userOther: UserOther = this.mine
+ ? {
+ name: message.recipient_name,
+ preferred_username: message.recipient_preferred_username,
+ id: message.id,
+ avatar: message.recipient_avatar,
+ local: message.recipient_local,
+ actor_id: message.recipient_actor_id,
+ published: message.published,
+ }
+ : {
+ name: message.creator_name,
+ preferred_username: message.creator_preferred_username,
+ id: message.id,
+ avatar: message.creator_avatar,
+ local: message.creator_local,
+ actor_id: message.creator_actor_id,
+ published: message.published,
+ };
+
+ return (
+ <div class="border-top border-light">
+ <div>
+ <ul class="list-inline mb-0 text-muted small">
+ {/* TODO refactor this */}
+ <li className="list-inline-item">
+ {this.mine ? i18n.t('to') : i18n.t('from')}
+ </li>
+ <li className="list-inline-item">
+ <UserListing user={userOther} />
+ </li>
+ <li className="list-inline-item">
+ <span>
+ <MomentTime data={message} />
+ </span>
+ </li>
+ <li className="list-inline-item">
+ <div
+ className="pointer text-monospace"
+ onClick={linkEvent(this, this.handleMessageCollapse)}
+ >
+ {this.state.collapsed ? (
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-plus-square"></use>
+ </svg>
+ ) : (
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-minus-square"></use>
+ </svg>
+ )}
+ </div>
+ </li>
+ </ul>
+ {this.state.showEdit && (
+ <PrivateMessageForm
+ privateMessage={message}
+ onEdit={this.handlePrivateMessageEdit}
+ onCreate={this.handlePrivateMessageCreate}
+ onCancel={this.handleReplyCancel}
+ />
+ )}
+ {!this.state.showEdit && !this.state.collapsed && (
+ <div>
+ {this.state.viewSource ? (
+ <pre>{this.messageUnlessRemoved}</pre>
+ ) : (
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(this.messageUnlessRemoved)}
+ />
+ )}
+ <ul class="list-inline mb-0 text-muted font-weight-bold">
+ {!this.mine && (
+ <>
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleMarkRead)}
+ data-tippy-content={
+ message.read
+ ? i18n.t('mark_as_unread')
+ : i18n.t('mark_as_read')
+ }
+ >
+ <svg
+ class={`icon icon-inline ${
+ message.read && 'text-success'
+ }`}
+ >
+ <use xlinkHref="#icon-check"></use>
+ </svg>
+ </button>
+ </li>
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleReplyClick)}
+ data-tippy-content={i18n.t('reply')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-reply1"></use>
+ </svg>
+ </button>
+ </li>
+ </>
+ )}
+ {this.mine && (
+ <>
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleEditClick)}
+ data-tippy-content={i18n.t('edit')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-edit"></use>
+ </svg>
+ </button>
+ </li>
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleDeleteClick)}
+ data-tippy-content={
+ !message.deleted
+ ? i18n.t('delete')
+ : i18n.t('restore')
+ }
+ >
+ <svg
+ class={`icon icon-inline ${
+ message.deleted && 'text-danger'
+ }`}
+ >
+ <use xlinkHref="#icon-trash"></use>
+ </svg>
+ </button>
+ </li>
+ </>
+ )}
+ <li className="list-inline-item">
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(this, this.handleViewSource)}
+ data-tippy-content={i18n.t('view_source')}
+ >
+ <svg
+ class={`icon icon-inline ${
+ this.state.viewSource && 'text-success'
+ }`}
+ >
+ <use xlinkHref="#icon-file-text"></use>
+ </svg>
+ </button>
+ </li>
+ </ul>
+ </div>
+ )}
+ </div>
+ {this.state.showReply && (
+ <PrivateMessageForm
+ params={{
+ recipient_id: this.props.privateMessage.creator_id,
+ }}
+ onCreate={this.handlePrivateMessageCreate}
+ />
+ )}
+ {/* A collapsed clearfix */}
+ {this.state.collapsed && <div class="row col-12"></div>}
+ </div>
+ );
+ }
+
+ get messageUnlessRemoved(): string {
+ let message = this.props.privateMessage;
+ return message.deleted ? `*${i18n.t('deleted')}*` : message.content;
+ }
+
+ handleReplyClick(i: PrivateMessage) {
+ i.state.showReply = true;
+ i.setState(i.state);
+ }
+
+ handleEditClick(i: PrivateMessage) {
+ i.state.showEdit = true;
+ i.setState(i.state);
+ }
+
+ handleDeleteClick(i: PrivateMessage) {
+ let form: DeletePrivateMessageForm = {
+ edit_id: i.props.privateMessage.id,
+ deleted: !i.props.privateMessage.deleted,
+ };
+ WebSocketService.Instance.deletePrivateMessage(form);
+ }
+
+ handleReplyCancel() {
+ this.state.showReply = false;
+ this.state.showEdit = false;
+ this.setState(this.state);
+ }
+
+ handleMarkRead(i: PrivateMessage) {
+ let form: MarkPrivateMessageAsReadForm = {
+ edit_id: i.props.privateMessage.id,
+ read: !i.props.privateMessage.read,
+ };
+ WebSocketService.Instance.markPrivateMessageAsRead(form);
+ }
+
+ handleMessageCollapse(i: PrivateMessage) {
+ i.state.collapsed = !i.state.collapsed;
+ i.setState(i.state);
+ }
+
+ handleViewSource(i: PrivateMessage) {
+ i.state.viewSource = !i.state.viewSource;
+ i.setState(i.state);
+ }
+
+ handlePrivateMessageEdit() {
+ this.state.showEdit = false;
+ this.setState(this.state);
+ }
+
+ handlePrivateMessageCreate(message: PrivateMessageI) {
+ if (
+ UserService.Instance.user &&
+ message.creator_id == UserService.Instance.user.id
+ ) {
+ this.state.showReply = false;
+ this.setState(this.state);
+ toast(i18n.t('message_sent'));
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ UserOperation,
+ Post,
+ Comment,
+ Community,
+ UserView,
+ SortType,
+ SearchForm,
+ SearchResponse,
+ SearchType,
+ PostResponse,
+ CommentResponse,
+ WebSocketJsonResponse,
+ GetSiteResponse,
+ Site,
+} from 'lemmy-js-client';
+import { WebSocketService } from '../services';
+import {
+ wsJsonToRes,
+ fetchLimit,
+ routeSearchTypeToEnum,
+ routeSortTypeToEnum,
+ toast,
+ createCommentLikeRes,
+ createPostLikeFindRes,
+ commentsToFlatNodes,
+ getPageFromProps,
+} from '../utils';
+import { PostListing } from './post-listing';
+import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
+import { SortSelect } from './sort-select';
+import { CommentNodes } from './comment-nodes';
+import { i18n } from '../i18next';
+
+interface SearchState {
+ q: string;
+ type_: SearchType;
+ sort: SortType;
+ page: number;
+ searchResponse: SearchResponse;
+ loading: boolean;
+ site: Site;
+ searchText: string;
+}
+
+interface SearchProps {
+ q: string;
+ type_: SearchType;
+ sort: SortType;
+ page: number;
+}
+
+interface UrlParams {
+ q?: string;
+ type_?: SearchType;
+ sort?: SortType;
+ page?: number;
+}
+
+export class Search extends Component<any, SearchState> {
+ private subscription: Subscription;
+ private emptyState: SearchState = {
+ q: Search.getSearchQueryFromProps(this.props),
+ type_: Search.getSearchTypeFromProps(this.props),
+ sort: Search.getSortTypeFromProps(this.props),
+ page: getPageFromProps(this.props),
+ searchText: Search.getSearchQueryFromProps(this.props),
+ searchResponse: {
+ type_: null,
+ posts: [],
+ comments: [],
+ communities: [],
+ users: [],
+ },
+ loading: false,
+ site: {
+ id: undefined,
+ name: undefined,
+ creator_id: undefined,
+ published: undefined,
+ creator_name: undefined,
+ number_of_users: undefined,
+ number_of_posts: undefined,
+ number_of_comments: undefined,
+ number_of_communities: undefined,
+ enable_downvotes: undefined,
+ open_registration: undefined,
+ enable_nsfw: undefined,
+ },
+ };
+
+ static getSearchQueryFromProps(props: any): string {
+ return props.match.params.q ? props.match.params.q : '';
+ }
+
+ static getSearchTypeFromProps(props: any): SearchType {
+ return props.match.params.type
+ ? routeSearchTypeToEnum(props.match.params.type)
+ : SearchType.All;
+ }
+
+ static getSortTypeFromProps(props: any): SortType {
+ return props.match.params.sort
+ ? routeSortTypeToEnum(props.match.params.sort)
+ : SortType.TopAll;
+ }
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handleSortChange = this.handleSortChange.bind(this);
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ WebSocketService.Instance.getSite();
+
+ if (this.state.q) {
+ this.search();
+ }
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ static getDerivedStateFromProps(props: any): SearchProps {
+ return {
+ q: Search.getSearchQueryFromProps(props),
+ type_: Search.getSearchTypeFromProps(props),
+ sort: Search.getSortTypeFromProps(props),
+ page: getPageFromProps(props),
+ };
+ }
+
+ componentDidUpdate(_: any, lastState: SearchState) {
+ if (
+ lastState.q !== this.state.q ||
+ lastState.type_ !== this.state.type_ ||
+ lastState.sort !== this.state.sort ||
+ lastState.page !== this.state.page
+ ) {
+ this.setState({ loading: true, searchText: this.state.q });
+ this.search();
+ }
+ }
+
+ get documentTitle(): string {
+ if (this.state.site.name) {
+ if (this.state.q) {
+ return `${i18n.t('search')} - ${this.state.q} - ${
+ this.state.site.name
+ }`;
+ } else {
+ return `${i18n.t('search')} - ${this.state.site.name}`;
+ }
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle} />
+ <h5>{i18n.t('search')}</h5>
+ {this.selects()}
+ {this.searchForm()}
+ {this.state.type_ == SearchType.All && this.all()}
+ {this.state.type_ == SearchType.Comments && this.comments()}
+ {this.state.type_ == SearchType.Posts && this.posts()}
+ {this.state.type_ == SearchType.Communities && this.communities()}
+ {this.state.type_ == SearchType.Users && this.users()}
+ {this.resultsCount() == 0 && <span>{i18n.t('no_results')}</span>}
+ {this.paginator()}
+ </div>
+ );
+ }
+
+ searchForm() {
+ return (
+ <form
+ class="form-inline"
+ onSubmit={linkEvent(this, this.handleSearchSubmit)}
+ >
+ <input
+ type="text"
+ class="form-control mr-2 mb-2"
+ value={this.state.searchText}
+ placeholder={`${i18n.t('search')}...`}
+ onInput={linkEvent(this, this.handleQChange)}
+ required
+ minLength={3}
+ />
+ <button type="submit" class="btn btn-secondary mr-2 mb-2">
+ {this.state.loading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ <span>{i18n.t('search')}</span>
+ )}
+ </button>
+ </form>
+ );
+ }
+
+ selects() {
+ return (
+ <div className="mb-2">
+ <select
+ value={this.state.type_}
+ onChange={linkEvent(this, this.handleTypeChange)}
+ class="custom-select w-auto mb-2"
+ >
+ <option disabled>{i18n.t('type')}</option>
+ <option value={SearchType.All}>{i18n.t('all')}</option>
+ <option value={SearchType.Comments}>{i18n.t('comments')}</option>
+ <option value={SearchType.Posts}>{i18n.t('posts')}</option>
+ <option value={SearchType.Communities}>
+ {i18n.t('communities')}
+ </option>
+ <option value={SearchType.Users}>{i18n.t('users')}</option>
+ </select>
+ <span class="ml-2">
+ <SortSelect
+ sort={this.state.sort}
+ onChange={this.handleSortChange}
+ hideHot
+ />
+ </span>
+ </div>
+ );
+ }
+
+ all() {
+ let combined: {
+ type_: string;
+ data: Comment | Post | Community | UserView;
+ }[] = [];
+ let comments = this.state.searchResponse.comments.map(e => {
+ return { type_: 'comments', data: e };
+ });
+ let posts = this.state.searchResponse.posts.map(e => {
+ return { type_: 'posts', data: e };
+ });
+ let communities = this.state.searchResponse.communities.map(e => {
+ return { type_: 'communities', data: e };
+ });
+ let users = this.state.searchResponse.users.map(e => {
+ return { type_: 'users', data: e };
+ });
+
+ combined.push(...comments);
+ combined.push(...posts);
+ combined.push(...communities);
+ combined.push(...users);
+
+ // Sort it
+ if (this.state.sort == SortType.New) {
+ combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
+ } else {
+ combined.sort(
+ (a, b) =>
+ ((b.data as Comment | Post).score |
+ (b.data as Community).number_of_subscribers |
+ (b.data as UserView).comment_score) -
+ ((a.data as Comment | Post).score |
+ (a.data as Community).number_of_subscribers |
+ (a.data as UserView).comment_score)
+ );
+ }
+
+ return (
+ <div>
+ {combined.map(i => (
+ <div class="row">
+ <div class="col-12">
+ {i.type_ == 'posts' && (
+ <PostListing
+ key={(i.data as Post).id}
+ post={i.data as Post}
+ showCommunity
+ enableDownvotes={this.state.site.enable_downvotes}
+ enableNsfw={this.state.site.enable_nsfw}
+ />
+ )}
+ {i.type_ == 'comments' && (
+ <CommentNodes
+ key={(i.data as Comment).id}
+ nodes={[{ comment: i.data as Comment }]}
+ locked
+ noIndent
+ enableDownvotes={this.state.site.enable_downvotes}
+ />
+ )}
+ {i.type_ == 'communities' && (
+ <div>{this.communityListing(i.data as Community)}</div>
+ )}
+ {i.type_ == 'users' && (
+ <div>
+ <span>
+ @
+ <UserListing
+ user={{
+ name: (i.data as UserView).name,
+ preferred_username: (i.data as UserView)
+ .preferred_username,
+ avatar: (i.data as UserView).avatar,
+ }}
+ />
+ </span>
+ <span>{` - ${i18n.t('number_of_comments', {
+ count: (i.data as UserView).number_of_comments,
+ })}`}</span>
+ </div>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ );
+ }
+
+ comments() {
+ return (
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
+ locked
+ noIndent
+ enableDownvotes={this.state.site.enable_downvotes}
+ />
+ );
+ }
+
+ posts() {
+ return (
+ <>
+ {this.state.searchResponse.posts.map(post => (
+ <div class="row">
+ <div class="col-12">
+ <PostListing
+ post={post}
+ showCommunity
+ enableDownvotes={this.state.site.enable_downvotes}
+ enableNsfw={this.state.site.enable_nsfw}
+ />
+ </div>
+ </div>
+ ))}
+ </>
+ );
+ }
+
+ // Todo possibly create UserListing and CommunityListing
+ communities() {
+ return (
+ <>
+ {this.state.searchResponse.communities.map(community => (
+ <div class="row">
+ <div class="col-12">{this.communityListing(community)}</div>
+ </div>
+ ))}
+ </>
+ );
+ }
+
+ communityListing(community: Community) {
+ return (
+ <>
+ <span>
+ <CommunityLink community={community} />
+ </span>
+ <span>{` - ${community.title} -
+ ${i18n.t('number_of_subscribers', {
+ count: community.number_of_subscribers,
+ })}
+ `}</span>
+ </>
+ );
+ }
+
+ users() {
+ return (
+ <>
+ {this.state.searchResponse.users.map(user => (
+ <div class="row">
+ <div class="col-12">
+ <span>
+ @
+ <UserListing
+ user={{
+ name: user.name,
+ avatar: user.avatar,
+ }}
+ />
+ </span>
+ <span>{` - ${i18n.t('number_of_comments', {
+ count: user.number_of_comments,
+ })}`}</span>
+ </div>
+ </div>
+ ))}
+ </>
+ );
+ }
+
+ paginator() {
+ return (
+ <div class="mt-2">
+ {this.state.page > 1 && (
+ <button
+ class="btn btn-secondary mr-1"
+ onClick={linkEvent(this, this.prevPage)}
+ >
+ {i18n.t('prev')}
+ </button>
+ )}
+
+ {this.resultsCount() > 0 && (
+ <button
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.nextPage)}
+ >
+ {i18n.t('next')}
+ </button>
+ )}
+ </div>
+ );
+ }
+
+ resultsCount(): number {
+ let res = this.state.searchResponse;
+ return (
+ res.posts.length +
+ res.comments.length +
+ res.communities.length +
+ res.users.length
+ );
+ }
+
+ nextPage(i: Search) {
+ i.updateUrl({ page: i.state.page + 1 });
+ }
+
+ prevPage(i: Search) {
+ i.updateUrl({ page: i.state.page - 1 });
+ }
+
+ search() {
+ let form: SearchForm = {
+ q: this.state.q,
+ type_: this.state.type_,
+ sort: this.state.sort,
+ page: this.state.page,
+ limit: fetchLimit,
+ };
+
+ if (this.state.q != '') {
+ WebSocketService.Instance.search(form);
+ }
+ }
+
+ handleSortChange(val: SortType) {
+ this.updateUrl({ sort: val, page: 1 });
+ }
+
+ handleTypeChange(i: Search, event: any) {
+ i.updateUrl({
+ type_: SearchType[event.target.value],
+ page: 1,
+ });
+ }
+
+ handleSearchSubmit(i: Search, event: any) {
+ event.preventDefault();
+ i.updateUrl({
+ q: i.state.searchText,
+ type_: i.state.type_,
+ sort: i.state.sort,
+ page: i.state.page,
+ });
+ }
+
+ handleQChange(i: Search, event: any) {
+ i.setState({ searchText: event.target.value });
+ }
+
+ updateUrl(paramUpdates: UrlParams) {
+ const qStr = paramUpdates.q || this.state.q;
+ const typeStr = paramUpdates.type_ || this.state.type_;
+ const sortStr = paramUpdates.sort || this.state.sort;
+ const page = paramUpdates.page || this.state.page;
+ this.props.history.push(
+ `/search/q/${qStr}/type/${typeStr}/sort/${sortStr}/page/${page}`
+ );
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ return;
+ } else if (res.op == UserOperation.Search) {
+ let data = res.data as SearchResponse;
+ this.state.searchResponse = data;
+ this.state.loading = false;
+ window.scrollTo(0, 0);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreateCommentLike) {
+ let data = res.data as CommentResponse;
+ createCommentLikeRes(data, this.state.searchResponse.comments);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreatePostLike) {
+ let data = res.data as PostResponse;
+ createPostLikeFindRes(data, this.state.searchResponse.posts);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.site = data.site;
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ RegisterForm,
+ LoginResponse,
+ UserOperation,
+ WebSocketJsonResponse,
+} from 'lemmy-js-client';
+import { WebSocketService, UserService } from '../services';
+import { wsJsonToRes, toast } from '../utils';
+import { SiteForm } from './site-form';
+import { i18n } from '../i18next';
+
+interface State {
+ userForm: RegisterForm;
+ doneRegisteringUser: boolean;
+ userLoading: boolean;
+}
+
+export class Setup extends Component<any, State> {
+ private subscription: Subscription;
+
+ private emptyState: State = {
+ userForm: {
+ username: undefined,
+ password: undefined,
+ password_verify: undefined,
+ admin: true,
+ show_nsfw: true,
+ // The first admin signup doesn't need a captcha
+ captcha_uuid: '',
+ captcha_answer: '',
+ },
+ doneRegisteringUser: false,
+ userLoading: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ get documentTitle(): string {
+ return `${i18n.t('setup')} - Lemmy`;
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle} />
+ <div class="row">
+ <div class="col-12 offset-lg-3 col-lg-6">
+ <h3>{i18n.t('lemmy_instance_setup')}</h3>
+ {!this.state.doneRegisteringUser ? (
+ this.registerUser()
+ ) : (
+ <SiteForm />
+ )}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ registerUser() {
+ return (
+ <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
+ <h5>{i18n.t('setup_admin')}</h5>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label" htmlFor="username">
+ {i18n.t('username')}
+ </label>
+ <div class="col-sm-10">
+ <input
+ type="text"
+ class="form-control"
+ id="username"
+ value={this.state.userForm.username}
+ onInput={linkEvent(this, this.handleRegisterUsernameChange)}
+ required
+ minLength={3}
+ maxLength={20}
+ pattern="[a-zA-Z0-9_]+"
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label" htmlFor="email">
+ {i18n.t('email')}
+ </label>
+
+ <div class="col-sm-10">
+ <input
+ type="email"
+ id="email"
+ class="form-control"
+ placeholder={i18n.t('optional')}
+ value={this.state.userForm.email}
+ onInput={linkEvent(this, this.handleRegisterEmailChange)}
+ minLength={3}
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label" htmlFor="password">
+ {i18n.t('password')}
+ </label>
+ <div class="col-sm-10">
+ <input
+ type="password"
+ id="password"
+ value={this.state.userForm.password}
+ onInput={linkEvent(this, this.handleRegisterPasswordChange)}
+ class="form-control"
+ required
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label" htmlFor="verify-password">
+ {i18n.t('verify_password')}
+ </label>
+ <div class="col-sm-10">
+ <input
+ type="password"
+ id="verify-password"
+ value={this.state.userForm.password_verify}
+ onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
+ class="form-control"
+ required
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <button type="submit" class="btn btn-secondary">
+ {this.state.userLoading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ i18n.t('sign_up')
+ )}
+ </button>
+ </div>
+ </div>
+ </form>
+ );
+ }
+
+ handleRegisterSubmit(i: Setup, event: any) {
+ event.preventDefault();
+ i.state.userLoading = true;
+ i.setState(i.state);
+ event.preventDefault();
+ WebSocketService.Instance.register(i.state.userForm);
+ }
+
+ handleRegisterUsernameChange(i: Setup, event: any) {
+ i.state.userForm.username = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleRegisterEmailChange(i: Setup, event: any) {
+ i.state.userForm.email = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleRegisterPasswordChange(i: Setup, event: any) {
+ i.state.userForm.password = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleRegisterPasswordVerifyChange(i: Setup, event: any) {
+ i.state.userForm.password_verify = event.target.value;
+ i.setState(i.state);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ this.state.userLoading = false;
+ this.setState(this.state);
+ return;
+ } else if (res.op == UserOperation.Register) {
+ let data = res.data as LoginResponse;
+ this.state.userLoading = false;
+ this.state.doneRegisteringUser = true;
+ UserService.Instance.login(data);
+ this.setState(this.state);
+ } else if (res.op == UserOperation.CreateSite) {
+ this.props.history.push('/');
+ }
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import {
+ Community,
+ CommunityUser,
+ FollowCommunityForm,
+ DeleteCommunityForm,
+ RemoveCommunityForm,
+ UserView,
+ AddModToCommunityForm,
+} from 'lemmy-js-client';
+import { WebSocketService, UserService } from '../services';
+import { mdToHtml, getUnixTime } from '../utils';
+import { CommunityForm } from './community-form';
+import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
+import { BannerIconHeader } from './banner-icon-header';
+import { i18n } from '../i18next';
+
+interface SidebarProps {
+ community: Community;
+ moderators: CommunityUser[];
+ admins: UserView[];
+ online: number;
+ enableNsfw: boolean;
+ showIcon?: boolean;
+}
+
+interface SidebarState {
+ showEdit: boolean;
+ showRemoveDialog: boolean;
+ removeReason: string;
+ removeExpires: string;
+ showConfirmLeaveModTeam: boolean;
+}
+
+export class Sidebar extends Component<SidebarProps, SidebarState> {
+ private emptyState: SidebarState = {
+ showEdit: false,
+ showRemoveDialog: false,
+ removeReason: null,
+ removeExpires: null,
+ showConfirmLeaveModTeam: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.state = this.emptyState;
+ this.handleEditCommunity = this.handleEditCommunity.bind(this);
+ this.handleEditCancel = this.handleEditCancel.bind(this);
+ }
+
+ render() {
+ return (
+ <div>
+ {!this.state.showEdit ? (
+ this.sidebar()
+ ) : (
+ <CommunityForm
+ community={this.props.community}
+ onEdit={this.handleEditCommunity}
+ onCancel={this.handleEditCancel}
+ enableNsfw={this.props.enableNsfw}
+ />
+ )}
+ </div>
+ );
+ }
+
+ sidebar() {
+ return (
+ <div>
+ <div class="card bg-transparent border-secondary mb-3">
+ <div class="card-header bg-transparent border-secondary">
+ {this.communityTitle()}
+ {this.adminButtons()}
+ </div>
+ <div class="card-body">{this.subscribes()}</div>
+ </div>
+ <div class="card bg-transparent border-secondary mb-3">
+ <div class="card-body">
+ {this.description()}
+ {this.badges()}
+ {this.mods()}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ communityTitle() {
+ let community = this.props.community;
+ return (
+ <div>
+ <h5 className="mb-0">
+ {this.props.showIcon && (
+ <BannerIconHeader icon={community.icon} banner={community.banner} />
+ )}
+ <span>{community.title}</span>
+ {community.removed && (
+ <small className="ml-2 text-muted font-italic">
+ {i18n.t('removed')}
+ </small>
+ )}
+ {community.deleted && (
+ <small className="ml-2 text-muted font-italic">
+ {i18n.t('deleted')}
+ </small>
+ )}
+ {community.nsfw && (
+ <small className="ml-2 text-muted font-italic">
+ {i18n.t('nsfw')}
+ </small>
+ )}
+ </h5>
+ <CommunityLink
+ community={community}
+ realLink
+ useApubName
+ muted
+ hideAvatar
+ />
+ </div>
+ );
+ }
+
+ badges() {
+ let community = this.props.community;
+ return (
+ <ul class="my-1 list-inline">
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_online', { count: this.props.online })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_subscribers', {
+ count: community.number_of_subscribers,
+ })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_posts', {
+ count: community.number_of_posts,
+ })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_comments', {
+ count: community.number_of_comments,
+ })}
+ </li>
+ <li className="list-inline-item">
+ <Link className="badge badge-light" to="/communities">
+ {community.category_name}
+ </Link>
+ </li>
+ <li className="list-inline-item">
+ <Link
+ className="badge badge-light"
+ to={`/modlog/community/${this.props.community.id}`}
+ >
+ {i18n.t('modlog')}
+ </Link>
+ </li>
+ <li className="list-inline-item badge badge-light">
+ <CommunityLink community={community} realLink />
+ </li>
+ </ul>
+ );
+ }
+
+ mods() {
+ return (
+ <ul class="list-inline small">
+ <li class="list-inline-item">{i18n.t('mods')}: </li>
+ {this.props.moderators.map(mod => (
+ <li class="list-inline-item">
+ <UserListing
+ user={{
+ name: mod.user_name,
+ preferred_username: mod.user_preferred_username,
+ avatar: mod.avatar,
+ id: mod.user_id,
+ local: mod.user_local,
+ actor_id: mod.user_actor_id,
+ }}
+ />
+ </li>
+ ))}
+ </ul>
+ );
+ }
+
+ subscribes() {
+ let community = this.props.community;
+ return (
+ <div class="d-flex flex-wrap">
+ <Link
+ class={`btn btn-secondary flex-fill mr-2 mb-2 ${
+ community.deleted || community.removed ? 'no-click' : ''
+ }`}
+ to={`/create_post?community=${community.name}`}
+ >
+ {i18n.t('create_a_post')}
+ </Link>
+ {community.subscribed ? (
+ <a
+ class="btn btn-secondary flex-fill mb-2"
+ href="#"
+ onClick={linkEvent(community.id, this.handleUnsubscribe)}
+ >
+ {i18n.t('unsubscribe')}
+ </a>
+ ) : (
+ <a
+ class="btn btn-secondary flex-fill mb-2"
+ href="#"
+ onClick={linkEvent(community.id, this.handleSubscribe)}
+ >
+ {i18n.t('subscribe')}
+ </a>
+ )}
+ </div>
+ );
+ }
+
+ description() {
+ let community = this.props.community;
+ return (
+ community.description && (
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(community.description)}
+ />
+ )
+ );
+ }
+
+ adminButtons() {
+ let community = this.props.community;
+ return (
+ <>
+ <ul class="list-inline mb-1 text-muted font-weight-bold">
+ {this.canMod && (
+ <>
+ <li className="list-inline-item-action">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleEditClick)}
+ data-tippy-content={i18n.t('edit')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-edit"></use>
+ </svg>
+ </span>
+ </li>
+ {!this.amCreator &&
+ (!this.state.showConfirmLeaveModTeam ? (
+ <li className="list-inline-item-action">
+ <span
+ class="pointer"
+ onClick={linkEvent(
+ this,
+ this.handleShowConfirmLeaveModTeamClick
+ )}
+ >
+ {i18n.t('leave_mod_team')}
+ </span>
+ </li>
+ ) : (
+ <>
+ <li className="list-inline-item-action">
+ {i18n.t('are_you_sure')}
+ </li>
+ <li className="list-inline-item-action">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleLeaveModTeamClick)}
+ >
+ {i18n.t('yes')}
+ </span>
+ </li>
+ <li className="list-inline-item-action">
+ <span
+ class="pointer"
+ onClick={linkEvent(
+ this,
+ this.handleCancelLeaveModTeamClick
+ )}
+ >
+ {i18n.t('no')}
+ </span>
+ </li>
+ </>
+ ))}
+ {this.amCreator && (
+ <li className="list-inline-item-action">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleDeleteClick)}
+ data-tippy-content={
+ !community.deleted ? i18n.t('delete') : i18n.t('restore')
+ }
+ >
+ <svg
+ class={`icon icon-inline ${
+ community.deleted && 'text-danger'
+ }`}
+ >
+ <use xlinkHref="#icon-trash"></use>
+ </svg>
+ </span>
+ </li>
+ )}
+ </>
+ )}
+ {this.canAdmin && (
+ <li className="list-inline-item">
+ {!this.props.community.removed ? (
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleModRemoveShow)}
+ >
+ {i18n.t('remove')}
+ </span>
+ ) : (
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleModRemoveSubmit)}
+ >
+ {i18n.t('restore')}
+ </span>
+ )}
+ </li>
+ )}
+ </ul>
+ {this.state.showRemoveDialog && (
+ <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
+ <div class="form-group row">
+ <label class="col-form-label" htmlFor="remove-reason">
+ {i18n.t('reason')}
+ </label>
+ <input
+ type="text"
+ id="remove-reason"
+ class="form-control mr-2"
+ placeholder={i18n.t('optional')}
+ value={this.state.removeReason}
+ onInput={linkEvent(this, this.handleModRemoveReasonChange)}
+ />
+ </div>
+ {/* TODO hold off on expires for now */}
+ {/* <div class="form-group row"> */}
+ {/* <label class="col-form-label">Expires</label> */}
+ {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
+ {/* </div> */}
+ <div class="form-group row">
+ <button type="submit" class="btn btn-secondary">
+ {i18n.t('remove_community')}
+ </button>
+ </div>
+ </form>
+ )}
+ </>
+ );
+ }
+
+ handleEditClick(i: Sidebar) {
+ i.state.showEdit = true;
+ i.setState(i.state);
+ }
+
+ handleEditCommunity() {
+ this.state.showEdit = false;
+ this.setState(this.state);
+ }
+
+ handleEditCancel() {
+ this.state.showEdit = false;
+ this.setState(this.state);
+ }
+
+ handleDeleteClick(i: Sidebar) {
+ event.preventDefault();
+ let deleteForm: DeleteCommunityForm = {
+ edit_id: i.props.community.id,
+ deleted: !i.props.community.deleted,
+ };
+ WebSocketService.Instance.deleteCommunity(deleteForm);
+ }
+
+ handleShowConfirmLeaveModTeamClick(i: Sidebar) {
+ i.state.showConfirmLeaveModTeam = true;
+ i.setState(i.state);
+ }
+
+ handleLeaveModTeamClick(i: Sidebar) {
+ let form: AddModToCommunityForm = {
+ user_id: UserService.Instance.user.id,
+ community_id: i.props.community.id,
+ added: false,
+ };
+ WebSocketService.Instance.addModToCommunity(form);
+ i.state.showConfirmLeaveModTeam = false;
+ i.setState(i.state);
+ }
+
+ handleCancelLeaveModTeamClick(i: Sidebar) {
+ i.state.showConfirmLeaveModTeam = false;
+ i.setState(i.state);
+ }
+
+ handleUnsubscribe(communityId: number) {
+ event.preventDefault();
+ let form: FollowCommunityForm = {
+ community_id: communityId,
+ follow: false,
+ };
+ WebSocketService.Instance.followCommunity(form);
+ }
+
+ handleSubscribe(communityId: number) {
+ event.preventDefault();
+ let form: FollowCommunityForm = {
+ community_id: communityId,
+ follow: true,
+ };
+ WebSocketService.Instance.followCommunity(form);
+ }
+
+ private get amCreator(): boolean {
+ return this.props.community.creator_id == UserService.Instance.user.id;
+ }
+
+ get canMod(): boolean {
+ return (
+ UserService.Instance.user &&
+ this.props.moderators
+ .map(m => m.user_id)
+ .includes(UserService.Instance.user.id)
+ );
+ }
+
+ get canAdmin(): boolean {
+ return (
+ UserService.Instance.user &&
+ this.props.admins.map(a => a.id).includes(UserService.Instance.user.id)
+ );
+ }
+
+ handleModRemoveShow(i: Sidebar) {
+ i.state.showRemoveDialog = true;
+ i.setState(i.state);
+ }
+
+ handleModRemoveReasonChange(i: Sidebar, event: any) {
+ i.state.removeReason = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleModRemoveExpiresChange(i: Sidebar, event: any) {
+ console.log(event.target.value);
+ i.state.removeExpires = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleModRemoveSubmit(i: Sidebar) {
+ event.preventDefault();
+ let removeForm: RemoveCommunityForm = {
+ edit_id: i.props.community.id,
+ removed: !i.props.community.removed,
+ reason: i.state.removeReason,
+ expires: getUnixTime(i.state.removeExpires),
+ };
+ WebSocketService.Instance.removeCommunity(removeForm);
+
+ i.state.showRemoveDialog = false;
+ i.setState(i.state);
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Prompt } from 'inferno-router';
+import { MarkdownTextArea } from './markdown-textarea';
+import { ImageUploadForm } from './image-upload-form';
+import { Site, SiteForm as SiteFormI } from 'lemmy-js-client';
+import { WebSocketService } from '../services';
+import { capitalizeFirstLetter, randomStr } from '../utils';
+import { i18n } from '../i18next';
+
+interface SiteFormProps {
+ site?: Site; // If a site is given, that means this is an edit
+ onCancel?(): any;
+}
+
+interface SiteFormState {
+ siteForm: SiteFormI;
+ loading: boolean;
+}
+
+export class SiteForm extends Component<SiteFormProps, SiteFormState> {
+ private id = `site-form-${randomStr()}`;
+ private emptyState: SiteFormState = {
+ siteForm: {
+ enable_downvotes: true,
+ open_registration: true,
+ enable_nsfw: true,
+ name: null,
+ icon: null,
+ banner: null,
+ },
+ loading: false,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handleSiteDescriptionChange = this.handleSiteDescriptionChange.bind(
+ this
+ );
+
+ this.handleIconUpload = this.handleIconUpload.bind(this);
+ this.handleIconRemove = this.handleIconRemove.bind(this);
+
+ this.handleBannerUpload = this.handleBannerUpload.bind(this);
+ this.handleBannerRemove = this.handleBannerRemove.bind(this);
+
+ if (this.props.site) {
+ this.state.siteForm = {
+ name: this.props.site.name,
+ description: this.props.site.description,
+ enable_downvotes: this.props.site.enable_downvotes,
+ open_registration: this.props.site.open_registration,
+ enable_nsfw: this.props.site.enable_nsfw,
+ icon: this.props.site.icon,
+ banner: this.props.site.banner,
+ };
+ }
+ }
+
+ // Necessary to stop the loading
+ componentWillReceiveProps() {
+ this.state.loading = false;
+ this.setState(this.state);
+ }
+
+ componentDidUpdate() {
+ if (
+ !this.state.loading &&
+ !this.props.site &&
+ (this.state.siteForm.name || this.state.siteForm.description)
+ ) {
+ window.onbeforeunload = () => true;
+ } else {
+ window.onbeforeunload = undefined;
+ }
+ }
+
+ componentWillUnmount() {
+ window.onbeforeunload = null;
+ }
+
+ render() {
+ return (
+ <>
+ <Prompt
+ when={
+ !this.state.loading &&
+ !this.props.site &&
+ (this.state.siteForm.name || this.state.siteForm.description)
+ }
+ message={i18n.t('block_leaving')}
+ />
+ <form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
+ <h5>{`${
+ this.props.site
+ ? capitalizeFirstLetter(i18n.t('save'))
+ : capitalizeFirstLetter(i18n.t('name'))
+ } ${i18n.t('your_site')}`}</h5>
+ <div class="form-group row">
+ <label class="col-12 col-form-label" htmlFor="create-site-name">
+ {i18n.t('name')}
+ </label>
+ <div class="col-12">
+ <input
+ type="text"
+ id="create-site-name"
+ class="form-control"
+ value={this.state.siteForm.name}
+ onInput={linkEvent(this, this.handleSiteNameChange)}
+ required
+ minLength={3}
+ maxLength={20}
+ />
+ </div>
+ </div>
+ <div class="form-group">
+ <label>{i18n.t('icon')}</label>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_icon')}
+ imageSrc={this.state.siteForm.icon}
+ onUpload={this.handleIconUpload}
+ onRemove={this.handleIconRemove}
+ rounded
+ />
+ </div>
+ <div class="form-group">
+ <label>{i18n.t('banner')}</label>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_banner')}
+ imageSrc={this.state.siteForm.banner}
+ onUpload={this.handleBannerUpload}
+ onRemove={this.handleBannerRemove}
+ />
+ </div>
+ <div class="form-group row">
+ <label class="col-12 col-form-label" htmlFor={this.id}>
+ {i18n.t('sidebar')}
+ </label>
+ <div class="col-12">
+ <MarkdownTextArea
+ initialContent={this.state.siteForm.description}
+ onContentChange={this.handleSiteDescriptionChange}
+ hideNavigationWarnings
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-12">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="create-site-downvotes"
+ type="checkbox"
+ checked={this.state.siteForm.enable_downvotes}
+ onChange={linkEvent(
+ this,
+ this.handleSiteEnableDownvotesChange
+ )}
+ />
+ <label class="form-check-label" htmlFor="create-site-downvotes">
+ {i18n.t('enable_downvotes')}
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-12">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="create-site-enable-nsfw"
+ type="checkbox"
+ checked={this.state.siteForm.enable_nsfw}
+ onChange={linkEvent(this, this.handleSiteEnableNsfwChange)}
+ />
+ <label
+ class="form-check-label"
+ htmlFor="create-site-enable-nsfw"
+ >
+ {i18n.t('enable_nsfw')}
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-12">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="create-site-open-registration"
+ type="checkbox"
+ checked={this.state.siteForm.open_registration}
+ onChange={linkEvent(
+ this,
+ this.handleSiteOpenRegistrationChange
+ )}
+ />
+ <label
+ class="form-check-label"
+ htmlFor="create-site-open-registration"
+ >
+ {i18n.t('open_registration')}
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-12">
+ <button
+ type="submit"
+ class="btn btn-secondary mr-2"
+ disabled={this.state.loading}
+ >
+ {this.state.loading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : this.props.site ? (
+ capitalizeFirstLetter(i18n.t('save'))
+ ) : (
+ capitalizeFirstLetter(i18n.t('create'))
+ )}
+ </button>
+ {this.props.site && (
+ <button
+ type="button"
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.handleCancel)}
+ >
+ {i18n.t('cancel')}
+ </button>
+ )}
+ </div>
+ </div>
+ </form>
+ </>
+ );
+ }
+
+ handleCreateSiteSubmit(i: SiteForm, event: any) {
+ event.preventDefault();
+ i.state.loading = true;
+ if (i.props.site) {
+ WebSocketService.Instance.editSite(i.state.siteForm);
+ } else {
+ WebSocketService.Instance.createSite(i.state.siteForm);
+ }
+ i.setState(i.state);
+ }
+
+ handleSiteNameChange(i: SiteForm, event: any) {
+ i.state.siteForm.name = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleSiteDescriptionChange(val: string) {
+ this.state.siteForm.description = val;
+ this.setState(this.state);
+ }
+
+ handleSiteEnableNsfwChange(i: SiteForm, event: any) {
+ i.state.siteForm.enable_nsfw = event.target.checked;
+ i.setState(i.state);
+ }
+
+ handleSiteOpenRegistrationChange(i: SiteForm, event: any) {
+ i.state.siteForm.open_registration = event.target.checked;
+ i.setState(i.state);
+ }
+
+ handleSiteEnableDownvotesChange(i: SiteForm, event: any) {
+ i.state.siteForm.enable_downvotes = event.target.checked;
+ i.setState(i.state);
+ }
+
+ handleCancel(i: SiteForm) {
+ i.props.onCancel();
+ }
+
+ handleIconUpload(url: string) {
+ this.state.siteForm.icon = url;
+ this.setState(this.state);
+ }
+
+ handleIconRemove() {
+ this.state.siteForm.icon = '';
+ this.setState(this.state);
+ }
+
+ handleBannerUpload(url: string) {
+ this.state.siteForm.banner = url;
+ this.setState(this.state);
+ }
+
+ handleBannerRemove() {
+ this.state.siteForm.banner = '';
+ this.setState(this.state);
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { SortType } from 'lemmy-js-client';
+import { sortingHelpUrl, randomStr } from '../utils';
+import { i18n } from '../i18next';
+
+interface SortSelectProps {
+ sort: SortType;
+ onChange?(val: SortType): any;
+ hideHot?: boolean;
+}
+
+interface SortSelectState {
+ sort: SortType;
+}
+
+export class SortSelect extends Component<SortSelectProps, SortSelectState> {
+ private id = `sort-select-${randomStr()}`;
+ private emptyState: SortSelectState = {
+ sort: this.props.sort,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.state = this.emptyState;
+ }
+
+ static getDerivedStateFromProps(props: any): SortSelectState {
+ return {
+ sort: props.sort,
+ };
+ }
+
+ render() {
+ return (
+ <>
+ <select
+ id={this.id}
+ name={this.id}
+ value={this.state.sort}
+ onChange={linkEvent(this, this.handleSortChange)}
+ class="custom-select w-auto mr-2 mb-2"
+ >
+ <option disabled>{i18n.t('sort_type')}</option>
+ {!this.props.hideHot && (
+ <>
+ <option value={SortType.Active}>{i18n.t('active')}</option>
+ <option value={SortType.Hot}>{i18n.t('hot')}</option>
+ </>
+ )}
+ <option value={SortType.New}>{i18n.t('new')}</option>
+ <option disabled>─────</option>
+ <option value={SortType.TopDay}>{i18n.t('top_day')}</option>
+ <option value={SortType.TopWeek}>{i18n.t('week')}</option>
+ <option value={SortType.TopMonth}>{i18n.t('month')}</option>
+ <option value={SortType.TopYear}>{i18n.t('year')}</option>
+ <option value={SortType.TopAll}>{i18n.t('all')}</option>
+ </select>
+ <a
+ className="text-muted"
+ href={sortingHelpUrl}
+ target="_blank"
+ rel="noopener"
+ title={i18n.t('sorting_help')}
+ >
+ <svg class={`icon icon-inline`}>
+ <use xlinkHref="#icon-help-circle"></use>
+ </svg>
+ </a>
+ </>
+ );
+ }
+
+ handleSortChange(i: SortSelect, event: any) {
+ i.props.onChange(event.target.value);
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { WebSocketService } from '../services';
+import {
+ GetSiteResponse,
+ Site,
+ WebSocketJsonResponse,
+ UserOperation,
+} from 'lemmy-js-client';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
+import { repoUrl, wsJsonToRes, toast } from '../utils';
+
+interface SilverUser {
+ name: string;
+ link?: string;
+}
+
+let general = [
+ 'Brendan',
+ 'mexicanhalloween',
+ 'William Moore',
+ 'Rachel Schmitz',
+ 'comradeda',
+ 'ybaumy',
+ 'dude in phx',
+ 'twilight loki',
+ 'Andrew Plaza',
+ 'Jonathan Cremin',
+ 'Arthur Nieuwland',
+ 'Ernest Wiśniewski',
+ 'HN',
+ 'Forrest Weghorst',
+ 'Andre Vallestero',
+ 'NotTooHighToHack',
+];
+let highlighted = ['DQW', 'DiscountFuneral', 'Oskenso Kashi', 'Alex Benishek'];
+let silver: SilverUser[] = [
+ {
+ name: 'Redjoker',
+ link: 'https://iww.org',
+ },
+];
+// let gold = [];
+// let latinum = [];
+
+interface SponsorsState {
+ site: Site;
+}
+
+export class Sponsors extends Component<any, SponsorsState> {
+ private subscription: Subscription;
+ private emptyState: SponsorsState = {
+ site: undefined,
+ };
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.state = this.emptyState;
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ WebSocketService.Instance.getSite();
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ get documentTitle(): string {
+ if (this.state.site) {
+ return `${i18n.t('sponsors')} - ${this.state.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ render() {
+ return (
+ <div class="container text-center">
+ <Helmet title={this.documentTitle} />
+ {this.topMessage()}
+ <hr />
+ {this.sponsors()}
+ <hr />
+ {this.bitcoin()}
+ </div>
+ );
+ }
+
+ topMessage() {
+ return (
+ <div>
+ <h5>{i18n.t('donate_to_lemmy')}</h5>
+ <p>
+ <T i18nKey="sponsor_message">
+ #<a href={repoUrl}>#</a>
+ </T>
+ </p>
+ <a class="btn btn-secondary" href="https://liberapay.com/Lemmy/">
+ {i18n.t('support_on_liberapay')}
+ </a>
+ <a
+ class="btn btn-secondary ml-2"
+ href="https://www.patreon.com/dessalines"
+ >
+ {i18n.t('support_on_patreon')}
+ </a>
+ <a
+ class="btn btn-secondary ml-2"
+ href="https://opencollective.com/lemmy"
+ >
+ {i18n.t('support_on_open_collective')}
+ </a>
+ </div>
+ );
+ }
+ sponsors() {
+ return (
+ <div class="container">
+ <h5>{i18n.t('sponsors')}</h5>
+ <p>{i18n.t('silver_sponsors')}</p>
+ <div class="row justify-content-md-center card-columns">
+ {silver.map(s => (
+ <div class="card col-12 col-md-2">
+ <div>
+ {s.link ? (
+ <a href={s.link} target="_blank" rel="noopener">
+ 💎 {s.name}
+ </a>
+ ) : (
+ <div>💎 {s.name}</div>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ <p>{i18n.t('general_sponsors')}</p>
+ <div class="row justify-content-md-center card-columns">
+ {highlighted.map(s => (
+ <div class="card bg-primary col-12 col-md-2 font-weight-bold">
+ <div>{s}</div>
+ </div>
+ ))}
+ {general.map(s => (
+ <div class="card col-12 col-md-2">
+ <div>{s}</div>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+ }
+
+ bitcoin() {
+ return (
+ <div>
+ <h5>{i18n.t('crypto')}</h5>
+ <div class="table-responsive">
+ <table class="table table-hover text-center">
+ <tbody>
+ <tr>
+ <td>{i18n.t('bitcoin')}</td>
+ <td>
+ <code>1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK</code>
+ </td>
+ </tr>
+ <tr>
+ <td>{i18n.t('ethereum')}</td>
+ <td>
+ <code>0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01</code>
+ </td>
+ </tr>
+ <tr>
+ <td>{i18n.t('monero')}</td>
+ <td>
+ <code>
+ 41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV
+ </code>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ );
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ let res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ return;
+ } else if (res.op == UserOperation.GetSite) {
+ let data = res.data as GetSiteResponse;
+ this.state.site = data.site;
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+
+export class Symbols extends Component<any, any> {
+ constructor(props: any, context: any) {
+ super(props, context);
+ }
+
+ render() {
+ return (
+ <svg
+ aria-hidden="true"
+ style="position: absolute; width: 0; height: 0; overflow: hidden;"
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlnsXlink="http://www.w3.org/1999/xlink"
+ >
+ <defs>
+ <symbol id="icon-x" viewBox="0 0 24 24">
+ <path d="M5.293 6.707l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l5.293-5.293 5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-5.293 5.293-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"></path>
+ </symbol>
+ <symbol id="icon-refresh-cw" viewBox="0 0 24 24">
+ <path d="M4.453 9.334c0.737-2.083 2.247-3.669 4.096-4.552s4.032-1.059 6.114-0.322c1.186 0.42 2.206 1.088 2.983 1.88l2.83 2.66h-3.476c-0.552 0-1 0.448-1 1s0.448 1 1 1h5.997c0.005 0 0.009 0 0.014 0 0.137-0.001 0.268-0.031 0.386-0.082 0.119-0.051 0.229-0.126 0.324-0.225 0.012-0.013 0.024-0.026 0.036-0.039 0.075-0.087 0.133-0.183 0.173-0.285s0.064-0.211 0.069-0.326c0.001-0.015 0.001-0.029 0.001-0.043v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1v3.689l-2.926-2.749c-0.992-1.010-2.271-1.843-3.743-2.364-2.603-0.921-5.335-0.699-7.643 0.402s-4.199 3.086-5.12 5.689c-0.185 0.52 0.088 1.091 0.608 1.276s1.092-0.088 1.276-0.609zM2 16.312l2.955 2.777c1.929 1.931 4.49 2.908 7.048 2.909s5.119-0.975 7.072-2.927c1.104-1.104 1.901-2.407 2.361-3.745 0.18-0.522-0.098-1.091-0.621-1.271s-1.091 0.098-1.271 0.621c-0.361 1.050-0.993 2.091-1.883 2.981-1.563 1.562-3.609 2.342-5.657 2.342s-4.094-0.782-5.679-2.366l-2.8-2.633h3.475c0.552 0 1-0.448 1-1s-0.448-1-1-1h-5.997c-0.005 0-0.009 0-0.014 0-0.137 0.001-0.268 0.031-0.386 0.082-0.119 0.051-0.229 0.126-0.324 0.225-0.012 0.013-0.024 0.026-0.036 0.039-0.075 0.087-0.133 0.183-0.173 0.285s-0.064 0.211-0.069 0.326c-0.001 0.015-0.001 0.029-0.001 0.043v6c0 0.552 0.448 1 1 1s1-0.448 1-1z"></path>
+ </symbol>
+ <symbol id="icon-play" viewBox="0 0 24 24">
+ <path d="M5.541 2.159c-0.153-0.1-0.34-0.159-0.541-0.159-0.552 0-1 0.448-1 1v18c-0.001 0.182 0.050 0.372 0.159 0.541 0.299 0.465 0.917 0.599 1.382 0.3l14-9c0.114-0.072 0.219-0.174 0.3-0.3 0.299-0.465 0.164-1.083-0.3-1.382zM6 4.832l11.151 7.168-11.151 7.168z"></path>
+ </symbol>
+ <symbol id="icon-strikethrough" viewBox="0 0 28 28">
+ <path d="M27.5 14c0.281 0 0.5 0.219 0.5 0.5v1c0 0.281-0.219 0.5-0.5 0.5h-27c-0.281 0-0.5-0.219-0.5-0.5v-1c0-0.281 0.219-0.5 0.5-0.5h27zM7.547 13c-0.297-0.375-0.562-0.797-0.797-1.25-0.5-1.016-0.75-2-0.75-2.938 0-1.906 0.703-3.5 2.094-4.828s3.437-1.984 6.141-1.984c0.594 0 1.453 0.109 2.609 0.297 0.688 0.125 1.609 0.375 2.766 0.75 0.109 0.406 0.219 1.031 0.328 1.844 0.141 1.234 0.219 2.187 0.219 2.859 0 0.219-0.031 0.453-0.078 0.703l-0.187 0.047-1.313-0.094-0.219-0.031c-0.531-1.578-1.078-2.641-1.609-3.203-0.922-0.953-2.031-1.422-3.281-1.422-1.188 0-2.141 0.313-2.844 0.922s-1.047 1.375-1.047 2.281c0 0.766 0.344 1.484 1.031 2.188s2.141 1.375 4.359 2.016c0.75 0.219 1.641 0.562 2.703 1.031 0.562 0.266 1.062 0.531 1.484 0.812h-11.609zM15.469 17h6.422c0.078 0.438 0.109 0.922 0.109 1.437 0 1.125-0.203 2.234-0.641 3.313-0.234 0.578-0.594 1.109-1.109 1.625-0.375 0.359-0.938 0.781-1.703 1.266-0.781 0.469-1.563 0.828-2.391 1.031-0.828 0.219-1.875 0.328-3.172 0.328-0.859 0-1.891-0.031-3.047-0.359l-2.188-0.625c-0.609-0.172-0.969-0.313-1.125-0.438-0.063-0.063-0.125-0.172-0.125-0.344v-0.203c0-0.125 0.031-0.938-0.031-2.438-0.031-0.781 0.031-1.328 0.031-1.641v-0.688l1.594-0.031c0.578 1.328 0.844 2.125 1.016 2.406 0.375 0.609 0.797 1.094 1.25 1.469s1 0.672 1.641 0.891c0.625 0.234 1.328 0.344 2.063 0.344 0.656 0 1.391-0.141 2.172-0.422 0.797-0.266 1.437-0.719 1.906-1.344 0.484-0.625 0.734-1.297 0.734-2.016 0-0.875-0.422-1.687-1.266-2.453-0.344-0.297-1.062-0.672-2.141-1.109z"></path>
+ </symbol>
+ <symbol id="icon-header" viewBox="0 0 28 28">
+ <path d="M26.281 26c-1.375 0-2.766-0.109-4.156-0.109-1.375 0-2.75 0.109-4.125 0.109-0.531 0-0.781-0.578-0.781-1.031 0-1.391 1.563-0.797 2.375-1.328 0.516-0.328 0.516-1.641 0.516-2.188l-0.016-6.109c0-0.172 0-0.328-0.016-0.484-0.25-0.078-0.531-0.063-0.781-0.063h-10.547c-0.266 0-0.547-0.016-0.797 0.063-0.016 0.156-0.016 0.313-0.016 0.484l-0.016 5.797c0 0.594 0 2.219 0.578 2.562 0.812 0.5 2.656-0.203 2.656 1.203 0 0.469-0.219 1.094-0.766 1.094-1.453 0-2.906-0.109-4.344-0.109-1.328 0-2.656 0.109-3.984 0.109-0.516 0-0.75-0.594-0.75-1.031 0-1.359 1.437-0.797 2.203-1.328 0.5-0.344 0.516-1.687 0.516-2.234l-0.016-0.891v-12.703c0-0.75 0.109-3.156-0.594-3.578-0.781-0.484-2.453 0.266-2.453-1.141 0-0.453 0.203-1.094 0.75-1.094 1.437 0 2.891 0.109 4.328 0.109 1.313 0 2.641-0.109 3.953-0.109 0.562 0 0.781 0.625 0.781 1.094 0 1.344-1.547 0.688-2.312 1.172-0.547 0.328-0.547 1.937-0.547 2.5l0.016 5c0 0.172 0 0.328 0.016 0.5 0.203 0.047 0.406 0.047 0.609 0.047h10.922c0.187 0 0.391 0 0.594-0.047 0.016-0.172 0.016-0.328 0.016-0.5l0.016-5c0-0.578 0-2.172-0.547-2.5-0.781-0.469-2.344 0.156-2.344-1.172 0-0.469 0.219-1.094 0.781-1.094 1.375 0 2.75 0.109 4.125 0.109 1.344 0 2.688-0.109 4.031-0.109 0.562 0 0.781 0.625 0.781 1.094 0 1.359-1.609 0.672-2.391 1.156-0.531 0.344-0.547 1.953-0.547 2.516l0.016 14.734c0 0.516 0.031 1.875 0.531 2.188 0.797 0.5 2.484-0.141 2.484 1.219 0 0.453-0.203 1.094-0.75 1.094z"></path>
+ </symbol>
+ <symbol id="icon-list" viewBox="0 0 24 24">
+ <path d="M8 7h13c0.552 0 1-0.448 1-1s-0.448-1-1-1h-13c-0.552 0-1 0.448-1 1s0.448 1 1 1zM8 13h13c0.552 0 1-0.448 1-1s-0.448-1-1-1h-13c-0.552 0-1 0.448-1 1s0.448 1 1 1zM8 19h13c0.552 0 1-0.448 1-1s-0.448-1-1-1h-13c-0.552 0-1 0.448-1 1s0.448 1 1 1zM3 7c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1zM3 13c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1zM3 19c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1z"></path>
+ </symbol>
+ <symbol id="icon-italic" viewBox="0 0 24 24">
+ <path d="M13.557 5l-5.25 14h-3.307c-0.552 0-1 0.448-1 1s0.448 1 1 1h9c0.552 0 1-0.448 1-1s-0.448-1-1-1h-3.557l5.25-14h3.307c0.552 0 1-0.448 1-1s-0.448-1-1-1h-9c-0.552 0-1 0.448-1 1s0.448 1 1 1z"></path>
+ </symbol>
+ <symbol id="icon-code" viewBox="0 0 24 24">
+ <path d="M16.707 18.707l6-6c0.391-0.391 0.391-1.024 0-1.414l-6-6c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0zM7.293 5.293l-6 6c-0.391 0.391-0.391 1.024 0 1.414l6 6c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0z"></path>
+ </symbol>
+ <symbol id="icon-bold" viewBox="0 0 24 24">
+ <path d="M7 11v-6h7c0.829 0 1.577 0.335 2.121 0.879s0.879 1.292 0.879 2.121-0.335 1.577-0.879 2.121-1.292 0.879-2.121 0.879zM5 12v8c0 0.552 0.448 1 1 1h9c1.38 0 2.632-0.561 3.536-1.464s1.464-2.156 1.464-3.536-0.561-2.632-1.464-3.536c-0.325-0.325-0.695-0.606-1.1-0.832 0.034-0.032 0.067-0.064 0.1-0.097 0.903-0.903 1.464-2.155 1.464-3.535s-0.561-2.632-1.464-3.536-2.156-1.464-3.536-1.464h-8c-0.552 0-1 0.448-1 1zM7 13h8c0.829 0 1.577 0.335 2.121 0.879s0.879 1.292 0.879 2.121-0.335 1.577-0.879 2.121-1.292 0.879-2.121 0.879h-8z"></path>
+ </symbol>
+ <symbol id="icon-format_quote" viewBox="0 0 24 24">
+ <path d="M14.016 17.016l1.969-4.031h-3v-6h6v6l-1.969 4.031h-3zM6 17.016l2.016-4.031h-3v-6h6v6l-2.016 4.031h-3z"></path>
+ </symbol>
+ <symbol id="icon-settings" viewBox="0 0 24 24">
+ <path d="M16 12c0-1.104-0.449-2.106-1.172-2.828s-1.724-1.172-2.828-1.172-2.106 0.449-2.828 1.172-1.172 1.724-1.172 2.828 0.449 2.106 1.172 2.828 1.724 1.172 2.828 1.172 2.106-0.449 2.828-1.172 1.172-1.724 1.172-2.828zM14 12c0 0.553-0.223 1.051-0.586 1.414s-0.861 0.586-1.414 0.586-1.051-0.223-1.414-0.586-0.586-0.861-0.586-1.414 0.223-1.051 0.586-1.414 0.861-0.586 1.414-0.586 1.051 0.223 1.414 0.586 0.586 0.861 0.586 1.414zM20.315 15.404c0.046-0.105 0.112-0.191 0.192-0.257 0.112-0.092 0.251-0.146 0.403-0.147h0.090c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121-0.337-1.58-0.879-2.121-1.293-0.879-2.121-0.879h-0.159c-0.11-0.001-0.215-0.028-0.308-0.076-0.127-0.066-0.23-0.172-0.292-0.312-0.003-0.029-0.004-0.059-0.004-0.089-0.024-0.055-0.040-0.111-0.049-0.168 0.020-0.334 0.077-0.454 0.168-0.547l0.062-0.062c0.585-0.586 0.878-1.356 0.877-2.122s-0.294-1.536-0.881-2.122c-0.586-0.585-1.356-0.878-2.122-0.877s-1.536 0.294-2.12 0.879l-0.046 0.046c-0.083 0.080-0.183 0.136-0.288 0.166-0.14 0.039-0.291 0.032-0.438-0.033-0.101-0.044-0.187-0.11-0.253-0.19-0.092-0.112-0.146-0.251-0.147-0.403v-0.090c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879-1.58 0.337-2.121 0.879-0.879 1.293-0.879 2.121v0.159c-0.001 0.11-0.028 0.215-0.076 0.308-0.066 0.127-0.172 0.23-0.312 0.292-0.029 0.003-0.059 0.004-0.089 0.004-0.055 0.024-0.111 0.040-0.168 0.049-0.335-0.021-0.455-0.078-0.548-0.169l-0.062-0.062c-0.586-0.585-1.355-0.878-2.122-0.878s-1.535 0.294-2.122 0.882c-0.585 0.586-0.878 1.355-0.878 2.122s0.294 1.536 0.879 2.12l0.048 0.047c0.080 0.083 0.136 0.183 0.166 0.288 0.039 0.14 0.032 0.291-0.031 0.434-0.006 0.016-0.013 0.034-0.021 0.052-0.041 0.109-0.108 0.203-0.191 0.275-0.11 0.095-0.25 0.153-0.383 0.156h-0.090c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.294-0.879 2.122 0.337 1.58 0.879 2.121 1.293 0.879 2.121 0.879h0.159c0.11 0.001 0.215 0.028 0.308 0.076 0.128 0.067 0.233 0.174 0.296 0.321 0.024 0.055 0.040 0.111 0.049 0.168-0.020 0.334-0.077 0.454-0.168 0.547l-0.062 0.062c-0.585 0.586-0.878 1.356-0.877 2.122s0.294 1.536 0.881 2.122c0.586 0.585 1.356 0.878 2.122 0.877s1.536-0.294 2.12-0.879l0.047-0.048c0.083-0.080 0.183-0.136 0.288-0.166 0.14-0.039 0.291-0.032 0.434 0.031 0.016 0.006 0.034 0.013 0.052 0.021 0.109 0.041 0.203 0.108 0.275 0.191 0.095 0.11 0.153 0.25 0.156 0.383v0.092c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879 1.58-0.337 2.121-0.879 0.879-1.293 0.879-2.121v-0.159c0.001-0.11 0.028-0.215 0.076-0.308 0.067-0.128 0.174-0.233 0.321-0.296 0.055-0.024 0.111-0.040 0.168-0.049 0.334 0.020 0.454 0.077 0.547 0.168l0.062 0.062c0.586 0.585 1.356 0.878 2.122 0.877s1.536-0.294 2.122-0.881c0.585-0.586 0.878-1.356 0.877-2.122s-0.294-1.536-0.879-2.12l-0.048-0.047c-0.080-0.083-0.136-0.183-0.166-0.288-0.039-0.14-0.032-0.291 0.031-0.434zM18.396 9.302c-0.012-0.201-0.038-0.297-0.076-0.382v0.080c0 0.043 0.003 0.084 0.008 0.125 0.021 0.060 0.043 0.119 0.068 0.177 0.004 0.090 0.005 0.091 0.005 0.092 0.249 0.581 0.684 1.030 1.208 1.303 0.371 0.193 0.785 0.298 1.211 0.303h0.18c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707-0.111 0.525-0.293 0.707-0.431 0.293-0.707 0.293h-0.090c-0.637 0.003-1.22 0.228-1.675 0.603-0.323 0.266-0.581 0.607-0.75 0.993-0.257 0.582-0.288 1.21-0.127 1.782 0.119 0.423 0.341 0.814 0.652 1.136l0.072 0.073c0.196 0.196 0.294 0.45 0.294 0.707s-0.097 0.512-0.292 0.707c-0.197 0.197-0.451 0.295-0.709 0.295s-0.512-0.097-0.707-0.292l-0.061-0.061c-0.463-0.453-1.040-0.702-1.632-0.752-0.437-0.037-0.882 0.034-1.293 0.212-0.578 0.248-1.027 0.683-1.3 1.206-0.193 0.371-0.298 0.785-0.303 1.211v0.181c0 0.276-0.111 0.525-0.293 0.707s-0.43 0.292-0.706 0.292-0.525-0.111-0.707-0.293-0.293-0.431-0.293-0.707v-0.090c-0.015-0.66-0.255-1.242-0.644-1.692-0.284-0.328-0.646-0.585-1.058-0.744-0.575-0.247-1.193-0.274-1.756-0.116-0.423 0.119-0.814 0.341-1.136 0.652l-0.073 0.072c-0.196 0.196-0.45 0.294-0.707 0.294s-0.512-0.097-0.707-0.292c-0.197-0.197-0.295-0.451-0.295-0.709s0.097-0.512 0.292-0.707l0.061-0.061c0.453-0.463 0.702-1.040 0.752-1.632 0.037-0.437-0.034-0.882-0.212-1.293-0.248-0.578-0.683-1.027-1.206-1.3-0.371-0.193-0.785-0.298-1.211-0.303l-0.18 0.001c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707 0.111-0.525 0.293-0.707 0.431-0.293 0.707-0.293h0.090c0.66-0.015 1.242-0.255 1.692-0.644 0.328-0.284 0.585-0.646 0.744-1.058 0.247-0.575 0.274-1.193 0.116-1.756-0.119-0.423-0.341-0.814-0.652-1.136l-0.073-0.073c-0.196-0.196-0.294-0.45-0.294-0.707s0.097-0.512 0.292-0.707c0.197-0.197 0.451-0.295 0.709-0.295s0.512 0.097 0.707 0.292l0.061 0.061c0.463 0.453 1.040 0.702 1.632 0.752 0.37 0.032 0.745-0.014 1.101-0.137 0.096-0.012 0.186-0.036 0.266-0.072-0.031 0.001-0.061 0.003-0.089 0.004-0.201 0.012-0.297 0.038-0.382 0.076h0.080c0.043 0 0.084-0.003 0.125-0.008 0.060-0.021 0.119-0.043 0.177-0.068 0.090-0.004 0.091-0.005 0.092-0.005 0.581-0.249 1.030-0.684 1.303-1.208 0.193-0.37 0.298-0.785 0.303-1.21v-0.181c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293 0.525 0.111 0.707 0.293 0.293 0.431 0.293 0.707v0.090c0.003 0.637 0.228 1.22 0.603 1.675 0.266 0.323 0.607 0.581 0.996 0.751 0.578 0.255 1.206 0.286 1.778 0.125 0.423-0.119 0.814-0.341 1.136-0.652l0.073-0.072c0.196-0.196 0.45-0.294 0.707-0.294s0.512 0.097 0.707 0.292c0.197 0.197 0.295 0.451 0.295 0.709s-0.097 0.512-0.292 0.707l-0.061 0.061c-0.453 0.463-0.702 1.040-0.752 1.632-0.032 0.37 0.014 0.745 0.137 1.101 0.012 0.095 0.037 0.185 0.072 0.266-0.001-0.032-0.002-0.062-0.004-0.089z"></path>
+ </symbol>
+ <symbol id="icon-smile" viewBox="0 0 24 24">
+ <path d="M23 12c0-3.037-1.232-5.789-3.222-7.778s-4.741-3.222-7.778-3.222-5.789 1.232-7.778 3.222-3.222 4.741-3.222 7.778 1.232 5.789 3.222 7.778 4.741 3.222 7.778 3.222 5.789-1.232 7.778-3.222 3.222-4.741 3.222-7.778zM21 12c0 2.486-1.006 4.734-2.636 6.364s-3.878 2.636-6.364 2.636-4.734-1.006-6.364-2.636-2.636-3.878-2.636-6.364 1.006-4.734 2.636-6.364 3.878-2.636 6.364-2.636 4.734 1.006 6.364 2.636 2.636 3.878 2.636 6.364zM7.2 14.6c0 0 0.131 0.173 0.331 0.383 0.145 0.153 0.338 0.341 0.577 0.54 0.337 0.281 0.772 0.59 1.297 0.853 0.705 0.352 1.579 0.624 2.595 0.624s1.89-0.272 2.595-0.624c0.525-0.263 0.96-0.572 1.297-0.853 0.239-0.199 0.432-0.387 0.577-0.54 0.2-0.21 0.331-0.383 0.331-0.383 0.331-0.442 0.242-1.069-0.2-1.4s-1.069-0.242-1.4 0.2c-0.041 0.050-0.181 0.206-0.181 0.206-0.1 0.105-0.237 0.239-0.408 0.382-0.243 0.203-0.549 0.419-0.91 0.6-0.48 0.239-1.050 0.412-1.701 0.412s-1.221-0.173-1.701-0.413c-0.36-0.18-0.667-0.397-0.91-0.6-0.171-0.143-0.308-0.277-0.408-0.382-0.14-0.155-0.181-0.205-0.181-0.205-0.331-0.442-0.958-0.531-1.4-0.2s-0.531 0.958-0.2 1.4zM9 10c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1zM15 10c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1z"></path>
+ </symbol>
+ <symbol id="icon-book-open" viewBox="0 0 24 24">
+ <path d="M21 4v13h-6c-0.728 0-1.412 0.195-2 0.535v-10.535c0-0.829 0.335-1.577 0.879-2.121s1.292-0.879 2.121-0.879zM11 17.535c-0.588-0.34-1.272-0.535-2-0.535h-6v-13h5c0.829 0 1.577 0.335 2.121 0.879s0.879 1.292 0.879 2.121zM22 2h-6c-1.38 0-2.632 0.561-3.536 1.464-0.167 0.167-0.322 0.346-0.464 0.536-0.142-0.19-0.297-0.369-0.464-0.536-0.904-0.903-2.156-1.464-3.536-1.464h-6c-0.552 0-1 0.448-1 1v15c0 0.552 0.448 1 1 1h7c0.553 0 1.051 0.223 1.414 0.586s0.586 0.861 0.586 1.414c0 0.552 0.448 1 1 1s1-0.448 1-1c0-0.553 0.223-1.051 0.586-1.414s0.861-0.586 1.414-0.586h7c0.552 0 1-0.448 1-1v-15c0-0.552-0.448-1-1-1z"></path>
+ </symbol>
+ <symbol id="icon-alert-triangle" viewBox="0 0 24 24">
+ <path d="M11.148 4.374c0.073-0.123 0.185-0.242 0.334-0.332 0.236-0.143 0.506-0.178 0.756-0.116s0.474 0.216 0.614 0.448l8.466 14.133c0.070 0.12 0.119 0.268 0.128 0.434-0.015 0.368-0.119 0.591-0.283 0.759-0.18 0.184-0.427 0.298-0.693 0.301l-16.937-0.001c-0.152-0.001-0.321-0.041-0.481-0.134-0.239-0.138-0.399-0.359-0.466-0.607s-0.038-0.519 0.092-0.745zM9.432 3.346l-8.47 14.14c-0.422 0.731-0.506 1.55-0.308 2.29s0.68 1.408 1.398 1.822c0.464 0.268 0.976 0.4 1.475 0.402h16.943c0.839-0.009 1.587-0.354 2.123-0.902s0.864-1.303 0.855-2.131c-0.006-0.536-0.153-1.044-0.406-1.474l-8.474-14.147c-0.432-0.713-1.11-1.181-1.854-1.363s-1.561-0.081-2.269 0.349c-0.429 0.26-0.775 0.615-1.012 1.014zM11 9v4c0 0.552 0.448 1 1 1s1-0.448 1-1v-4c0-0.552-0.448-1-1-1s-1 0.448-1 1zM12 18c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1z"></path>
+ </symbol>
+ <symbol id="icon-zap" viewBox="0 0 24 24">
+ <path d="M11.585 5.26l-0.577 4.616c0.033 0.716 0.465 1.124 0.992 1.124h6.865l-6.45 7.74 0.577-4.616c-0.033-0.716-0.465-1.124-0.992-1.124h-6.865zM12.232 1.36l-10 12c-0.354 0.424-0.296 1.055 0.128 1.408 0.187 0.157 0.415 0.233 0.64 0.232h7.867l-0.859 6.876c-0.069 0.548 0.32 1.048 0.868 1.116 0.349 0.044 0.678-0.098 0.892-0.352l10-12c0.354-0.424 0.296-1.055-0.128-1.408-0.187-0.157-0.415-0.233-0.64-0.232h-7.867l0.859-6.876c0.069-0.548-0.32-1.048-0.868-1.116-0.349-0.044-0.678 0.098-0.892 0.352z"></path>
+ </symbol>
+ <symbol id="icon-heart" viewBox="0 0 24 24">
+ <path d="M20.133 5.317c0.88 0.881 1.319 2.031 1.319 3.184s-0.44 2.303-1.319 3.182l-8.133 8.133-8.133-8.133c-0.879-0.879-1.318-2.029-1.318-3.183s0.439-2.304 1.318-3.183 2.029-1.318 3.183-1.318 2.304 0.439 3.183 1.318l1.060 1.060c0.391 0.391 1.024 0.391 1.414 0l1.062-1.062c0.879-0.879 2.029-1.318 3.182-1.317s2.303 0.44 3.182 1.319zM21.547 3.903c-1.269-1.269-2.934-1.904-4.596-1.905s-3.327 0.634-4.597 1.903l-0.354 0.355-0.353-0.353c-1.269-1.269-2.935-1.904-4.597-1.904s-3.328 0.635-4.597 1.904-1.904 2.935-1.904 4.597 0.635 3.328 1.904 4.597l8.84 8.84c0.391 0.391 1.024 0.391 1.414 0l8.84-8.84c1.269-1.269 1.904-2.934 1.905-4.596s-0.634-3.327-1.905-4.598z"></path>
+ </symbol>
+ <symbol id="icon-link" viewBox="0 0 24 24">
+ <path d="M9.199 13.599c0.992 1.327 2.43 2.126 3.948 2.345s3.123-0.142 4.45-1.134c0.239-0.179 0.465-0.375 0.655-0.568l2.995-2.995c1.163-1.204 1.722-2.751 1.696-4.285s-0.639-3.061-1.831-4.211c-1.172-1.132-2.688-1.692-4.199-1.683-1.492 0.008-2.984 0.571-4.137 1.683l-1.731 1.721c-0.392 0.389-0.394 1.023-0.004 1.414s1.023 0.394 1.414 0.004l1.709-1.699c0.77-0.742 1.763-1.117 2.76-1.123 1.009-0.006 2.016 0.367 2.798 1.122 0.795 0.768 1.203 1.783 1.221 2.808s-0.355 2.054-1.11 2.836l-3.005 3.005c-0.114 0.116-0.263 0.247-0.428 0.37-0.885 0.662-1.952 0.902-2.967 0.756s-1.971-0.678-2.632-1.563c-0.331-0.442-0.957-0.533-1.4-0.202s-0.533 0.957-0.202 1.4zM14.801 10.401c-0.992-1.327-2.43-2.126-3.948-2.345s-3.124 0.142-4.451 1.134c-0.239 0.179-0.464 0.375-0.655 0.568l-2.995 2.995c-1.163 1.204-1.722 2.751-1.696 4.285s0.639 3.061 1.831 4.211c1.172 1.132 2.688 1.692 4.199 1.683 1.492-0.008 2.984-0.571 4.137-1.683l1.723-1.723c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-1.696 1.698c-0.77 0.742-1.763 1.117-2.76 1.123-1.009 0.006-2.016-0.367-2.798-1.122-0.795-0.768-1.203-1.783-1.221-2.808s0.355-2.054 1.11-2.836l3.005-3.005c0.114-0.116 0.263-0.247 0.428-0.37 0.885-0.662 1.952-0.902 2.967-0.756s1.971 0.678 2.632 1.563c0.331 0.442 0.957 0.533 1.4 0.202s0.533-0.957 0.202-1.4z"></path>
+ </symbol>
+ <symbol id="icon-minus-square" viewBox="0 0 24 24">
+ <path d="M5 2c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-14c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879zM5 4h14c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v14c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-14c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM8 13h8c0.552 0 1-0.448 1-1s-0.448-1-1-1h-8c-0.552 0-1 0.448-1 1s0.448 1 1 1z"></path>
+ </symbol>
+ <symbol id="icon-plus-square" viewBox="0 0 24 24">
+ <path d="M5 2c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-14c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879zM5 4h14c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v14c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-14c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM8 13h3v3c0 0.552 0.448 1 1 1s1-0.448 1-1v-3h3c0.552 0 1-0.448 1-1s-0.448-1-1-1h-3v-3c0-0.552-0.448-1-1-1s-1 0.448-1 1v3h-3c-0.552 0-1 0.448-1 1s0.448 1 1 1z"></path>
+ </symbol>
+ <symbol id="icon-help-circle" viewBox="0 0 24 24">
+ <path d="M23 12c0-3.037-1.232-5.789-3.222-7.778s-4.741-3.222-7.778-3.222-5.789 1.232-7.778 3.222-3.222 4.741-3.222 7.778 1.232 5.789 3.222 7.778 4.741 3.222 7.778 3.222 5.789-1.232 7.778-3.222 3.222-4.741 3.222-7.778zM21 12c0 2.486-1.006 4.734-2.636 6.364s-3.878 2.636-6.364 2.636-4.734-1.006-6.364-2.636-2.636-3.878-2.636-6.364 1.006-4.734 2.636-6.364 3.878-2.636 6.364-2.636 4.734 1.006 6.364 2.636 2.636 3.878 2.636 6.364zM10.033 9.332c0.183-0.521 0.559-0.918 1.022-1.14s1.007-0.267 1.528-0.083c0.458 0.161 0.819 0.47 1.050 0.859 0.183 0.307 0.284 0.665 0.286 1.037 0 0.155-0.039 0.309-0.117 0.464-0.080 0.16-0.203 0.325-0.368 0.49-0.709 0.709-1.831 1.092-1.831 1.092-0.524 0.175-0.807 0.741-0.632 1.265s0.741 0.807 1.265 0.632c0 0 1.544-0.506 2.613-1.575 0.279-0.279 0.545-0.614 0.743-1.010 0.2-0.4 0.328-0.858 0.328-1.369-0.004-0.731-0.204-1.437-0.567-2.049-0.463-0.778-1.19-1.402-2.105-1.724-1.042-0.366-2.135-0.275-3.057 0.167s-1.678 1.238-2.044 2.28c-0.184 0.521 0.090 1.092 0.611 1.275s1.092-0.091 1.275-0.611zM12 18c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1z"></path>
+ </symbol>
+ <symbol id="icon-pin" viewBox="0 0 18 18">
+ <path d="M15 2v-1h-12v1c0 0.552 0.448 1 1 1v8c-0.552 0-1 0.448-1 1v1h5v3c0 0.552 0.448 1 1 1s1-0.448 1-1v-3h5v-1c0-0.552-0.448-1-1-1v-8c0.552 0 1-0.448 1-1zM12 11h-6v-8h6v8z"></path>
+ </symbol>
+ <symbol id="icon-lock" viewBox="0 0 24 24">
+ <path d="M5 12h14c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v7c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-14c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-7c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM18 10v-3c0-1.657-0.673-3.158-1.757-4.243s-2.586-1.757-4.243-1.757-3.158 0.673-4.243 1.757-1.757 2.586-1.757 4.243v3h-1c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v7c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-7c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879zM8 10v-3c0-1.105 0.447-2.103 1.172-2.828s1.723-1.172 2.828-1.172 2.103 0.447 2.828 1.172 1.172 1.723 1.172 2.828v3z"></path>
+ </symbol>
+ <symbol id="icon-check" viewBox="0 0 24 24">
+ <path d="M19.293 5.293l-10.293 10.293-4.293-4.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414l5 5c0.391 0.391 1.024 0.391 1.414 0l11-11c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0z"></path>
+ </symbol>
+ <symbol id="icon-copy" viewBox="0 0 24 24">
+ <path d="M11 8c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v9c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h9c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-9c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879zM11 10h9c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v9c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-9c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-9c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM5 14h-1c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-9c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h9c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v1c0 0.552 0.448 1 1 1s1-0.448 1-1v-1c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879h-9c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v9c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h1c0.552 0 1-0.448 1-1s-0.448-1-1-1z"></path>
+ </symbol>
+ <symbol id="icon-more-vertical" viewBox="0 0 24 24">
+ <path d="M14 12c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414zM14 5c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414zM14 19c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414z"></path>
+ </symbol>
+ <symbol id="icon-bell" viewBox="0 0 24 24">
+ <path d="M17 8c0 4.011 0.947 6.52 1.851 8h-13.702c0.904-1.48 1.851-3.989 1.851-8 0-1.381 0.559-2.63 1.464-3.536s2.155-1.464 3.536-1.464 2.63 0.559 3.536 1.464 1.464 2.155 1.464 3.536zM19 8c0-1.933-0.785-3.684-2.050-4.95s-3.017-2.050-4.95-2.050-3.684 0.785-4.95 2.050-2.050 3.017-2.050 4.95c0 6.127-2.393 8.047-2.563 8.174-0.453 0.308-0.573 0.924-0.269 1.381 0.192 0.287 0.506 0.443 0.832 0.445h18c0.552 0 1-0.448 1-1 0-0.339-0.168-0.638-0.429-0.821-0.176-0.13-2.571-2.050-2.571-8.179zM12.865 20.498c-0.139 0.239-0.359 0.399-0.608 0.465s-0.52 0.037-0.759-0.101c-0.162-0.094-0.283-0.222-0.359-0.357-0.274-0.48-0.884-0.647-1.364-0.373s-0.647 0.884-0.373 1.364c0.25 0.439 0.623 0.823 1.093 1.096 0.716 0.416 1.535 0.501 2.276 0.304s1.409-0.678 1.824-1.394c0.277-0.478 0.114-1.090-0.363-1.367s-1.090-0.114-1.367 0.363z"></path>
+ </symbol>
+ <symbol id="icon-file-text" viewBox="0 0 24 24">
+ <path d="M17.586 7h-2.586v-2.586zM20.707 7.293l-6-6c-0.092-0.092-0.202-0.166-0.324-0.217s-0.253-0.076-0.383-0.076h-8c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v16c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h12c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-12c0-0.276-0.112-0.526-0.293-0.707zM13 3v5c0 0.552 0.448 1 1 1h5v11c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-12c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-16c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM16 12h-8c-0.552 0-1 0.448-1 1s0.448 1 1 1h8c0.552 0 1-0.448 1-1s-0.448-1-1-1zM16 16h-8c-0.552 0-1 0.448-1 1s0.448 1 1 1h8c0.552 0 1-0.448 1-1s-0.448-1-1-1zM10 8h-2c-0.552 0-1 0.448-1 1s0.448 1 1 1h2c0.552 0 1-0.448 1-1s-0.448-1-1-1z"></path>
+ </symbol>
+ <symbol id="icon-eye" viewBox="0 0 24 24">
+ <path d="M0.106 11.553c-0.136 0.274-0.146 0.603 0 0.894 0 0 0.396 0.789 1.12 1.843 0.451 0.656 1.038 1.432 1.757 2.218 0.894 0.979 2.004 1.987 3.319 2.8 1.595 0.986 3.506 1.692 5.698 1.692s4.103-0.706 5.698-1.692c1.315-0.813 2.425-1.821 3.319-2.8 0.718-0.786 1.306-1.562 1.757-2.218 0.724-1.054 1.12-1.843 1.12-1.843 0.136-0.274 0.146-0.603 0-0.894 0 0-0.396-0.789-1.12-1.843-0.451-0.656-1.038-1.432-1.757-2.218-0.894-0.979-2.004-1.987-3.319-2.8-1.595-0.986-3.506-1.692-5.698-1.692s-4.103 0.706-5.698 1.692c-1.315 0.813-2.425 1.821-3.319 2.8-0.719 0.786-1.306 1.561-1.757 2.218-0.724 1.054-1.12 1.843-1.12 1.843zM2.14 12c0.163-0.281 0.407-0.681 0.734-1.158 0.41-0.596 0.94-1.296 1.585-2.001 0.805-0.881 1.775-1.756 2.894-2.448 1.35-0.834 2.901-1.393 4.647-1.393s3.297 0.559 4.646 1.393c1.119 0.692 2.089 1.567 2.894 2.448 0.644 0.705 1.175 1.405 1.585 2.001 0.328 0.477 0.572 0.876 0.734 1.158-0.163 0.281-0.407 0.681-0.734 1.158-0.41 0.596-0.94 1.296-1.585 2.001-0.805 0.881-1.775 1.756-2.894 2.448-1.349 0.834-2.9 1.393-4.646 1.393s-3.297-0.559-4.646-1.393c-1.119-0.692-2.089-1.567-2.894-2.448-0.644-0.705-1.175-1.405-1.585-2.001-0.328-0.477-0.572-0.877-0.735-1.158zM16 12c0-1.104-0.449-2.106-1.172-2.828s-1.724-1.172-2.828-1.172-2.106 0.449-2.828 1.172-1.172 1.724-1.172 2.828 0.449 2.106 1.172 2.828 1.724 1.172 2.828 1.172 2.106-0.449 2.828-1.172 1.172-1.724 1.172-2.828zM14 12c0 0.553-0.223 1.051-0.586 1.414s-0.861 0.586-1.414 0.586-1.051-0.223-1.414-0.586-0.586-0.861-0.586-1.414 0.223-1.051 0.586-1.414 0.861-0.586 1.414-0.586 1.051 0.223 1.414 0.586 0.586 0.861 0.586 1.414z"></path>
+ </symbol>
+ <symbol id="icon-edit" viewBox="0 0 24 24">
+ <path d="M11 3h-7c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-7c0-0.552-0.448-1-1-1s-1 0.448-1 1v7c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-14c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h7c0.552 0 1-0.448 1-1s-0.448-1-1-1zM17.793 1.793l-9.5 9.5c-0.122 0.121-0.217 0.28-0.263 0.465l-1 4c-0.039 0.15-0.042 0.318 0 0.485 0.134 0.536 0.677 0.862 1.213 0.728l4-1c0.167-0.041 0.33-0.129 0.465-0.263l9.5-9.5c0.609-0.609 0.914-1.41 0.914-2.207s-0.305-1.598-0.914-2.207-1.411-0.915-2.208-0.915-1.598 0.305-2.207 0.914zM19.207 3.207c0.219-0.219 0.504-0.328 0.793-0.328s0.574 0.109 0.793 0.328 0.328 0.504 0.328 0.793-0.109 0.574-0.328 0.793l-9.304 9.304-2.114 0.529 0.529-2.114z"></path>
+ </symbol>
+ <symbol id="icon-edit-2" viewBox="0 0 24 24">
+ <path d="M16.293 2.293l-13.5 13.5c-0.117 0.116-0.21 0.268-0.258 0.444l-1.5 5.5c-0.046 0.163-0.049 0.346 0 0.526 0.145 0.533 0.695 0.847 1.228 0.702l5.5-1.5c0.159-0.042 0.315-0.129 0.444-0.258l13.5-13.5c0.747-0.747 1.121-1.729 1.121-2.707s-0.374-1.96-1.121-2.707-1.729-1.121-2.707-1.121-1.96 0.374-2.707 1.121zM17.707 3.707c0.357-0.357 0.824-0.535 1.293-0.535s0.936 0.178 1.293 0.536 0.535 0.823 0.535 1.292-0.178 0.936-0.535 1.293l-13.312 13.312-3.556 0.97 0.97-3.555z"></path>
+ </symbol>
+ <symbol id="icon-trash" viewBox="0 0 24 24">
+ <path d="M18 7v13c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-10c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-13zM17 5v-1c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879h-4c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v1h-4c-0.552 0-1 0.448-1 1s0.448 1 1 1h1v13c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h10c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-13h1c0.552 0 1-0.448 1-1s-0.448-1-1-1zM9 5v-1c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h4c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v1z"></path>
+ </symbol>
+ <symbol id="icon-reply1" viewBox="0 0 20 20">
+ <path d="M19 16.685c0 0-2.225-9.732-11-9.732v-3.984l-7 6.573 7 6.69v-4.357c4.763-0.001 8.516 0.421 11 4.81z"></path>
+ </symbol>
+ <symbol id="icon-star" viewBox="0 0 24 24">
+ <path d="M12.897 1.557c-0.092-0.189-0.248-0.352-0.454-0.454-0.495-0.244-1.095-0.041-1.339 0.454l-2.858 5.789-6.391 0.935c-0.208 0.029-0.411 0.127-0.571 0.291-0.386 0.396-0.377 1.029 0.018 1.414l4.623 4.503-1.091 6.362c-0.036 0.207-0.006 0.431 0.101 0.634 0.257 0.489 0.862 0.677 1.351 0.42l5.714-3.005 5.715 3.005c0.186 0.099 0.408 0.139 0.634 0.101 0.544-0.093 0.91-0.61 0.817-1.155l-1.091-6.362 4.623-4.503c0.151-0.146 0.259-0.344 0.292-0.572 0.080-0.546-0.298-1.054-0.845-1.134l-6.39-0.934zM12 4.259l2.193 4.444c0.151 0.305 0.436 0.499 0.752 0.547l4.906 0.717-3.549 3.457c-0.244 0.238-0.341 0.569-0.288 0.885l0.837 4.883-4.386-2.307c-0.301-0.158-0.647-0.148-0.931 0l-4.386 2.307 0.837-4.883c0.058-0.336-0.059-0.661-0.288-0.885l-3.549-3.457 4.907-0.718c0.336-0.049 0.609-0.26 0.752-0.546z"></path>
+ </symbol>
+ <symbol id="icon-message-square" viewBox="0 0 24 24">
+ <path d="M22 15v-10c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879h-14c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v16c0 0.256 0.098 0.512 0.293 0.707 0.391 0.391 1.024 0.391 1.414 0l3.707-3.707h11.586c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121zM20 15c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-12c-0.276 0-0.526 0.112-0.707 0.293l-2.293 2.293v-13.586c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h14c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707z"></path>
+ </symbol>
+ <symbol id="icon-image" viewBox="0 0 24 24">
+ <path d="M5 2c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-14c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879zM11 8.5c0-0.69-0.281-1.316-0.732-1.768s-1.078-0.732-1.768-0.732-1.316 0.281-1.768 0.732-0.732 1.078-0.732 1.768 0.281 1.316 0.732 1.768 1.078 0.732 1.768 0.732 1.316-0.281 1.768-0.732 0.732-1.078 0.732-1.768zM9 8.5c0 0.138-0.055 0.262-0.146 0.354s-0.216 0.146-0.354 0.146-0.262-0.055-0.354-0.146-0.146-0.216-0.146-0.354 0.055-0.262 0.146-0.354 0.216-0.146 0.354-0.146 0.262 0.055 0.354 0.146 0.146 0.216 0.146 0.354zM7.414 20l8.586-8.586 4 4v3.586c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293zM20 12.586l-3.293-3.293c-0.391-0.391-1.024-0.391-1.414 0l-10.644 10.644c-0.135-0.050-0.255-0.129-0.356-0.23-0.182-0.182-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h14c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707z"></path>
+ </symbol>
+ <symbol id="icon-external-link" viewBox="0 0 24 24">
+ <path d="M17 13v6c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-11c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-11c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h6c0.552 0 1-0.448 1-1s-0.448-1-1-1h-6c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v11c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h11c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1zM10.707 14.707l9.293-9.293v3.586c0 0.552 0.448 1 1 1s1-0.448 1-1v-6c0-0.136-0.027-0.265-0.076-0.383s-0.121-0.228-0.216-0.323c-0.001-0.001-0.001-0.001-0.002-0.002-0.092-0.092-0.202-0.166-0.323-0.216-0.118-0.049-0.247-0.076-0.383-0.076h-6c-0.552 0-1 0.448-1 1s0.448 1 1 1h3.586l-9.293 9.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0z"></path>
+ </symbol>
+ <symbol id="icon-coffee" viewBox="0 0 24 24">
+ <path d="M17 19h-12c-0.553 0-1-0.447-1-1s0.447-1 1-1h12c0.553 0 1 0.447 1 1s-0.447 1-1 1z"></path>
+ <path d="M17.5 5h-12.5v9c0 1.1 0.9 2 2 2h8c1.1 0 2-0.9 2-2v-2h0.5c1.93 0 3.5-1.57 3.5-3.5s-1.57-3.5-3.5-3.5zM15 14h-8v-7h8v7zM17.5 10h-1.5v-3h1.5c0.827 0 1.5 0.673 1.5 1.5s-0.673 1.5-1.5 1.5z"></path>
+ </symbol>
+ <symbol id="icon-rss" viewBox="0 0 24 24">
+ <path d="M4 12c2.209 0 4.208 0.894 5.657 2.343s2.343 3.448 2.343 5.657c0 0.552 0.448 1 1 1s1-0.448 1-1c0-2.761-1.12-5.263-2.929-7.071s-4.31-2.929-7.071-2.929c-0.552 0-1 0.448-1 1s0.448 1 1 1zM4 5c4.142 0 7.891 1.678 10.607 4.393s4.393 6.465 4.393 10.607c0 0.552 0.448 1 1 1s1-0.448 1-1c0-4.694-1.904-8.946-4.979-12.021s-7.327-4.979-12.021-4.979c-0.552 0-1 0.448-1 1s0.448 1 1 1zM7 19c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414z"></path>
+ </symbol>
+ <symbol id="icon-arrow-down" viewBox="0 0 24 24">
+ <path d="M18.293 11.293l-5.293 5.293v-11.586c0-0.552-0.448-1-1-1s-1 0.448-1 1v11.586l-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414l7 7c0.092 0.092 0.202 0.166 0.324 0.217 0.245 0.101 0.521 0.101 0.766 0 0.118-0.049 0.228-0.121 0.324-0.217l7-7c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0z"></path>
+ </symbol>
+ <symbol id="icon-arrow-up" viewBox="0 0 24 24">
+ <path d="M5.707 12.707l5.293-5.293v11.586c0 0.552 0.448 1 1 1s1-0.448 1-1v-11.586l5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-7-7c-0.092-0.092-0.202-0.166-0.324-0.217s-0.253-0.076-0.383-0.076c-0.256 0-0.512 0.098-0.707 0.293l-7 7c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0z"></path>
+ </symbol>
+ <symbol id="icon-arrow-up1" viewBox="0 0 26 28">
+ <path d="M25.172 15.172c0 0.531-0.219 1.031-0.578 1.406l-1.172 1.172c-0.375 0.375-0.891 0.594-1.422 0.594s-1.047-0.219-1.406-0.594l-4.594-4.578v11c0 1.125-0.938 1.828-2 1.828h-2c-1.062 0-2-0.703-2-1.828v-11l-4.594 4.578c-0.359 0.375-0.875 0.594-1.406 0.594s-1.047-0.219-1.406-0.594l-1.172-1.172c-0.375-0.375-0.594-0.875-0.594-1.406s0.219-1.047 0.594-1.422l10.172-10.172c0.359-0.375 0.875-0.578 1.406-0.578s1.047 0.203 1.422 0.578l10.172 10.172c0.359 0.375 0.578 0.891 0.578 1.422z"></path>
+ </symbol>
+ <symbol id="icon-arrow-down1" viewBox="0 0 26 28">
+ <path d="M25.172 13c0 0.531-0.219 1.047-0.578 1.406l-10.172 10.187c-0.375 0.359-0.891 0.578-1.422 0.578s-1.047-0.219-1.406-0.578l-10.172-10.187c-0.375-0.359-0.594-0.875-0.594-1.406s0.219-1.047 0.594-1.422l1.156-1.172c0.375-0.359 0.891-0.578 1.422-0.578s1.047 0.219 1.406 0.578l4.594 4.594v-11c0-1.094 0.906-2 2-2h2c1.094 0 2 0.906 2 2v11l4.594-4.594c0.359-0.359 0.875-0.578 1.406-0.578s1.047 0.219 1.422 0.578l1.172 1.172c0.359 0.375 0.578 0.891 0.578 1.422z"></path>
+ </symbol>
+ <symbol id="icon-mail" viewBox="0 0 24 24">
+ <path d="M3 7.921l8.427 5.899c0.34 0.235 0.795 0.246 1.147 0l8.426-5.899v10.079c0 0.272-0.11 0.521-0.295 0.705s-0.433 0.295-0.705 0.295h-16c-0.272 0-0.521-0.11-0.705-0.295s-0.295-0.433-0.295-0.705zM1 5.983c0 0.010 0 0.020 0 0.030v11.987c0 0.828 0.34 1.579 0.88 2.12s1.292 0.88 2.12 0.88h16c0.828 0 1.579-0.34 2.12-0.88s0.88-1.292 0.88-2.12v-11.988c0-0.010 0-0.020 0-0.030-0.005-0.821-0.343-1.565-0.88-2.102-0.541-0.54-1.292-0.88-2.12-0.88h-16c-0.828 0-1.579 0.34-2.12 0.88-0.537 0.537-0.875 1.281-0.88 2.103zM20.894 5.554l-8.894 6.225-8.894-6.225c0.048-0.096 0.112-0.183 0.188-0.259 0.185-0.185 0.434-0.295 0.706-0.295h16c0.272 0 0.521 0.11 0.705 0.295 0.076 0.076 0.14 0.164 0.188 0.259z"></path>
+ </symbol>
+ <symbol
+ id="icon-mouse"
+ version="1.1"
+ x="0px"
+ y="0px"
+ viewBox="0 0 1024 1024"
+ >
+ <g
+ id="layer1"
+ transform="translate(0,-26.066658)"
+ style="display:inline"
+ >
+ <path
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="m 167.03908,270.78735 c -0.94784,-0.002 -1.8939,0.004 -2.83789,0.0215 -4.31538,0.0778 -8.58934,0.3593 -12.8125,0.8457 -33.78522,3.89116 -64.215716,21.86394 -82.871086,53.27344 -18.27982,30.77718 -22.77749,64.66635 -13.46094,96.06837 9.31655,31.40203 31.88488,59.93174 65.296886,82.5332 0.20163,0.13618 0.40678,0.26709 0.61523,0.39258 28.65434,17.27768 57.18167,28.93179 87.74218,34.95508 -0.74566,12.61339 -0.72532,25.5717 0.082,38.84375 2.43989,40.10943 16.60718,77.03742 38.0957,109.67187 l -77.00781,31.4375 c -8.30605,3.25932 -12.34178,12.68234 -8.96967,20.94324 3.37211,8.2609 12.84919,12.16798 21.06342,8.68371 l 84.69727,-34.57617 c 15.70675,18.72702 33.75346,35.68305 53.12109,50.57032 0.74013,0.56891 1.4904,1.12236 2.23437,1.68554 l -49.61132,65.69141 c -5.45446,7.0474 -4.10058,17.19288 3.01098,22.5634 7.11156,5.37052 17.24028,3.89649 22.52612,-3.27824 l 50.38672,-66.71876 c 27.68572,17.53469 57.07524,31.20388 86.07227,40.25196 14.88153,27.28008 43.96965,44.64648 77.58789,44.64648 33.93762,0 63.04252,-18.68693 77.80082,-45.4375 28.7072,-9.21295 57.7527,-22.93196 85.1484,-40.40234 l 51.0977,67.66016 c 5.2858,7.17473 15.4145,8.64876 22.5261,3.27824 7.1115,-5.37052 8.4654,-15.516 3.011,-22.5634 l -50.3614,-66.68555 c 0.334,-0.25394 0.6727,-0.50077 1.0059,-0.75586 19.1376,-14.64919 37.0259,-31.28581 52.7031,-49.63476 l 82.5625,33.70507 c 8.2143,3.48427 17.6913,-0.42281 21.0634,-8.68371 3.3722,-8.2609 -0.6636,-17.68392 -8.9696,-20.94324 l -74.5391,-30.42773 c 22.1722,-32.82971 37.0383,-70.03397 40.1426,-110.46094 1.0253,-13.35251 1.2292,-26.42535 0.6387,-39.17578 30.3557,-6.05408 58.7164,-17.66833 87.2011,-34.84375 0.2085,-0.12549 0.4136,-0.2564 0.6153,-0.39258 33.412,-22.60147 55.9803,-51.13117 65.2968,-82.5332 9.3166,-31.40202 4.8189,-65.29118 -13.4609,-96.06837 -18.6553,-31.40951 -49.0859,-49.38228 -82.8711,-53.27344 -4.2231,-0.4864 -8.4971,-0.76791 -12.8125,-0.8457 -30.2077,-0.54448 -62.4407,8.82427 -93.4316,26.71484 -22.7976,13.16063 -43.3521,33.31423 -59.4375,55.30469 -44.9968,-25.75094 -103.5444,-40.25065 -175.4785,-41.43945 -6.4522,-0.10663 -13.0125,-0.10696 -19.67974,0.002 -80.18875,1.30929 -144.38284,16.5086 -192.87109,43.9922 -0.11914,-0.19111 -0.24287,-0.37932 -0.37109,-0.56446 -16.29,-22.764 -37.41085,-43.73706 -60.89649,-57.29493 -30.02247,-17.33149 -61.21051,-26.66489 -90.59375,-26.73633 z"
+ id="path817-3"
+ />
+ <path
+ id="path1087"
+ style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 716.85595,362.96478 c 15.29075,-21.36763 35.36198,-41.10921 56.50979,-53.31749 66.66377,-38.48393 137.02617,-33.22172 170.08018,22.43043 33.09493,55.72093 14.98656,117.48866 -47.64399,159.85496 -31.95554,19.26819 -62.93318,30.92309 -97.22892,35.54473 M 307.14407,362.96478 C 291.85332,341.59715 271.78209,321.85557 250.63429,309.64729 183.97051,271.16336 113.60811,276.42557 80.554051,332.07772 47.459131,387.79865 65.56752,449.56638 128.19809,491.93268 c 31.95554,19.26819 62.93319,30.92309 97.22893,35.54473"
+ />
+ <path
+ style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 801.23205,576.8699 C 812.73478,427.06971 720.58431,321.98291 511.99999,325.38859 303.41568,328.79426 213.71393,428.0311 222.76794,576.8699 c 8.64289,142.08048 176.80223,246.40388 288.12038,246.40388 111.31815,0 279.45076,-104.5447 290.34373,-246.40388 z"
+ id="path969"
+ />
+ <path
+ id="path1084"
+ style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 610.4991,644.28932 c 0,23.11198 18.70595,41.84795 41.78091,41.84795 23.07495,0 41.7809,-18.73597 41.7809,-41.84795 0,-23.112 -18.70594,-41.84796 -41.7809,-41.84796 -23.07496,0 -41.78091,18.73596 -41.78091,41.84796 z m -280.56002,0 c 0,23.32492 18.87829,42.23352 42.16586,42.23352 23.28755,0 42.16585,-18.9086 42.16585,-42.23352 0,-23.32494 -18.87829,-42.23353 -42.16585,-42.23353 -23.28757,0 -42.16586,18.90859 -42.16586,42.23353 z"
+ />
+ <path
+ id="path1008"
+ style="display:inline;opacity:1;fill:none;stroke:#000000;stroke-width:32;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 339.72919,769.2467 -54.54422,72.22481 m 399.08582,-72.22481 54.54423,72.22481 M 263.68341,697.82002 175.92752,733.64353 m 579.85765,-35.82351 87.7559,35.82351"
+ />
+ <path
+ style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 512.00082,713.08977 c -45.86417,0 -75.13006,31.84485 -74.14159,71.10084 1.07048,42.51275 32.46865,71.10323 74.14159,71.10323 41.67296,0 74.05118,-32.99608 74.14161,-71.10323 0.0932,-39.26839 -28.27742,-71.10084 -74.14161,-71.10084 z"
+ id="path1115"
+ />
+ </g>
+ </symbol>
+ <symbol id="icon-search" viewBox="0 0 24 24">
+ <path d="M16.041 15.856c-0.034 0.026-0.067 0.055-0.099 0.087s-0.060 0.064-0.087 0.099c-1.258 1.213-2.969 1.958-4.855 1.958-1.933 0-3.682-0.782-4.95-2.050s-2.050-3.017-2.050-4.95 0.782-3.682 2.050-4.95 3.017-2.050 4.95-2.050 3.682 0.782 4.95 2.050 2.050 3.017 2.050 4.95c0 1.886-0.745 3.597-1.959 4.856zM21.707 20.293l-3.675-3.675c1.231-1.54 1.968-3.493 1.968-5.618 0-2.485-1.008-4.736-2.636-6.364s-3.879-2.636-6.364-2.636-4.736 1.008-6.364 2.636-2.636 3.879-2.636 6.364 1.008 4.736 2.636 6.364 3.879 2.636 6.364 2.636c2.125 0 4.078-0.737 5.618-1.968l3.675 3.675c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414z"></path>
+ </symbol>
+ <symbol id="icon-github" viewBox="0 0 32 32">
+ <path d="M16 0.395c-8.836 0-16 7.163-16 16 0 7.069 4.585 13.067 10.942 15.182 0.8 0.148 1.094-0.347 1.094-0.77 0-0.381-0.015-1.642-0.022-2.979-4.452 0.968-5.391-1.888-5.391-1.888-0.728-1.849-1.776-2.341-1.776-2.341-1.452-0.993 0.11-0.973 0.11-0.973 1.606 0.113 2.452 1.649 2.452 1.649 1.427 2.446 3.743 1.739 4.656 1.33 0.143-1.034 0.558-1.74 1.016-2.14-3.554-0.404-7.29-1.777-7.29-7.907 0-1.747 0.625-3.174 1.649-4.295-0.166-0.403-0.714-2.030 0.155-4.234 0 0 1.344-0.43 4.401 1.64 1.276-0.355 2.645-0.532 4.005-0.539 1.359 0.006 2.729 0.184 4.008 0.539 3.054-2.070 4.395-1.64 4.395-1.64 0.871 2.204 0.323 3.831 0.157 4.234 1.026 1.12 1.647 2.548 1.647 4.295 0 6.145-3.743 7.498-7.306 7.895 0.574 0.497 1.085 1.47 1.085 2.963 0 2.141-0.019 3.864-0.019 4.391 0 0.426 0.288 0.925 1.099 0.768 6.354-2.118 10.933-8.113 10.933-15.18 0-8.837-7.164-16-16-16z"></path>
+ </symbol>
+ <symbol id="icon-spinner" viewBox="0 0 32 32">
+ <path d="M16 32c-4.274 0-8.292-1.664-11.314-4.686s-4.686-7.040-4.686-11.314c0-3.026 0.849-5.973 2.456-8.522 1.563-2.478 3.771-4.48 6.386-5.791l1.344 2.682c-2.126 1.065-3.922 2.693-5.192 4.708-1.305 2.069-1.994 4.462-1.994 6.922 0 7.168 5.832 13 13 13s13-5.832 13-13c0-2.459-0.69-4.853-1.994-6.922-1.271-2.015-3.066-3.643-5.192-4.708l1.344-2.682c2.615 1.31 4.824 3.313 6.386 5.791 1.607 2.549 2.456 5.495 2.456 8.522 0 4.274-1.664 8.292-4.686 11.314s-7.040 4.686-11.314 4.686z"></path>
+ </symbol>
+ <symbol id="icon-cake" viewBox="0 0 24 24">
+ <path d="M 23.296875 22.394531 L 22.082031 22.394531 L 22.082031 17.007812 C 22.453125 16.699219 22.664062 16.261719 22.664062 15.796875 L 22.664062 13.984375 C 22.664062 12.996094 21.785156 12.191406 20.703125 12.191406 L 19.785156 12.191406 L 19.785156 7.785156 C 19.785156 7.050781 19.1875 6.449219 18.449219 6.449219 L 18.367188 6.449219 L 18.367188 5.96875 C 19.199219 5.675781 19.796875 4.882812 19.796875 3.957031 C 19.796875 3.644531 19.703125 3.117188 18.996094 1.800781 C 18.632812 1.121094 18.273438 0.550781 18.257812 0.527344 C 18.128906 0.320312 17.90625 0.199219 17.664062 0.199219 C 17.421875 0.199219 17.199219 0.320312 17.070312 0.527344 C 17.054688 0.550781 16.695312 1.121094 16.332031 1.800781 C 15.621094 3.117188 15.53125 3.644531 15.53125 3.957031 C 15.53125 4.882812 16.128906 5.675781 16.960938 5.96875 L 16.960938 6.449219 L 16.878906 6.449219 C 16.140625 6.449219 15.542969 7.050781 15.542969 7.785156 L 15.542969 12.191406 L 14.121094 12.191406 L 14.121094 7.785156 C 14.121094 7.050781 13.523438 6.449219 12.785156 6.449219 L 12.703125 6.449219 L 12.703125 5.96875 C 13.535156 5.675781 14.132812 4.882812 14.132812 3.957031 C 14.132812 3.644531 14.039062 3.117188 13.332031 1.800781 C 12.96875 1.121094 12.609375 0.550781 12.59375 0.527344 C 12.464844 0.320312 12.242188 0.199219 12 0.199219 C 11.757812 0.199219 11.535156 0.320312 11.40625 0.527344 C 11.390625 0.550781 11.03125 1.121094 10.667969 1.800781 C 9.960938 3.117188 9.867188 3.644531 9.867188 3.957031 C 9.867188 4.882812 10.464844 5.675781 11.296875 5.96875 L 11.296875 6.449219 L 11.214844 6.449219 C 10.476562 6.449219 9.878906 7.050781 9.878906 7.785156 L 9.878906 12.191406 L 8.457031 12.191406 L 8.457031 7.785156 C 8.457031 7.050781 7.859375 6.449219 7.121094 6.449219 L 7.039062 6.449219 L 7.039062 5.96875 C 7.871094 5.675781 8.46875 4.882812 8.46875 3.957031 C 8.46875 3.644531 8.378906 3.117188 7.667969 1.800781 C 7.304688 1.121094 6.945312 0.550781 6.929688 0.527344 C 6.800781 0.320312 6.578125 0.199219 6.335938 0.199219 C 6.09375 0.199219 5.871094 0.320312 5.742188 0.527344 C 5.726562 0.550781 5.367188 1.121094 5.003906 1.800781 C 4.296875 3.117188 4.203125 3.644531 4.203125 3.957031 C 4.203125 4.882812 4.800781 5.675781 5.632812 5.96875 L 5.632812 6.449219 L 5.550781 6.449219 C 4.8125 6.449219 4.214844 7.050781 4.214844 7.785156 L 4.214844 12.191406 L 3.296875 12.191406 C 2.214844 12.191406 1.335938 12.996094 1.335938 13.984375 L 1.335938 15.796875 C 1.335938 16.261719 1.546875 16.699219 1.917969 17.007812 L 1.917969 22.394531 L 0.703125 22.394531 C 0.316406 22.394531 0 22.710938 0 23.097656 C 0 23.488281 0.316406 23.800781 0.703125 23.800781 L 23.296875 23.800781 C 23.683594 23.800781 24 23.488281 24 23.097656 C 24 22.710938 23.683594 22.394531 23.296875 22.394531 Z M 16.9375 3.957031 C 16.941406 3.730469 17.246094 3.054688 17.664062 2.289062 C 18.082031 3.054688 18.382812 3.730469 18.390625 3.957031 C 18.390625 4.355469 18.0625 4.679688 17.664062 4.679688 C 17.265625 4.679688 16.9375 4.355469 16.9375 3.957031 Z M 16.949219 7.855469 L 18.378906 7.855469 L 18.378906 12.1875 L 16.949219 12.1875 Z M 11.273438 3.957031 C 11.277344 3.730469 11.582031 3.054688 12 2.289062 C 12.417969 3.054688 12.722656 3.730469 12.726562 3.957031 C 12.726562 4.355469 12.398438 4.679688 12 4.679688 C 11.601562 4.679688 11.273438 4.355469 11.273438 3.957031 Z M 11.285156 7.855469 L 12.714844 7.855469 L 12.714844 12.1875 L 11.285156 12.1875 Z M 5.609375 3.957031 C 5.613281 3.730469 5.917969 3.054688 6.335938 2.289062 C 6.753906 3.054688 7.058594 3.730469 7.0625 3.957031 C 7.0625 4.355469 6.734375 4.679688 6.335938 4.679688 C 5.9375 4.679688 5.609375 4.355469 5.609375 3.957031 Z M 5.621094 7.855469 L 7.050781 7.855469 L 7.050781 12.1875 L 5.621094 12.1875 Z M 20.675781 22.394531 L 3.324219 22.394531 L 3.324219 17.414062 C 3.433594 17.398438 3.546875 17.378906 3.652344 17.347656 L 5.429688 16.820312 C 6.453125 16.515625 7.582031 16.515625 8.609375 16.820312 L 10.011719 17.234375 C 10.652344 17.425781 11.324219 17.519531 12 17.519531 C 12.675781 17.519531 13.347656 17.425781 13.988281 17.234375 L 15.390625 16.820312 C 16.417969 16.515625 17.546875 16.515625 18.570312 16.820312 L 20.347656 17.347656 C 20.453125 17.378906 20.5625 17.398438 20.675781 17.414062 Z M 21.257812 15.796875 C 21.257812 15.855469 21.210938 15.902344 21.171875 15.933594 C 21.082031 16 20.925781 16.050781 20.746094 15.996094 L 18.972656 15.472656 C 17.6875 15.09375 16.273438 15.09375 14.992188 15.472656 L 13.589844 15.886719 C 12.566406 16.191406 11.433594 16.191406 10.410156 15.886719 L 9.007812 15.472656 C 8.367188 15.28125 7.691406 15.1875 7.019531 15.1875 C 6.34375 15.1875 5.671875 15.28125 5.027344 15.472656 L 3.253906 15.996094 C 3.074219 16.050781 2.917969 16 2.828125 15.933594 C 2.789062 15.902344 2.742188 15.855469 2.742188 15.796875 L 2.742188 13.984375 C 2.742188 13.800781 2.96875 13.597656 3.296875 13.597656 L 20.703125 13.597656 C 21.03125 13.597656 21.257812 13.800781 21.257812 13.984375 Z M 21.257812 15.796875 " />
+ </symbol>
+ <symbol id="icon-subscript" viewBox="0 0 20 20">
+ <path d="M13.68 16h-2.42a.67.67 0 0 1-.46-.15 1.33 1.33 0 0 1-.28-.34l-2.77-4.44a2.65 2.65 0 0 1-.28.69L5 15.51a2.22 2.22 0 0 1-.29.34.58.58 0 0 1-.42.15H2l4.15-6.19L2.17 4h2.42a.81.81 0 0 1 .41.09.8.8 0 0 1 .24.26L8 8.59a2.71 2.71 0 0 1 .33-.74L10.6 4.4a.69.69 0 0 1 .6-.4h2.32l-4 5.71zm3.82-4h.5v-1h-.5a1.49 1.49 0 0 0-1 .39 1.49 1.49 0 0 0-1-.39H15v1h.5a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-.5.5H15v1h.5a1.49 1.49 0 0 0 1-.39 1.49 1.49 0 0 0 1 .39h.5v-1h-.5a.5.5 0 0 1-.5-.5v-6a.5.5 0 0 1 .5-.5z" />
+ </symbol>
+ <symbol id="icon-superscript" viewBox="0 0 20 20">
+ <path d="M17.5 1h.5V0h-.5a1.49 1.49 0 0 0-1 .39 1.49 1.49 0 0 0-1-.39H15v1h.5a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-.5.5H15v1h.5a1.49 1.49 0 0 0 1-.39 1.49 1.49 0 0 0 1 .39h.5V8h-.5a.5.5 0 0 1-.5-.5v-6a.5.5 0 0 1 .5-.5zm-3.82 15h-2.42a.67.67 0 0 1-.46-.15 1.33 1.33 0 0 1-.28-.34l-2.77-4.44a2.65 2.65 0 0 1-.28.69L5 15.51a2.22 2.22 0 0 1-.29.34.58.58 0 0 1-.42.15H2l4.15-6.19L2.17 4h2.42a.81.81 0 0 1 .41.09.8.8 0 0 1 .24.26L8 8.59a2.71 2.71 0 0 1 .33-.74L10.6 4.4a.69.69 0 0 1 .6-.4h2.32l-4 5.71z" />
+ </symbol>
+ </defs>
+ </svg>
+ );
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { WebSocketService, UserService } from '../services';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { i18n } from '../i18next';
+import {
+ UserOperation,
+ Post,
+ Comment,
+ CommunityUser,
+ SortType,
+ UserDetailsResponse,
+ UserView,
+ WebSocketJsonResponse,
+ CommentResponse,
+ BanUserResponse,
+ PostResponse,
+} from 'lemmy-js-client';
+import { UserDetailsView } from '../interfaces';
+import {
+ wsJsonToRes,
+ toast,
+ commentsToFlatNodes,
+ setupTippy,
+ editCommentRes,
+ saveCommentRes,
+ createCommentLikeRes,
+ createPostLikeFindRes,
+} from '../utils';
+import { PostListing } from './post-listing';
+import { CommentNodes } from './comment-nodes';
+
+interface UserDetailsProps {
+ username?: string;
+ user_id?: number;
+ page: number;
+ limit: number;
+ sort: SortType;
+ enableDownvotes: boolean;
+ enableNsfw: boolean;
+ view: UserDetailsView;
+ onPageChange(page: number): number | any;
+ admins: UserView[];
+}
+
+interface UserDetailsState {
+ follows: CommunityUser[];
+ moderates: CommunityUser[];
+ comments: Comment[];
+ posts: Post[];
+ saved?: Post[];
+}
+
+export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
+ private subscription: Subscription;
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = {
+ follows: [],
+ moderates: [],
+ comments: [],
+ posts: [],
+ saved: [],
+ };
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ componentDidMount() {
+ this.fetchUserData();
+ setupTippy();
+ }
+
+ componentDidUpdate(lastProps: UserDetailsProps) {
+ for (const key of Object.keys(lastProps)) {
+ if (lastProps[key] !== this.props[key]) {
+ this.fetchUserData();
+ break;
+ }
+ }
+ }
+
+ fetchUserData() {
+ WebSocketService.Instance.getUserDetails({
+ user_id: this.props.user_id,
+ username: this.props.username,
+ sort: this.props.sort,
+ saved_only: this.props.view === UserDetailsView.Saved,
+ page: this.props.page,
+ limit: this.props.limit,
+ });
+ }
+
+ render() {
+ return (
+ <div>
+ {this.viewSelector(this.props.view)}
+ {this.paginator()}
+ </div>
+ );
+ }
+
+ viewSelector(view: UserDetailsView) {
+ if (view === UserDetailsView.Overview || view === UserDetailsView.Saved) {
+ return this.overview();
+ }
+ if (view === UserDetailsView.Comments) {
+ return this.comments();
+ }
+ if (view === UserDetailsView.Posts) {
+ return this.posts();
+ }
+ }
+
+ overview() {
+ const comments = this.state.comments.map((c: Comment) => {
+ return { type: 'comments', data: c };
+ });
+ const posts = this.state.posts.map((p: Post) => {
+ return { type: 'posts', data: p };
+ });
+
+ const combined: { type: string; data: Comment | Post }[] = [
+ ...comments,
+ ...posts,
+ ];
+
+ // Sort it
+ if (this.props.sort === SortType.New) {
+ combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
+ } else {
+ combined.sort((a, b) => b.data.score - a.data.score);
+ }
+
+ return (
+ <div>
+ {combined.map(i => (
+ <>
+ <div>
+ {i.type === 'posts' ? (
+ <PostListing
+ key={(i.data as Post).id}
+ post={i.data as Post}
+ admins={this.props.admins}
+ showCommunity
+ enableDownvotes={this.props.enableDownvotes}
+ enableNsfw={this.props.enableNsfw}
+ />
+ ) : (
+ <CommentNodes
+ key={(i.data as Comment).id}
+ nodes={[{ comment: i.data as Comment }]}
+ admins={this.props.admins}
+ noBorder
+ noIndent
+ showCommunity
+ showContext
+ enableDownvotes={this.props.enableDownvotes}
+ />
+ )}
+ </div>
+ <hr class="my-3" />
+ </>
+ ))}
+ </div>
+ );
+ }
+
+ comments() {
+ return (
+ <div>
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.comments)}
+ admins={this.props.admins}
+ noIndent
+ showCommunity
+ showContext
+ enableDownvotes={this.props.enableDownvotes}
+ />
+ </div>
+ );
+ }
+
+ posts() {
+ return (
+ <div>
+ {this.state.posts.map(post => (
+ <>
+ <PostListing
+ post={post}
+ admins={this.props.admins}
+ showCommunity
+ enableDownvotes={this.props.enableDownvotes}
+ enableNsfw={this.props.enableNsfw}
+ />
+ <hr class="my-3" />
+ </>
+ ))}
+ </div>
+ );
+ }
+
+ paginator() {
+ return (
+ <div class="my-2">
+ {this.props.page > 1 && (
+ <button
+ class="btn btn-secondary mr-1"
+ onClick={linkEvent(this, this.prevPage)}
+ >
+ {i18n.t('prev')}
+ </button>
+ )}
+ {this.state.comments.length + this.state.posts.length > 0 && (
+ <button
+ class="btn btn-secondary"
+ onClick={linkEvent(this, this.nextPage)}
+ >
+ {i18n.t('next')}
+ </button>
+ )}
+ </div>
+ );
+ }
+
+ nextPage(i: UserDetails) {
+ i.props.onPageChange(i.props.page + 1);
+ }
+
+ prevPage(i: UserDetails) {
+ i.props.onPageChange(i.props.page - 1);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ const res = wsJsonToRes(msg);
+
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ if (msg.error == 'couldnt_find_that_username_or_email') {
+ this.context.router.history.push('/');
+ }
+ return;
+ } else if (msg.reconnect) {
+ this.fetchUserData();
+ } else if (res.op == UserOperation.GetUserDetails) {
+ const data = res.data as UserDetailsResponse;
+ this.setState({
+ comments: data.comments,
+ follows: data.follows,
+ moderates: data.moderates,
+ posts: data.posts,
+ });
+ } else if (res.op == UserOperation.CreateCommentLike) {
+ const data = res.data as CommentResponse;
+ createCommentLikeRes(data, this.state.comments);
+ this.setState({
+ comments: this.state.comments,
+ });
+ } else if (
+ res.op == UserOperation.EditComment ||
+ res.op == UserOperation.DeleteComment ||
+ res.op == UserOperation.RemoveComment
+ ) {
+ const data = res.data as CommentResponse;
+ editCommentRes(data, this.state.comments);
+ this.setState({
+ comments: this.state.comments,
+ });
+ } else if (res.op == UserOperation.CreateComment) {
+ const data = res.data as CommentResponse;
+ if (
+ UserService.Instance.user &&
+ data.comment.creator_id == UserService.Instance.user.id
+ ) {
+ toast(i18n.t('reply_sent'));
+ }
+ } else if (res.op == UserOperation.SaveComment) {
+ const data = res.data as CommentResponse;
+ saveCommentRes(data, this.state.comments);
+ this.setState({
+ comments: this.state.comments,
+ });
+ } else if (res.op == UserOperation.CreatePostLike) {
+ const data = res.data as PostResponse;
+ createPostLikeFindRes(data, this.state.posts);
+ this.setState({
+ posts: this.state.posts,
+ });
+ } else if (res.op == UserOperation.BanUser) {
+ const data = res.data as BanUserResponse;
+ this.state.comments
+ .filter(c => c.creator_id == data.user.id)
+ .forEach(c => (c.banned = data.banned));
+ this.state.posts
+ .filter(c => c.creator_id == data.user.id)
+ .forEach(c => (c.banned = data.banned));
+ this.setState({
+ posts: this.state.posts,
+ comments: this.state.comments,
+ });
+ }
+ }
+}
--- /dev/null
+import { Component } from 'inferno';
+import { Link } from 'inferno-router';
+import { UserView } from 'lemmy-js-client';
+import {
+ pictrsAvatarThumbnail,
+ showAvatars,
+ hostname,
+ isCakeDay,
+} from '../utils';
+import { CakeDay } from './cake-day';
+
+export interface UserOther {
+ name: string;
+ preferred_username?: string;
+ id?: number; // Necessary if its federated
+ avatar?: string;
+ local?: boolean;
+ actor_id?: string;
+ published?: string;
+}
+
+interface UserListingProps {
+ user: UserView | UserOther;
+ realLink?: boolean;
+ useApubName?: boolean;
+ muted?: boolean;
+ hideAvatar?: boolean;
+}
+
+export class UserListing extends Component<UserListingProps, any> {
+ constructor(props: any, context: any) {
+ super(props, context);
+ }
+
+ render() {
+ let user = this.props.user;
+ let local = user.local == null ? true : user.local;
+ let apubName: string, link: string;
+
+ if (local) {
+ apubName = `@${user.name}`;
+ link = `/u/${user.name}`;
+ } else {
+ apubName = `@${user.name}@${hostname(user.actor_id)}`;
+ link = !this.props.realLink ? `/user/${user.id}` : user.actor_id;
+ }
+
+ let displayName = this.props.useApubName
+ ? apubName
+ : user.preferred_username
+ ? user.preferred_username
+ : apubName;
+
+ return (
+ <>
+ <Link
+ title={apubName}
+ className={this.props.muted ? 'text-muted' : 'text-info'}
+ to={link}
+ >
+ {!this.props.hideAvatar && user.avatar && showAvatars() && (
+ <img
+ style="width: 2rem; height: 2rem;"
+ src={pictrsAvatarThumbnail(user.avatar)}
+ class="rounded-circle mr-2"
+ />
+ )}
+ <span>{displayName}</span>
+ </Link>
+
+ {isCakeDay(user.published) && <CakeDay creatorName={apubName} />}
+ </>
+ );
+ }
+}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Link } from 'inferno-router';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+ UserOperation,
+ CommunityUser,
+ SortType,
+ ListingType,
+ UserView,
+ UserSettingsForm,
+ LoginResponse,
+ DeleteAccountForm,
+ WebSocketJsonResponse,
+ GetSiteResponse,
+ UserDetailsResponse,
+ AddAdminResponse,
+} from 'lemmy-js-client';
+import { UserDetailsView } from '../interfaces';
+import { WebSocketService, UserService } from '../services';
+import {
+ wsJsonToRes,
+ fetchLimit,
+ routeSortTypeToEnum,
+ capitalizeFirstLetter,
+ themes,
+ setTheme,
+ languages,
+ toast,
+ setupTippy,
+ getLanguage,
+ mdToHtml,
+ elementUrl,
+ favIconUrl,
+} from '../utils';
+import { UserListing } from './user-listing';
+import { SortSelect } from './sort-select';
+import { ListingTypeSelect } from './listing-type-select';
+import { MomentTime } from './moment-time';
+import { i18n } from '../i18next';
+import moment from 'moment';
+import { UserDetails } from './user-details';
+import { MarkdownTextArea } from './markdown-textarea';
+import { ImageUploadForm } from './image-upload-form';
+import { BannerIconHeader } from './banner-icon-header';
+
+interface UserState {
+ user: UserView;
+ user_id: number;
+ username: string;
+ follows: CommunityUser[];
+ moderates: CommunityUser[];
+ view: UserDetailsView;
+ sort: SortType;
+ page: number;
+ loading: boolean;
+ userSettingsForm: UserSettingsForm;
+ userSettingsLoading: boolean;
+ deleteAccountLoading: boolean;
+ deleteAccountShowConfirm: boolean;
+ deleteAccountForm: DeleteAccountForm;
+ siteRes: GetSiteResponse;
+}
+
+interface UserProps {
+ view: UserDetailsView;
+ sort: SortType;
+ page: number;
+ user_id: number | null;
+ username: string;
+}
+
+interface UrlParams {
+ view?: string;
+ sort?: SortType;
+ page?: number;
+}
+
+export class User extends Component<any, UserState> {
+ private subscription: Subscription;
+ private emptyState: UserState = {
+ user: {
+ id: null,
+ name: null,
+ published: null,
+ number_of_posts: null,
+ post_score: null,
+ number_of_comments: null,
+ comment_score: null,
+ banned: null,
+ avatar: null,
+ actor_id: null,
+ local: null,
+ },
+ user_id: null,
+ username: null,
+ follows: [],
+ moderates: [],
+ loading: true,
+ view: User.getViewFromProps(this.props.match.view),
+ sort: User.getSortTypeFromProps(this.props.match.sort),
+ page: User.getPageFromProps(this.props.match.page),
+ userSettingsForm: {
+ show_nsfw: null,
+ theme: null,
+ default_sort_type: null,
+ default_listing_type: null,
+ lang: null,
+ show_avatars: null,
+ send_notifications_to_email: null,
+ auth: null,
+ bio: null,
+ preferred_username: null,
+ },
+ userSettingsLoading: null,
+ deleteAccountLoading: null,
+ deleteAccountShowConfirm: false,
+ deleteAccountForm: {
+ password: null,
+ },
+ siteRes: {
+ admins: [],
+ banned: [],
+ online: undefined,
+ site: {
+ id: undefined,
+ name: undefined,
+ creator_id: undefined,
+ published: undefined,
+ creator_name: undefined,
+ number_of_users: undefined,
+ number_of_posts: undefined,
+ number_of_comments: undefined,
+ number_of_communities: undefined,
+ enable_downvotes: undefined,
+ open_registration: undefined,
+ enable_nsfw: undefined,
+ icon: undefined,
+ banner: undefined,
+ creator_preferred_username: undefined,
+ },
+ version: undefined,
+ my_user: undefined,
+ federated_instances: undefined,
+ },
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handleSortChange = this.handleSortChange.bind(this);
+ this.handleUserSettingsSortTypeChange = this.handleUserSettingsSortTypeChange.bind(
+ this
+ );
+ this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
+ this
+ );
+ this.handlePageChange = this.handlePageChange.bind(this);
+ this.handleUserSettingsBioChange = this.handleUserSettingsBioChange.bind(
+ this
+ );
+
+ this.handleAvatarUpload = this.handleAvatarUpload.bind(this);
+ this.handleAvatarRemove = this.handleAvatarRemove.bind(this);
+
+ this.handleBannerUpload = this.handleBannerUpload.bind(this);
+ this.handleBannerRemove = this.handleBannerRemove.bind(this);
+
+ this.state.user_id = Number(this.props.match.params.id) || null;
+ this.state.username = this.props.match.params.username;
+
+ this.subscription = WebSocketService.Instance.subject
+ .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+ .subscribe(
+ msg => this.parseMessage(msg),
+ err => console.error(err),
+ () => console.log('complete')
+ );
+
+ WebSocketService.Instance.getSite();
+ setupTippy();
+ }
+
+ get isCurrentUser() {
+ return (
+ UserService.Instance.user &&
+ UserService.Instance.user.id == this.state.user.id
+ );
+ }
+
+ static getViewFromProps(view: string): UserDetailsView {
+ return view ? UserDetailsView[view] : UserDetailsView.Overview;
+ }
+
+ static getSortTypeFromProps(sort: string): SortType {
+ return sort ? routeSortTypeToEnum(sort) : SortType.New;
+ }
+
+ static getPageFromProps(page: number): number {
+ return page ? Number(page) : 1;
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ static getDerivedStateFromProps(props: any): UserProps {
+ return {
+ view: this.getViewFromProps(props.match.params.view),
+ sort: this.getSortTypeFromProps(props.match.params.sort),
+ page: this.getPageFromProps(props.match.params.page),
+ user_id: Number(props.match.params.id) || null,
+ username: props.match.params.username,
+ };
+ }
+
+ componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) {
+ // Necessary if you are on a post and you click another post (same route)
+ if (
+ lastProps.location.pathname.split('/')[2] !==
+ lastProps.history.location.pathname.split('/')[2]
+ ) {
+ // Couldnt get a refresh working. This does for now.
+ location.reload();
+ }
+ }
+
+ get documentTitle(): string {
+ if (this.state.siteRes.site.name) {
+ return `@${this.state.username} - ${this.state.siteRes.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ get favIcon(): string {
+ return this.state.siteRes.site.icon
+ ? this.state.siteRes.site.icon
+ : favIconUrl;
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <Helmet title={this.documentTitle}>
+ <link
+ id="favicon"
+ rel="icon"
+ type="image/x-icon"
+ href={this.favIcon}
+ />
+ </Helmet>
+ <div class="row">
+ <div class="col-12 col-md-8">
+ {this.state.loading ? (
+ <h5>
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ </h5>
+ ) : (
+ <>
+ {this.userInfo()}
+ <hr />
+ </>
+ )}
+ {!this.state.loading && this.selects()}
+ <UserDetails
+ user_id={this.state.user_id}
+ username={this.state.username}
+ sort={this.state.sort}
+ page={this.state.page}
+ limit={fetchLimit}
+ enableDownvotes={this.state.siteRes.site.enable_downvotes}
+ enableNsfw={this.state.siteRes.site.enable_nsfw}
+ admins={this.state.siteRes.admins}
+ view={this.state.view}
+ onPageChange={this.handlePageChange}
+ />
+ </div>
+
+ {!this.state.loading && (
+ <div class="col-12 col-md-4">
+ {this.isCurrentUser && this.userSettings()}
+ {this.moderates()}
+ {this.follows()}
+ </div>
+ )}
+ </div>
+ </div>
+ );
+ }
+
+ viewRadios() {
+ return (
+ <div class="btn-group btn-group-toggle flex-wrap mb-2">
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.view == UserDetailsView.Overview && 'active'}
+ `}
+ >
+ <input
+ type="radio"
+ value={UserDetailsView.Overview}
+ checked={this.state.view === UserDetailsView.Overview}
+ onChange={linkEvent(this, this.handleViewChange)}
+ />
+ {i18n.t('overview')}
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.view == UserDetailsView.Comments && 'active'}
+ `}
+ >
+ <input
+ type="radio"
+ value={UserDetailsView.Comments}
+ checked={this.state.view == UserDetailsView.Comments}
+ onChange={linkEvent(this, this.handleViewChange)}
+ />
+ {i18n.t('comments')}
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.view == UserDetailsView.Posts && 'active'}
+ `}
+ >
+ <input
+ type="radio"
+ value={UserDetailsView.Posts}
+ checked={this.state.view == UserDetailsView.Posts}
+ onChange={linkEvent(this, this.handleViewChange)}
+ />
+ {i18n.t('posts')}
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.view == UserDetailsView.Saved && 'active'}
+ `}
+ >
+ <input
+ type="radio"
+ value={UserDetailsView.Saved}
+ checked={this.state.view == UserDetailsView.Saved}
+ onChange={linkEvent(this, this.handleViewChange)}
+ />
+ {i18n.t('saved')}
+ </label>
+ </div>
+ );
+ }
+
+ selects() {
+ return (
+ <div className="mb-2">
+ <span class="mr-3">{this.viewRadios()}</span>
+ <SortSelect
+ sort={this.state.sort}
+ onChange={this.handleSortChange}
+ hideHot
+ />
+ <a
+ href={`/feeds/u/${this.state.username}.xml?sort=${this.state.sort}`}
+ target="_blank"
+ rel="noopener"
+ title="RSS"
+ >
+ <svg class="icon mx-2 text-muted small">
+ <use xlinkHref="#icon-rss">#</use>
+ </svg>
+ </a>
+ </div>
+ );
+ }
+
+ userInfo() {
+ let user = this.state.user;
+
+ return (
+ <div>
+ <BannerIconHeader
+ banner={this.state.user.banner}
+ icon={this.state.user.avatar}
+ />
+ <div class="mb-3">
+ <div class="">
+ <div class="mb-0 d-flex flex-wrap">
+ <div>
+ {user.preferred_username && (
+ <h5 class="mb-0">{user.preferred_username}</h5>
+ )}
+ <ul class="list-inline mb-2">
+ <li className="list-inline-item">
+ <UserListing
+ user={user}
+ realLink
+ useApubName
+ muted
+ hideAvatar
+ />
+ </li>
+ {user.banned && (
+ <li className="list-inline-item badge badge-danger">
+ {i18n.t('banned')}
+ </li>
+ )}
+ </ul>
+ </div>
+ <div className="flex-grow-1 unselectable pointer mx-2"></div>
+ {this.isCurrentUser ? (
+ <button
+ class="d-flex align-self-start btn btn-secondary ml-2"
+ onClick={linkEvent(this, this.handleLogoutClick)}
+ >
+ {i18n.t('logout')}
+ </button>
+ ) : (
+ <>
+ <a
+ className={`d-flex align-self-start btn btn-secondary ml-2 ${
+ !this.state.user.matrix_user_id && 'invisible'
+ }`}
+ target="_blank"
+ rel="noopener"
+ href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
+ >
+ {i18n.t('send_secure_message')}
+ </a>
+ <Link
+ class="d-flex align-self-start btn btn-secondary ml-2"
+ to={`/create_private_message?recipient_id=${this.state.user.id}`}
+ >
+ {i18n.t('send_message')}
+ </Link>
+ </>
+ )}
+ </div>
+ {user.bio && (
+ <div className="d-flex align-items-center mb-2">
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(user.bio)}
+ />
+ </div>
+ )}
+ <div>
+ <ul class="list-inline mb-2">
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_posts', { count: user.number_of_posts })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_comments', {
+ count: user.number_of_comments,
+ })}
+ </li>
+ </ul>
+ </div>
+ <div class="text-muted">
+ {i18n.t('joined')} <MomentTime data={user} showAgo />
+ </div>
+ <div className="d-flex align-items-center text-muted mb-2">
+ <svg class="icon">
+ <use xlinkHref="#icon-cake"></use>
+ </svg>
+ <span className="ml-2">
+ {i18n.t('cake_day_title')}{' '}
+ {moment.utc(user.published).local().format('MMM DD, YYYY')}
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ userSettings() {
+ return (
+ <div>
+ <div class="card bg-transparent border-secondary mb-3">
+ <div class="card-body">
+ <h5>{i18n.t('settings')}</h5>
+ <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
+ <div class="form-group">
+ <label>{i18n.t('avatar')}</label>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_avatar')}
+ imageSrc={this.state.userSettingsForm.avatar}
+ onUpload={this.handleAvatarUpload}
+ onRemove={this.handleAvatarRemove}
+ rounded
+ />
+ </div>
+ <div class="form-group">
+ <label>{i18n.t('banner')}</label>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_banner')}
+ imageSrc={this.state.userSettingsForm.banner}
+ onUpload={this.handleBannerUpload}
+ onRemove={this.handleBannerRemove}
+ />
+ </div>
+ <div class="form-group">
+ <label>{i18n.t('language')}</label>
+ <select
+ value={this.state.userSettingsForm.lang}
+ onChange={linkEvent(this, this.handleUserSettingsLangChange)}
+ class="ml-2 custom-select w-auto"
+ >
+ <option disabled>{i18n.t('language')}</option>
+ <option value="browser">{i18n.t('browser_default')}</option>
+ <option disabled>──</option>
+ {languages.map(lang => (
+ <option value={lang.code}>{lang.name}</option>
+ ))}
+ </select>
+ </div>
+ <div class="form-group">
+ <label>{i18n.t('theme')}</label>
+ <select
+ value={this.state.userSettingsForm.theme}
+ onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
+ class="ml-2 custom-select w-auto"
+ >
+ <option disabled>{i18n.t('theme')}</option>
+ {themes.map(theme => (
+ <option value={theme}>{theme}</option>
+ ))}
+ </select>
+ </div>
+ <form className="form-group">
+ <label>
+ <div class="mr-2">{i18n.t('sort_type')}</div>
+ </label>
+ <ListingTypeSelect
+ type_={
+ Object.values(ListingType)[
+ this.state.userSettingsForm.default_listing_type
+ ]
+ }
+ onChange={this.handleUserSettingsListingTypeChange}
+ />
+ </form>
+ <form className="form-group">
+ <label>
+ <div class="mr-2">{i18n.t('type')}</div>
+ </label>
+ <SortSelect
+ sort={
+ Object.values(SortType)[
+ this.state.userSettingsForm.default_sort_type
+ ]
+ }
+ onChange={this.handleUserSettingsSortTypeChange}
+ />
+ </form>
+ <div class="form-group row">
+ <label class="col-lg-5 col-form-label">
+ {i18n.t('display_name')}
+ </label>
+ <div class="col-lg-7">
+ <input
+ type="text"
+ class="form-control"
+ placeholder={i18n.t('optional')}
+ value={this.state.userSettingsForm.preferred_username}
+ onInput={linkEvent(
+ this,
+ this.handleUserSettingsPreferredUsernameChange
+ )}
+ pattern="^(?!@)(.+)$"
+ minLength={3}
+ maxLength={20}
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-lg-3 col-form-label" htmlFor="user-bio">
+ {i18n.t('bio')}
+ </label>
+ <div class="col-lg-9">
+ <MarkdownTextArea
+ initialContent={this.state.userSettingsForm.bio}
+ onContentChange={this.handleUserSettingsBioChange}
+ maxLength={300}
+ hideNavigationWarnings
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-lg-3 col-form-label" htmlFor="user-email">
+ {i18n.t('email')}
+ </label>
+ <div class="col-lg-9">
+ <input
+ type="email"
+ id="user-email"
+ class="form-control"
+ placeholder={i18n.t('optional')}
+ value={this.state.userSettingsForm.email}
+ onInput={linkEvent(
+ this,
+ this.handleUserSettingsEmailChange
+ )}
+ minLength={3}
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-lg-5 col-form-label">
+ <a href={elementUrl} target="_blank" rel="noopener">
+ {i18n.t('matrix_user_id')}
+ </a>
+ </label>
+ <div class="col-lg-7">
+ <input
+ type="text"
+ class="form-control"
+ placeholder="@user:example.com"
+ value={this.state.userSettingsForm.matrix_user_id}
+ onInput={linkEvent(
+ this,
+ this.handleUserSettingsMatrixUserIdChange
+ )}
+ minLength={3}
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-lg-5 col-form-label" htmlFor="user-password">
+ {i18n.t('new_password')}
+ </label>
+ <div class="col-lg-7">
+ <input
+ type="password"
+ id="user-password"
+ class="form-control"
+ value={this.state.userSettingsForm.new_password}
+ autoComplete="new-password"
+ onInput={linkEvent(
+ this,
+ this.handleUserSettingsNewPasswordChange
+ )}
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label
+ class="col-lg-5 col-form-label"
+ htmlFor="user-verify-password"
+ >
+ {i18n.t('verify_password')}
+ </label>
+ <div class="col-lg-7">
+ <input
+ type="password"
+ id="user-verify-password"
+ class="form-control"
+ value={this.state.userSettingsForm.new_password_verify}
+ autoComplete="new-password"
+ onInput={linkEvent(
+ this,
+ this.handleUserSettingsNewPasswordVerifyChange
+ )}
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label
+ class="col-lg-5 col-form-label"
+ htmlFor="user-old-password"
+ >
+ {i18n.t('old_password')}
+ </label>
+ <div class="col-lg-7">
+ <input
+ type="password"
+ id="user-old-password"
+ class="form-control"
+ value={this.state.userSettingsForm.old_password}
+ autoComplete="new-password"
+ onInput={linkEvent(
+ this,
+ this.handleUserSettingsOldPasswordChange
+ )}
+ />
+ </div>
+ </div>
+ {this.state.siteRes.site.enable_nsfw && (
+ <div class="form-group">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="user-show-nsfw"
+ type="checkbox"
+ checked={this.state.userSettingsForm.show_nsfw}
+ onChange={linkEvent(
+ this,
+ this.handleUserSettingsShowNsfwChange
+ )}
+ />
+ <label class="form-check-label" htmlFor="user-show-nsfw">
+ {i18n.t('show_nsfw')}
+ </label>
+ </div>
+ </div>
+ )}
+ <div class="form-group">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="user-show-avatars"
+ type="checkbox"
+ checked={this.state.userSettingsForm.show_avatars}
+ onChange={linkEvent(
+ this,
+ this.handleUserSettingsShowAvatarsChange
+ )}
+ />
+ <label class="form-check-label" htmlFor="user-show-avatars">
+ {i18n.t('show_avatars')}
+ </label>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ id="user-send-notifications-to-email"
+ type="checkbox"
+ disabled={!this.state.userSettingsForm.email}
+ checked={
+ this.state.userSettingsForm.send_notifications_to_email
+ }
+ onChange={linkEvent(
+ this,
+ this.handleUserSettingsSendNotificationsToEmailChange
+ )}
+ />
+ <label
+ class="form-check-label"
+ htmlFor="user-send-notifications-to-email"
+ >
+ {i18n.t('send_notifications_to_email')}
+ </label>
+ </div>
+ </div>
+ <div class="form-group">
+ <button type="submit" class="btn btn-block btn-secondary mr-4">
+ {this.state.userSettingsLoading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ capitalizeFirstLetter(i18n.t('save'))
+ )}
+ </button>
+ </div>
+ <hr />
+ <div class="form-group mb-0">
+ <button
+ class="btn btn-block btn-danger"
+ onClick={linkEvent(
+ this,
+ this.handleDeleteAccountShowConfirmToggle
+ )}
+ >
+ {i18n.t('delete_account')}
+ </button>
+ {this.state.deleteAccountShowConfirm && (
+ <>
+ <div class="my-2 alert alert-danger" role="alert">
+ {i18n.t('delete_account_confirm')}
+ </div>
+ <input
+ type="password"
+ value={this.state.deleteAccountForm.password}
+ autoComplete="new-password"
+ onInput={linkEvent(
+ this,
+ this.handleDeleteAccountPasswordChange
+ )}
+ class="form-control my-2"
+ />
+ <button
+ class="btn btn-danger mr-4"
+ disabled={!this.state.deleteAccountForm.password}
+ onClick={linkEvent(this, this.handleDeleteAccount)}
+ >
+ {this.state.deleteAccountLoading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ capitalizeFirstLetter(i18n.t('delete'))
+ )}
+ </button>
+ <button
+ class="btn btn-secondary"
+ onClick={linkEvent(
+ this,
+ this.handleDeleteAccountShowConfirmToggle
+ )}
+ >
+ {i18n.t('cancel')}
+ </button>
+ </>
+ )}
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ moderates() {
+ return (
+ <div>
+ {this.state.moderates.length > 0 && (
+ <div class="card bg-transparent border-secondary mb-3">
+ <div class="card-body">
+ <h5>{i18n.t('moderates')}</h5>
+ <ul class="list-unstyled mb-0">
+ {this.state.moderates.map(community => (
+ <li>
+ <Link to={`/c/${community.community_name}`}>
+ {community.community_name}
+ </Link>
+ </li>
+ ))}
+ </ul>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ follows() {
+ return (
+ <div>
+ {this.state.follows.length > 0 && (
+ <div class="card bg-transparent border-secondary mb-3">
+ <div class="card-body">
+ <h5>{i18n.t('subscribed')}</h5>
+ <ul class="list-unstyled mb-0">
+ {this.state.follows.map(community => (
+ <li>
+ <Link to={`/c/${community.community_name}`}>
+ {community.community_name}
+ </Link>
+ </li>
+ ))}
+ </ul>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ updateUrl(paramUpdates: UrlParams) {
+ const page = paramUpdates.page || this.state.page;
+ const viewStr = paramUpdates.view || UserDetailsView[this.state.view];
+ const sortStr = paramUpdates.sort || this.state.sort;
+ this.props.history.push(
+ `/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${page}`
+ );
+ }
+
+ handlePageChange(page: number) {
+ this.updateUrl({ page });
+ }
+
+ handleSortChange(val: SortType) {
+ this.updateUrl({ sort: val, page: 1 });
+ }
+
+ handleViewChange(i: User, event: any) {
+ i.updateUrl({
+ view: UserDetailsView[Number(event.target.value)],
+ page: 1,
+ });
+ }
+
+ handleUserSettingsShowNsfwChange(i: User, event: any) {
+ i.state.userSettingsForm.show_nsfw = event.target.checked;
+ i.setState(i.state);
+ }
+
+ handleUserSettingsShowAvatarsChange(i: User, event: any) {
+ i.state.userSettingsForm.show_avatars = event.target.checked;
+ UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
+ i.setState(i.state);
+ }
+
+ handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
+ i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
+ i.setState(i.state);
+ }
+
+ handleUserSettingsThemeChange(i: User, event: any) {
+ i.state.userSettingsForm.theme = event.target.value;
+ setTheme(event.target.value, true);
+ i.setState(i.state);
+ }
+
+ handleUserSettingsLangChange(i: User, event: any) {
+ i.state.userSettingsForm.lang = event.target.value;
+ i18n.changeLanguage(getLanguage(i.state.userSettingsForm.lang));
+ i.setState(i.state);
+ }
+
+ handleUserSettingsSortTypeChange(val: SortType) {
+ this.state.userSettingsForm.default_sort_type = Object.keys(
+ SortType
+ ).indexOf(val);
+ this.setState(this.state);
+ }
+
+ handleUserSettingsListingTypeChange(val: ListingType) {
+ this.state.userSettingsForm.default_listing_type = Object.keys(
+ ListingType
+ ).indexOf(val);
+ this.setState(this.state);
+ }
+
+ handleUserSettingsEmailChange(i: User, event: any) {
+ i.state.userSettingsForm.email = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleUserSettingsBioChange(val: string) {
+ this.state.userSettingsForm.bio = val;
+ this.setState(this.state);
+ }
+
+ handleAvatarUpload(url: string) {
+ this.state.userSettingsForm.avatar = url;
+ this.setState(this.state);
+ }
+
+ handleAvatarRemove() {
+ this.state.userSettingsForm.avatar = '';
+ this.setState(this.state);
+ }
+
+ handleBannerUpload(url: string) {
+ this.state.userSettingsForm.banner = url;
+ this.setState(this.state);
+ }
+
+ handleBannerRemove() {
+ this.state.userSettingsForm.banner = '';
+ this.setState(this.state);
+ }
+
+ handleUserSettingsPreferredUsernameChange(i: User, event: any) {
+ i.state.userSettingsForm.preferred_username = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleUserSettingsMatrixUserIdChange(i: User, event: any) {
+ i.state.userSettingsForm.matrix_user_id = event.target.value;
+ if (
+ i.state.userSettingsForm.matrix_user_id == '' &&
+ !i.state.user.matrix_user_id
+ ) {
+ i.state.userSettingsForm.matrix_user_id = undefined;
+ }
+ i.setState(i.state);
+ }
+
+ handleUserSettingsNewPasswordChange(i: User, event: any) {
+ i.state.userSettingsForm.new_password = event.target.value;
+ if (i.state.userSettingsForm.new_password == '') {
+ i.state.userSettingsForm.new_password = undefined;
+ }
+ i.setState(i.state);
+ }
+
+ handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
+ i.state.userSettingsForm.new_password_verify = event.target.value;
+ if (i.state.userSettingsForm.new_password_verify == '') {
+ i.state.userSettingsForm.new_password_verify = undefined;
+ }
+ i.setState(i.state);
+ }
+
+ handleUserSettingsOldPasswordChange(i: User, event: any) {
+ i.state.userSettingsForm.old_password = event.target.value;
+ if (i.state.userSettingsForm.old_password == '') {
+ i.state.userSettingsForm.old_password = undefined;
+ }
+ i.setState(i.state);
+ }
+
+ handleUserSettingsSubmit(i: User, event: any) {
+ event.preventDefault();
+ i.state.userSettingsLoading = true;
+ i.setState(i.state);
+
+ WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
+ }
+
+ handleDeleteAccountShowConfirmToggle(i: User, event: any) {
+ event.preventDefault();
+ i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
+ i.setState(i.state);
+ }
+
+ handleDeleteAccountPasswordChange(i: User, event: any) {
+ i.state.deleteAccountForm.password = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleLogoutClick(i: User) {
+ UserService.Instance.logout();
+ i.context.router.history.push('/');
+ }
+
+ handleDeleteAccount(i: User, event: any) {
+ event.preventDefault();
+ i.state.deleteAccountLoading = true;
+ i.setState(i.state);
+
+ WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
+ }
+
+ parseMessage(msg: WebSocketJsonResponse) {
+ console.log(msg);
+ const res = wsJsonToRes(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), 'danger');
+ if (msg.error == 'couldnt_find_that_username_or_email') {
+ this.context.router.history.push('/');
+ }
+ this.setState({
+ deleteAccountLoading: false,
+ userSettingsLoading: false,
+ });
+ return;
+ } else if (res.op == UserOperation.GetUserDetails) {
+ // Since the UserDetails contains posts/comments as well as some general user info we listen here as well
+ // and set the parent state if it is not set or differs
+ const data = res.data as UserDetailsResponse;
+
+ if (this.state.user.id !== data.user.id) {
+ this.state.user = data.user;
+ this.state.follows = data.follows;
+ this.state.moderates = data.moderates;
+
+ if (this.isCurrentUser) {
+ this.state.userSettingsForm.show_nsfw =
+ UserService.Instance.user.show_nsfw;
+ this.state.userSettingsForm.theme = UserService.Instance.user.theme
+ ? UserService.Instance.user.theme
+ : 'darkly';
+ this.state.userSettingsForm.default_sort_type =
+ UserService.Instance.user.default_sort_type;
+ this.state.userSettingsForm.default_listing_type =
+ UserService.Instance.user.default_listing_type;
+ this.state.userSettingsForm.lang = UserService.Instance.user.lang;
+ this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
+ this.state.userSettingsForm.banner = UserService.Instance.user.banner;
+ this.state.userSettingsForm.preferred_username =
+ UserService.Instance.user.preferred_username;
+ this.state.userSettingsForm.show_avatars =
+ UserService.Instance.user.show_avatars;
+ this.state.userSettingsForm.email = UserService.Instance.user.email;
+ this.state.userSettingsForm.bio = UserService.Instance.user.bio;
+ this.state.userSettingsForm.send_notifications_to_email =
+ UserService.Instance.user.send_notifications_to_email;
+ this.state.userSettingsForm.matrix_user_id =
+ UserService.Instance.user.matrix_user_id;
+ }
+ this.state.loading = false;
+ this.setState(this.state);
+ }
+ } else if (res.op == UserOperation.SaveUserSettings) {
+ const data = res.data as LoginResponse;
+ UserService.Instance.login(data);
+ this.state.user.bio = this.state.userSettingsForm.bio;
+ this.state.user.preferred_username = this.state.userSettingsForm.preferred_username;
+ this.state.user.banner = this.state.userSettingsForm.banner;
+ this.state.user.avatar = this.state.userSettingsForm.avatar;
+ this.state.userSettingsLoading = false;
+ this.setState(this.state);
+
+ window.scrollTo(0, 0);
+ } else if (res.op == UserOperation.DeleteAccount) {
+ this.setState({
+ deleteAccountLoading: false,
+ deleteAccountShowConfirm: false,
+ });
+ this.context.router.history.push('/');
+ } else if (res.op == UserOperation.GetSite) {
+ const data = res.data as GetSiteResponse;
+ this.state.siteRes = data;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.AddAdmin) {
+ const data = res.data as AddAdminResponse;
+ this.state.siteRes.admins = data.admins;
+ this.setState(this.state);
+ }
+ }
+}
--- /dev/null
+// TODO
+// const host = `${window.location.hostname}`;
+// const port = `${
+// window.location.port == '4444' ? '8536' : window.location.port
+// }`;
+// const endpoint = `${host}:${port}`;
+
+// export const wsUri = `${
+// window.location.protocol == 'https:' ? 'wss://' : 'ws://'
+// }${endpoint}/api/v1/ws`;
+
+const host = '192.168.50.60';
+const port = 8536;
+const endpoint = `${host}:${port}`;
+export const wsUri = `ws://${endpoint}/api/v1/ws`;
--- /dev/null
+import i18next from 'i18next';
+import { getLanguage } from './utils';
+import { en } from './translations/en';
+import { el } from './translations/el';
+import { eu } from './translations/eu';
+import { eo } from './translations/eo';
+import { es } from './translations/es';
+import { de } from './translations/de';
+import { fr } from './translations/fr';
+import { sv } from './translations/sv';
+import { ru } from './translations/ru';
+import { zh } from './translations/zh';
+import { nl } from './translations/nl';
+import { it } from './translations/it';
+import { fi } from './translations/fi';
+import { ca } from './translations/ca';
+import { fa } from './translations/fa';
+import { hi } from './translations/hi';
+import { pl } from './translations/pl';
+import { pt_BR } from './translations/pt_BR';
+import { ja } from './translations/ja';
+import { ka } from './translations/ka';
+import { gl } from './translations/gl';
+import { tr } from './translations/tr';
+import { hu } from './translations/hu';
+import { uk } from './translations/uk';
+import { sq } from './translations/sq';
+import { km } from './translations/km';
+import { ga } from './translations/ga';
+import { sr_Latn } from './translations/sr_Latn';
+
+// https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66
+const resources = {
+ en,
+ el,
+ eu,
+ eo,
+ es,
+ ka,
+ hi,
+ de,
+ zh,
+ fr,
+ sv,
+ ru,
+ nl,
+ it,
+ fi,
+ ca,
+ fa,
+ pl,
+ pt_BR,
+ ja,
+ gl,
+ tr,
+ hu,
+ uk,
+ sq,
+ km,
+ ga,
+ sr_Latn,
+};
+
+function format(value: any, format: any, lng: any): any {
+ return format === 'uppercase' ? value.toUpperCase() : value;
+}
+
+i18next.init({
+ debug: false,
+ // load: 'languageOnly',
+
+ // initImmediate: false,
+ lng: getLanguage(),
+ fallbackLng: 'en',
+ resources,
+ interpolation: { format },
+});
+
+export { i18next as i18n, resources };
--- /dev/null
+export enum CommentSortType {
+ Hot,
+ Top,
+ New,
+ Old,
+}
+
+export enum CommentViewType {
+ Tree,
+ Chat,
+}
+
+export enum DataType {
+ Post,
+ Comment,
+}
+
+export enum BanType {
+ Community,
+ Site,
+}
+
+export enum UserDetailsView {
+ Overview,
+ Comments,
+ Posts,
+ Saved,
+}
--- /dev/null
+import { BrowserRouter, Route, Switch } from 'inferno-router';
+import { IRouteProps } from 'inferno-router/dist/Route';
+import { Main } from './components/main';
+import { Navbar } from './components/navbar';
+import { Footer } from './components/footer';
+import { Login } from './components/login';
+import { CreatePost } from './components/create-post';
+import { CreateCommunity } from './components/create-community';
+import { CreatePrivateMessage } from './components/create-private-message';
+import { PasswordChange } from './components/password_change';
+import { Post } from './components/post';
+import { Community } from './components/community';
+import { Communities } from './components/communities';
+import { User } from './components/user';
+import { Modlog } from './components/modlog';
+import { Setup } from './components/setup';
+import { AdminSettings } from './components/admin-settings';
+import { Inbox } from './components/inbox';
+import { Search } from './components/search';
+import { Sponsors } from './components/sponsors';
+import { Instances } from './components/instances';
+
+export const routes: IRouteProps[] = [
+ { exact: true, path: `/`, component: Main },
+ {
+ path: `/home/data_type/:data_type/listing_type/:listing_type/sort/:sort/page/:page`,
+ component: Main,
+ },
+ { path: `/login`, component: Login },
+ { path: `/create_post`, component: CreatePost },
+ { path: `/create_community`, component: CreateCommunity },
+ {
+ path: `/create_private_message`,
+ component: CreatePrivateMessage,
+ },
+ {
+ path: `/communities/page/:page`,
+ component: Communities,
+ },
+ { path: `/communities`, component: Communities },
+ {
+ path: `/post/:id/comment/:comment_id`,
+ component: Post,
+ },
+ { path: `/post/:id`, component: Post },
+ {
+ path: `/c/:name/data_type/:data_type/sort/:sort/page/:page`,
+ component: Community,
+ },
+ { path: `/community/:id`, component: Community },
+ { path: `/c/:name`, component: Community },
+ {
+ path: `/u/:username/view/:view/sort/:sort/page/:page`,
+ component: User,
+ },
+ { path: `/user/:id`, component: User },
+ { path: `/u/:username`, component: User },
+ { path: `/inbox`, component: Inbox },
+ {
+ path: `/modlog/community/:community_id`,
+ component: Modlog,
+ },
+ { path: `/modlog`, component: Modlog },
+ { path: `/setup`, component: Setup },
+ { path: `/admin`, component: AdminSettings },
+ {
+ path: `/search/q/:q/type/:type/sort/:sort/page/:page`,
+ component: Search,
+ },
+ { path: `/search`, component: Search },
+ { path: `/sponsors`, component: Sponsors },
+ {
+ path: `/password_change/:token`,
+ component: PasswordChange,
+ },
+ { path: `/instances`, component: Instances },
+];
--- /dev/null
+// import Cookies from 'js-cookie';
+import IsomorphicCookie from 'isomorphic-cookie';
+import { User, LoginResponse } from 'lemmy-js-client';
+import { setTheme } from '../utils';
+import jwt_decode from 'jwt-decode';
+import { Subject, BehaviorSubject } from 'rxjs';
+
+interface Claims {
+ id: number;
+ iss: string;
+}
+
+export class UserService {
+ private static _instance: UserService;
+ public user: User;
+ public claims: Claims;
+ public jwtSub: Subject<string> = new Subject<string>();
+ public unreadCountSub: BehaviorSubject<number> = new BehaviorSubject<number>(
+ 0
+ );
+
+ private constructor() {
+ let jwt = IsomorphicCookie.load('jwt');
+ if (jwt) {
+ this.setClaims(jwt);
+ } else {
+ setTheme();
+ console.log('No JWT cookie found.');
+ }
+ }
+
+ public login(res: LoginResponse) {
+ this.setClaims(res.jwt);
+ IsomorphicCookie.save('jwt', res.jwt, { expires: 365 });
+ console.log('jwt cookie set');
+ }
+
+ public logout() {
+ this.claims = undefined;
+ this.user = undefined;
+ IsomorphicCookie.remove('jwt');
+ setTheme();
+ this.jwtSub.next();
+ console.log('Logged out.');
+ }
+
+ public get auth(): string {
+ return IsomorphicCookie.load('jwt');
+ }
+
+ private setClaims(jwt: string) {
+ this.claims = jwt_decode(jwt);
+ this.jwtSub.next(jwt);
+ }
+
+ public static get Instance() {
+ return this._instance || (this._instance = new this());
+ }
+}
--- /dev/null
+import { wsUri } from '../env';
+import {
+ LemmyWebsocket,
+ LoginForm,
+ RegisterForm,
+ CommunityForm,
+ DeleteCommunityForm,
+ RemoveCommunityForm,
+ PostForm,
+ DeletePostForm,
+ RemovePostForm,
+ LockPostForm,
+ StickyPostForm,
+ SavePostForm,
+ CommentForm,
+ DeleteCommentForm,
+ RemoveCommentForm,
+ MarkCommentAsReadForm,
+ SaveCommentForm,
+ CommentLikeForm,
+ GetPostForm,
+ GetPostsForm,
+ CreatePostLikeForm,
+ GetCommunityForm,
+ FollowCommunityForm,
+ GetFollowedCommunitiesForm,
+ GetUserDetailsForm,
+ ListCommunitiesForm,
+ GetModlogForm,
+ BanFromCommunityForm,
+ AddModToCommunityForm,
+ TransferCommunityForm,
+ AddAdminForm,
+ TransferSiteForm,
+ BanUserForm,
+ SiteForm,
+ UserView,
+ GetRepliesForm,
+ GetUserMentionsForm,
+ MarkUserMentionAsReadForm,
+ SearchForm,
+ UserSettingsForm,
+ DeleteAccountForm,
+ PasswordResetForm,
+ PasswordChangeForm,
+ PrivateMessageForm,
+ EditPrivateMessageForm,
+ DeletePrivateMessageForm,
+ MarkPrivateMessageAsReadForm,
+ GetPrivateMessagesForm,
+ GetCommentsForm,
+ UserJoinForm,
+ GetSiteConfig,
+ GetSiteForm,
+ SiteConfigForm,
+ MarkAllAsReadForm,
+ WebSocketJsonResponse,
+} from 'lemmy-js-client';
+import { UserService } from './';
+import { i18n } from '../i18next';
+import { toast, isBrowser } from '../utils';
+import { Observable } from 'rxjs';
+import { share } from 'rxjs/operators';
+import WebSocket from 'isomorphic-ws';
+import {
+ Options as WSOptions,
+ default as ReconnectingWebSocket,
+} from 'reconnecting-websocket';
+
+export class WebSocketService {
+ private static _instance: WebSocketService;
+ public ws: ReconnectingWebSocket;
+ public wsOptions: WSOptions = {
+ WebSocket: WebSocket,
+ connectionTimeout: 1000,
+ maxRetries: 10,
+ };
+ public subject: Observable<any>;
+
+ public admins: UserView[];
+ public banned: UserView[];
+ private client = new LemmyWebsocket();
+
+ private constructor() {
+ this.ws = new ReconnectingWebSocket(wsUri, [], this.wsOptions);
+ let firstConnect = true;
+
+ this.subject = Observable.create((obs: any) => {
+ this.ws.onmessage = e => {
+ obs.next(JSON.parse(e.data));
+ };
+ this.ws.onopen = () => {
+ console.log(`Connected to ${wsUri}`);
+
+ if (!firstConnect) {
+ let res: WebSocketJsonResponse = {
+ reconnect: true,
+ };
+ obs.next(res);
+ }
+
+ firstConnect = false;
+ };
+ }).pipe(share());
+ }
+
+ public static get Instance() {
+ return this._instance || (this._instance = new this());
+ }
+
+ public userJoin() {
+ let form: UserJoinForm = { auth: UserService.Instance.auth };
+ this.ws.send(this.client.userJoin(form));
+ }
+
+ public login(form: LoginForm) {
+ this.ws.send(this.client.login(form));
+ }
+
+ public register(form: RegisterForm) {
+ this.ws.send(this.client.register(form));
+ }
+
+ public getCaptcha() {
+ this.ws.send(this.client.getCaptcha());
+ }
+
+ public createCommunity(form: CommunityForm) {
+ this.setAuth(form); // TODO all these setauths at some point would be good to make required
+ this.ws.send(this.client.createCommunity(form));
+ }
+
+ public editCommunity(form: CommunityForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.editCommunity(form));
+ }
+
+ public deleteCommunity(form: DeleteCommunityForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.deleteCommunity(form));
+ }
+
+ public removeCommunity(form: RemoveCommunityForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.removeCommunity(form));
+ }
+
+ public followCommunity(form: FollowCommunityForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.followCommunity(form));
+ }
+
+ public listCommunities(form: ListCommunitiesForm) {
+ this.setAuth(form, false);
+ this.ws.send(this.client.listCommunities(form));
+ }
+
+ public getFollowedCommunities() {
+ let form: GetFollowedCommunitiesForm = { auth: UserService.Instance.auth };
+ this.ws.send(this.client.getFollowedCommunities(form));
+ }
+
+ public listCategories() {
+ this.ws.send(this.client.listCategories());
+ }
+
+ public createPost(form: PostForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.createPost(form));
+ }
+
+ public getPost(form: GetPostForm) {
+ this.setAuth(form, false);
+ this.ws.send(this.client.getPost(form));
+ }
+
+ public getCommunity(form: GetCommunityForm) {
+ this.setAuth(form, false);
+ this.ws.send(this.client.getCommunity(form));
+ }
+
+ public createComment(form: CommentForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.createComment(form));
+ }
+
+ public editComment(form: CommentForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.editComment(form));
+ }
+
+ public deleteComment(form: DeleteCommentForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.deleteComment(form));
+ }
+
+ public removeComment(form: RemoveCommentForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.removeComment(form));
+ }
+
+ public markCommentAsRead(form: MarkCommentAsReadForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.markCommentAsRead(form));
+ }
+
+ public likeComment(form: CommentLikeForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.likeComment(form));
+ }
+
+ public saveComment(form: SaveCommentForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.saveComment(form));
+ }
+
+ public getPosts(form: GetPostsForm) {
+ this.setAuth(form, false);
+ this.ws.send(this.client.getPosts(form));
+ }
+
+ public getComments(form: GetCommentsForm) {
+ this.setAuth(form, false);
+ this.ws.send(this.client.getComments(form));
+ }
+
+ public likePost(form: CreatePostLikeForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.likePost(form));
+ }
+
+ public editPost(form: PostForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.editPost(form));
+ }
+
+ public deletePost(form: DeletePostForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.deletePost(form));
+ }
+
+ public removePost(form: RemovePostForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.removePost(form));
+ }
+
+ public lockPost(form: LockPostForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.lockPost(form));
+ }
+
+ public stickyPost(form: StickyPostForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.stickyPost(form));
+ }
+
+ public savePost(form: SavePostForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.savePost(form));
+ }
+
+ public banFromCommunity(form: BanFromCommunityForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.banFromCommunity(form));
+ }
+
+ public addModToCommunity(form: AddModToCommunityForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.addModToCommunity(form));
+ }
+
+ public transferCommunity(form: TransferCommunityForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.transferCommunity(form));
+ }
+
+ public transferSite(form: TransferSiteForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.transferSite(form));
+ }
+
+ public banUser(form: BanUserForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.banUser(form));
+ }
+
+ public addAdmin(form: AddAdminForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.addAdmin(form));
+ }
+
+ public getUserDetails(form: GetUserDetailsForm) {
+ this.setAuth(form, false);
+ this.ws.send(this.client.getUserDetails(form));
+ }
+
+ public getReplies(form: GetRepliesForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.getReplies(form));
+ }
+
+ public getUserMentions(form: GetUserMentionsForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.getUserMentions(form));
+ }
+
+ public markUserMentionAsRead(form: MarkUserMentionAsReadForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.markUserMentionAsRead(form));
+ }
+
+ public getModlog(form: GetModlogForm) {
+ this.ws.send(this.client.getModlog(form));
+ }
+
+ public createSite(form: SiteForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.createSite(form));
+ }
+
+ public editSite(form: SiteForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.editSite(form));
+ }
+
+ public getSite(form: GetSiteForm = {}) {
+ this.setAuth(form, false);
+ this.ws.send(this.client.getSite(form));
+ }
+
+ public getSiteConfig() {
+ let form: GetSiteConfig = {};
+ this.setAuth(form);
+ this.ws.send(this.client.getSiteConfig(form));
+ }
+
+ public search(form: SearchForm) {
+ this.setAuth(form, false);
+ this.ws.send(this.client.search(form));
+ }
+
+ public markAllAsRead() {
+ let form: MarkAllAsReadForm;
+ this.setAuth(form);
+ this.ws.send(this.client.markAllAsRead(form));
+ }
+
+ public saveUserSettings(form: UserSettingsForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.saveUserSettings(form));
+ }
+
+ public deleteAccount(form: DeleteAccountForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.deleteAccount(form));
+ }
+
+ public passwordReset(form: PasswordResetForm) {
+ this.ws.send(this.client.passwordReset(form));
+ }
+
+ public passwordChange(form: PasswordChangeForm) {
+ this.ws.send(this.client.passwordChange(form));
+ }
+
+ public createPrivateMessage(form: PrivateMessageForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.createPrivateMessage(form));
+ }
+
+ public editPrivateMessage(form: EditPrivateMessageForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.editPrivateMessage(form));
+ }
+
+ public deletePrivateMessage(form: DeletePrivateMessageForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.deletePrivateMessage(form));
+ }
+
+ public markPrivateMessageAsRead(form: MarkPrivateMessageAsReadForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.markPrivateMessageAsRead(form));
+ }
+
+ public getPrivateMessages(form: GetPrivateMessagesForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.getPrivateMessages(form));
+ }
+
+ public saveSiteConfig(form: SiteConfigForm) {
+ this.setAuth(form);
+ this.ws.send(this.client.saveSiteConfig(form));
+ }
+
+ private setAuth(obj: any, throwErr: boolean = true) {
+ obj.auth = UserService.Instance.auth;
+ if (obj.auth == null && throwErr) {
+ toast(i18n.t('not_logged_in'), 'danger');
+ throw 'Not logged in';
+ }
+ }
+}
+
+if (isBrowser()) {
+ window.onbeforeunload = () => {
+ WebSocketService.Instance.ws.close();
+ };
+}
--- /dev/null
+export { UserService } from './UserService';
+export { WebSocketService } from './WebSocketService';
--- /dev/null
+import 'moment/locale/es';
+import 'moment/locale/el';
+import 'moment/locale/eu';
+import 'moment/locale/eo';
+import 'moment/locale/de';
+import 'moment/locale/zh-cn';
+import 'moment/locale/fr';
+import 'moment/locale/sv';
+import 'moment/locale/ru';
+import 'moment/locale/nl';
+import 'moment/locale/it';
+import 'moment/locale/fi';
+import 'moment/locale/ca';
+import 'moment/locale/fa';
+import 'moment/locale/pl';
+import 'moment/locale/pt-br';
+import 'moment/locale/ja';
+import 'moment/locale/ka';
+import 'moment/locale/hi';
+import 'moment/locale/gl';
+import 'moment/locale/tr';
+import 'moment/locale/hu';
+import 'moment/locale/uk';
+import 'moment/locale/sq';
+import 'moment/locale/km';
+import 'moment/locale/ga';
+import 'moment/locale/sr';
+
+import {
+ UserOperation,
+ Comment,
+ CommentNode as CommentNodeI,
+ Post,
+ PrivateMessage,
+ User,
+ SortType,
+ ListingType,
+ SearchType,
+ WebSocketResponse,
+ WebSocketJsonResponse,
+ SearchForm,
+ SearchResponse,
+ CommentResponse,
+ PostResponse,
+} from 'lemmy-js-client';
+
+import { CommentSortType, DataType } from './interfaces';
+import { UserService, WebSocketService } from './services';
+
+// import Tribute from 'tributejs';
+import markdown_it from 'markdown-it';
+import markdown_it_sub from 'markdown-it-sub';
+import markdown_it_sup from 'markdown-it-sup';
+import markdownitEmoji from 'markdown-it-emoji/light';
+import markdown_it_container from 'markdown-it-container';
+import emojiShortName from 'emoji-short-name';
+import Toastify from 'toastify-js';
+import tippy from 'tippy.js';
+import moment from 'moment';
+
+export const favIconUrl = '/static/assets/favicon.svg';
+export const favIconPngUrl = '/static/assets/apple-touch-icon.png';
+// TODO
+// export const defaultFavIcon = `${window.location.protocol}//${window.location.host}${favIconPngUrl}`;
+export const defaultFavIcon = 'test';
+export const repoUrl = 'https://github.com/LemmyNet/lemmy';
+export const helpGuideUrl = '/docs/about_guide.html';
+export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
+export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
+export const archiveUrl = 'https://archive.is';
+export const elementUrl = 'https://element.io/';
+
+export const postRefetchSeconds: number = 60 * 1000;
+export const fetchLimit: number = 20;
+export const mentionDropdownFetchLimit = 10;
+
+export const languages = [
+ { code: 'ca', name: 'Català' },
+ { code: 'en', name: 'English' },
+ { code: 'el', name: 'Ελληνικά' },
+ { code: 'eu', name: 'Euskara' },
+ { code: 'eo', name: 'Esperanto' },
+ { code: 'es', name: 'Español' },
+ { code: 'de', name: 'Deutsch' },
+ { code: 'ga', name: 'Gaeilge' },
+ { code: 'gl', name: 'Galego' },
+ { code: 'hu', name: 'Magyar Nyelv' },
+ { code: 'ka', name: 'ქართული ენა' },
+ { code: 'km', name: 'ភាសាខ្មែរ' },
+ { code: 'hi', name: 'मानक हिन्दी' },
+ { code: 'fa', name: 'فارسی' },
+ { code: 'ja', name: '日本語' },
+ { code: 'pl', name: 'Polski' },
+ { code: 'pt_BR', name: 'Português Brasileiro' },
+ { code: 'zh', name: '中文' },
+ { code: 'fi', name: 'Suomi' },
+ { code: 'fr', name: 'Français' },
+ { code: 'sv', name: 'Svenska' },
+ { code: 'sq', name: 'Shqip' },
+ { code: 'sr_Latn', name: 'srpski' },
+ { code: 'tr', name: 'Türkçe' },
+ { code: 'uk', name: 'Українська Mова' },
+ { code: 'ru', name: 'Русский' },
+ { code: 'nl', name: 'Nederlands' },
+ { code: 'it', name: 'Italiano' },
+];
+
+export const themes = [
+ 'litera',
+ 'materia',
+ 'minty',
+ 'solar',
+ 'united',
+ 'cyborg',
+ 'darkly',
+ 'journal',
+ 'sketchy',
+ 'vaporwave',
+ 'vaporwave-dark',
+ 'i386',
+ 'litely',
+];
+
+const DEFAULT_ALPHABET =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+function getRandomCharFromAlphabet(alphabet: string): string {
+ return alphabet.charAt(Math.floor(Math.random() * alphabet.length));
+}
+
+export function randomStr(
+ idDesiredLength: number = 20,
+ alphabet = DEFAULT_ALPHABET
+): string {
+ /**
+ * Create n-long array and map it to random chars from given alphabet.
+ * Then join individual chars as string
+ */
+ return Array.from({ length: idDesiredLength })
+ .map(() => {
+ return getRandomCharFromAlphabet(alphabet);
+ })
+ .join('');
+}
+
+export function wsJsonToRes(msg: WebSocketJsonResponse): WebSocketResponse {
+ let opStr: string = msg.op;
+ return {
+ op: UserOperation[opStr],
+ data: msg.data,
+ };
+}
+
+export const md = new markdown_it({
+ html: false,
+ linkify: true,
+ typographer: true,
+})
+ .use(markdown_it_sub)
+ .use(markdown_it_sup)
+ .use(markdown_it_container, 'spoiler', {
+ validate: function (params: any) {
+ return params.trim().match(/^spoiler\s+(.*)$/);
+ },
+
+ render: function (tokens: any, idx: any) {
+ var m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/);
+
+ if (tokens[idx].nesting === 1) {
+ // opening tag
+ return `<details><summary> ${md.utils.escapeHtml(m[1])} </summary>\n`;
+ } else {
+ // closing tag
+ return '</details>\n';
+ }
+ },
+ })
+ .use(markdownitEmoji, {
+ defs: objectFlip(emojiShortName),
+ });
+
+export function hotRankComment(comment: Comment): number {
+ return hotRank(comment.score, comment.published);
+}
+
+export function hotRankPost(post: Post): number {
+ return hotRank(post.score, post.newest_activity_time);
+}
+
+export function hotRank(score: number, timeStr: string): number {
+ // Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
+ let date: Date = new Date(timeStr + 'Z'); // Add Z to convert from UTC date
+ let now: Date = new Date();
+ let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
+
+ let rank =
+ (10000 * Math.log10(Math.max(1, 3 + score))) /
+ Math.pow(hoursElapsed + 2, 1.8);
+
+ // console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`);
+
+ return rank;
+}
+
+export function mdToHtml(text: string) {
+ return { __html: md.render(text) };
+}
+
+export function getUnixTime(text: string): number {
+ return text ? new Date(text).getTime() / 1000 : undefined;
+}
+
+export function addTypeInfo<T>(
+ arr: T[],
+ name: string
+): { type_: string; data: T }[] {
+ return arr.map(e => {
+ return { type_: name, data: e };
+ });
+}
+
+export function canMod(
+ user: User,
+ modIds: number[],
+ creator_id: number,
+ onSelf: boolean = false
+): boolean {
+ // You can do moderator actions only on the mods added after you.
+ if (user) {
+ let yourIndex = modIds.findIndex(id => id == user.id);
+ if (yourIndex == -1) {
+ return false;
+ } else {
+ // onSelf +1 on mod actions not for yourself, IE ban, remove, etc
+ modIds = modIds.slice(0, yourIndex + (onSelf ? 0 : 1));
+ return !modIds.includes(creator_id);
+ }
+ } else {
+ return false;
+ }
+}
+
+export function isMod(modIds: number[], creator_id: number): boolean {
+ return modIds.includes(creator_id);
+}
+
+const imageRegex = new RegExp(
+ /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/
+);
+const videoRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:mp4))`);
+
+export function isImage(url: string) {
+ return imageRegex.test(url);
+}
+
+export function isVideo(url: string) {
+ return videoRegex.test(url);
+}
+
+// TODO this broke
+export function validURL(str: string) {
+ // try {
+ return !!new URL(str);
+ // } catch {
+ // return false;
+ // }
+}
+
+export function validEmail(email: string) {
+ let re = /^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/;
+ return re.test(String(email).toLowerCase());
+}
+
+export function capitalizeFirstLetter(str: string): string {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}
+
+export function routeSortTypeToEnum(sort: string): SortType {
+ return SortType[sort];
+}
+
+export function routeListingTypeToEnum(type: string): ListingType {
+ return ListingType[type];
+}
+
+export function routeDataTypeToEnum(type: string): DataType {
+ return DataType[capitalizeFirstLetter(type)];
+}
+
+export function routeSearchTypeToEnum(type: string): SearchType {
+ return SearchType[capitalizeFirstLetter(type)];
+}
+
+export async function getPageTitle(url: string) {
+ let res = await fetch(`/iframely/oembed?url=${url}`).then(res => res.json());
+ let title = await res.title;
+ return title;
+}
+
+export function debounce(
+ func: any,
+ wait: number = 1000,
+ immediate: boolean = false
+) {
+ // 'private' variable for instance
+ // The returned function will be able to reference this due to closure.
+ // Each call to the returned function will share this common timer.
+ let timeout: any;
+
+ // Calling debounce returns a new anonymous function
+ return function () {
+ // reference the context and args for the setTimeout function
+ var context = this,
+ args = arguments;
+
+ // Should the function be called now? If immediate is true
+ // and not already in a timeout then the answer is: Yes
+ var callNow = immediate && !timeout;
+
+ // This is the basic debounce behaviour where you can call this
+ // function several times, but it will only execute once
+ // [before or after imposing a delay].
+ // Each time the returned function is called, the timer starts over.
+ clearTimeout(timeout);
+
+ // Set the new timeout
+ timeout = setTimeout(function () {
+ // Inside the timeout function, clear the timeout variable
+ // which will let the next execution run when in 'immediate' mode
+ timeout = null;
+
+ // Check if the function already ran with the immediate flag
+ if (!immediate) {
+ // Call the original function with apply
+ // apply lets you define the 'this' object as well as the arguments
+ // (both captured before setTimeout)
+ func.apply(context, args);
+ }
+ }, wait);
+
+ // Immediate mode and no wait timer? Execute the function..
+ if (callNow) func.apply(context, args);
+ };
+}
+
+// TODO
+export function getLanguage(override?: string): string {
+ return 'en';
+ // let user = UserService.Instance.user;
+ // let lang = override || (user && user.lang ? user.lang : 'browser');
+
+ // if (lang == 'browser') {
+ // return getBrowserLanguage();
+ // } else {
+ // return lang;
+ // }
+}
+
+// TODO
+export function getBrowserLanguage(): string {
+ return navigator.language;
+}
+
+export function getMomentLanguage(): string {
+ let lang = getLanguage();
+ if (lang.startsWith('zh')) {
+ lang = 'zh-cn';
+ } else if (lang.startsWith('sv')) {
+ lang = 'sv';
+ } else if (lang.startsWith('fr')) {
+ lang = 'fr';
+ } else if (lang.startsWith('de')) {
+ lang = 'de';
+ } else if (lang.startsWith('ru')) {
+ lang = 'ru';
+ } else if (lang.startsWith('es')) {
+ lang = 'es';
+ } else if (lang.startsWith('eo')) {
+ lang = 'eo';
+ } else if (lang.startsWith('nl')) {
+ lang = 'nl';
+ } else if (lang.startsWith('it')) {
+ lang = 'it';
+ } else if (lang.startsWith('fi')) {
+ lang = 'fi';
+ } else if (lang.startsWith('ca')) {
+ lang = 'ca';
+ } else if (lang.startsWith('fa')) {
+ lang = 'fa';
+ } else if (lang.startsWith('pl')) {
+ lang = 'pl';
+ } else if (lang.startsWith('pt')) {
+ lang = 'pt-br';
+ } else if (lang.startsWith('ja')) {
+ lang = 'ja';
+ } else if (lang.startsWith('ka')) {
+ lang = 'ka';
+ } else if (lang.startsWith('hi')) {
+ lang = 'hi';
+ } else if (lang.startsWith('el')) {
+ lang = 'el';
+ } else if (lang.startsWith('eu')) {
+ lang = 'eu';
+ } else if (lang.startsWith('gl')) {
+ lang = 'gl';
+ } else if (lang.startsWith('tr')) {
+ lang = 'tr';
+ } else if (lang.startsWith('hu')) {
+ lang = 'hu';
+ } else if (lang.startsWith('uk')) {
+ lang = 'uk';
+ } else if (lang.startsWith('sq')) {
+ lang = 'sq';
+ } else if (lang.startsWith('km')) {
+ lang = 'km';
+ } else if (lang.startsWith('ga')) {
+ lang = 'ga';
+ } else if (lang.startsWith('sr')) {
+ lang = 'sr';
+ } else {
+ lang = 'en';
+ }
+ return lang;
+}
+
+// TODO
+export function setTheme(theme: string = 'darkly', loggedIn: boolean = false) {
+ // unload all the other themes
+ // for (var i = 0; i < themes.length; i++) {
+ // let styleSheet = document.getElementById(themes[i]);
+ // if (styleSheet) {
+ // styleSheet.setAttribute('disabled', 'disabled');
+ // }
+ // }
+ // // if the user is not logged in, we load the default themes and let the browser decide
+ // if (!loggedIn) {
+ // document.getElementById('default-light').removeAttribute('disabled');
+ // document.getElementById('default-dark').removeAttribute('disabled');
+ // } else {
+ // document
+ // .getElementById('default-light')
+ // .setAttribute('disabled', 'disabled');
+ // document
+ // .getElementById('default-dark')
+ // .setAttribute('disabled', 'disabled');
+ // // Load the theme dynamically
+ // let cssLoc = `/static/assets/css/themes/${theme}.min.css`;
+ // loadCss(theme, cssLoc);
+ // document.getElementById(theme).removeAttribute('disabled');
+ // }
+}
+
+export function loadCss(id: string, loc: string) {
+ if (!document.getElementById(id)) {
+ var head = document.getElementsByTagName('head')[0];
+ var link = document.createElement('link');
+ link.id = id;
+ link.rel = 'stylesheet';
+ link.type = 'text/css';
+ link.href = loc;
+ link.media = 'all';
+ head.appendChild(link);
+ }
+}
+
+export function objectFlip(obj: any) {
+ const ret = {};
+ Object.keys(obj).forEach(key => {
+ ret[obj[key]] = key;
+ });
+ return ret;
+}
+
+export function pictrsAvatarThumbnail(src: string): string {
+ // sample url: http://localhost:8535/pictrs/image/thumbnail256/gs7xuu.jpg
+ let split = src.split('/pictrs/image');
+ let out = `${split[0]}/pictrs/image/${
+ canUseWebP() ? 'webp/' : ''
+ }thumbnail96${split[1]}`;
+ return out;
+}
+
+export function showAvatars(): boolean {
+ return (
+ (UserService.Instance.user && UserService.Instance.user.show_avatars) ||
+ !UserService.Instance.user
+ );
+}
+
+export function isCakeDay(published: string): boolean {
+ // moment(undefined) or moment.utc(undefined) returns the current date/time
+ // moment(null) or moment.utc(null) returns null
+ const userCreationDate = moment.utc(published || null).local();
+ const currentDate = moment(new Date());
+
+ return (
+ userCreationDate.date() === currentDate.date() &&
+ userCreationDate.month() === currentDate.month() &&
+ userCreationDate.year() !== currentDate.year()
+ );
+}
+
+// Converts to image thumbnail
+export function pictrsImage(hash: string, thumbnail: boolean = false): string {
+ let root = `/pictrs/image`;
+
+ // Necessary for other servers / domains
+ if (hash.includes('pictrs')) {
+ let split = hash.split('/pictrs/image/');
+ root = `${split[0]}/pictrs/image`;
+ hash = split[1];
+ }
+
+ let out = `${root}/${canUseWebP() ? 'webp/' : ''}${
+ thumbnail ? 'thumbnail256/' : ''
+ }${hash}`;
+ return out;
+}
+
+export function isCommentType(
+ item: Comment | PrivateMessage | Post
+): item is Comment {
+ return (
+ (item as Comment).community_id !== undefined &&
+ (item as Comment).content !== undefined
+ );
+}
+
+export function isPostType(
+ item: Comment | PrivateMessage | Post
+): item is Post {
+ return (item as Post).stickied !== undefined;
+}
+
+export function toast(text: string, background: string = 'success') {
+ let backgroundColor = `var(--${background})`;
+ Toastify({
+ text: text,
+ backgroundColor: backgroundColor,
+ gravity: 'bottom',
+ position: 'left',
+ }).showToast();
+}
+
+export function pictrsDeleteToast(
+ clickToDeleteText: string,
+ deletePictureText: string,
+ deleteUrl: string
+) {
+ let backgroundColor = `var(--light)`;
+ let toast = Toastify({
+ text: clickToDeleteText,
+ backgroundColor: backgroundColor,
+ gravity: 'top',
+ position: 'right',
+ duration: 10000,
+ onClick: () => {
+ if (toast) {
+ window.location.replace(deleteUrl);
+ alert(deletePictureText);
+ toast.hideToast();
+ }
+ },
+ close: true,
+ }).showToast();
+}
+
+interface NotifyInfo {
+ name: string;
+ icon: string;
+ link: string;
+ body: string;
+}
+
+export function messageToastify(info: NotifyInfo, router: any) {
+ let htmlBody = info.body ? md.render(info.body) : '';
+ let backgroundColor = `var(--light)`;
+
+ let toast = Toastify({
+ text: `${htmlBody}<br />${info.name}`,
+ avatar: info.icon,
+ backgroundColor: backgroundColor,
+ className: 'text-dark',
+ close: true,
+ gravity: 'top',
+ position: 'right',
+ duration: 5000,
+ onClick: () => {
+ if (toast) {
+ toast.hideToast();
+ router.history.push(info.link);
+ }
+ },
+ }).showToast();
+}
+
+export function notifyPost(post: Post, router: any) {
+ let info: NotifyInfo = {
+ name: post.community_name,
+ icon: post.community_icon ? post.community_icon : defaultFavIcon,
+ link: `/post/${post.id}`,
+ body: post.name,
+ };
+ notify(info, router);
+}
+
+export function notifyComment(comment: Comment, router: any) {
+ let info: NotifyInfo = {
+ name: comment.creator_name,
+ icon: comment.creator_avatar ? comment.creator_avatar : defaultFavIcon,
+ link: `/post/${comment.post_id}/comment/${comment.id}`,
+ body: comment.content,
+ };
+ notify(info, router);
+}
+
+export function notifyPrivateMessage(pm: PrivateMessage, router: any) {
+ let info: NotifyInfo = {
+ name: pm.creator_name,
+ icon: pm.creator_avatar ? pm.creator_avatar : defaultFavIcon,
+ link: `/inbox`,
+ body: pm.content,
+ };
+ notify(info, router);
+}
+
+function notify(info: NotifyInfo, router: any) {
+ messageToastify(info, router);
+
+ if (Notification.permission !== 'granted') Notification.requestPermission();
+ else {
+ var notification = new Notification(info.name, {
+ icon: info.icon,
+ body: info.body,
+ });
+
+ notification.onclick = () => {
+ event.preventDefault();
+ router.history.push(info.link);
+ };
+ }
+}
+
+// export function setupTribute(): Tribute<{}> {
+// return new Tribute({
+// noMatchTemplate: function () {
+// return '';
+// },
+// collection: [
+// // Emojis
+// {
+// trigger: ':',
+// menuItemTemplate: (item: any) => {
+// let shortName = `:${item.original.key}:`;
+// return `${item.original.val} ${shortName}`;
+// },
+// selectTemplate: (item: any) => {
+// return `:${item.original.key}:`;
+// },
+// values: Object.entries(emojiShortName).map(e => {
+// return { key: e[1], val: e[0] };
+// }),
+// allowSpaces: false,
+// autocompleteMode: true,
+// // TODO
+// // menuItemLimit: mentionDropdownFetchLimit,
+// menuShowMinLength: 2,
+// },
+// // Users
+// {
+// trigger: '@',
+// selectTemplate: (item: any) => {
+// let link = item.original.local
+// ? `[${item.original.key}](/u/${item.original.name})`
+// : `[${item.original.key}](/user/${item.original.id})`;
+// return link;
+// },
+// values: (text: string, cb: any) => {
+// userSearch(text, (users: any) => cb(users));
+// },
+// allowSpaces: false,
+// autocompleteMode: true,
+// // TODO
+// // menuItemLimit: mentionDropdownFetchLimit,
+// menuShowMinLength: 2,
+// },
+
+// // Communities
+// {
+// trigger: '!',
+// selectTemplate: (item: any) => {
+// let link = item.original.local
+// ? `[${item.original.key}](/c/${item.original.name})`
+// : `[${item.original.key}](/community/${item.original.id})`;
+// return link;
+// },
+// values: (text: string, cb: any) => {
+// communitySearch(text, (communities: any) => cb(communities));
+// },
+// allowSpaces: false,
+// autocompleteMode: true,
+// // TODO
+// // menuItemLimit: mentionDropdownFetchLimit,
+// menuShowMinLength: 2,
+// },
+// ],
+// });
+// }
+
+// TODO
+// let tippyInstance = tippy('[data-tippy-content]');
+
+export function setupTippy() {
+ // tippyInstance.forEach(e => e.destroy());
+ // tippyInstance = tippy('[data-tippy-content]', {
+ // delay: [500, 0],
+ // // Display on "long press"
+ // touch: ['hold', 500],
+ // });
+}
+
+function userSearch(text: string, cb: any) {
+ if (text) {
+ let form: SearchForm = {
+ q: text,
+ type_: SearchType.Users,
+ sort: SortType.TopAll,
+ page: 1,
+ limit: mentionDropdownFetchLimit,
+ };
+
+ WebSocketService.Instance.search(form);
+
+ let userSub = WebSocketService.Instance.subject.subscribe(
+ msg => {
+ let res = wsJsonToRes(msg);
+ if (res.op == UserOperation.Search) {
+ let data = res.data as SearchResponse;
+ let users = data.users.map(u => {
+ return {
+ key: `@${u.name}@${hostname(u.actor_id)}`,
+ name: u.name,
+ local: u.local,
+ id: u.id,
+ };
+ });
+ cb(users);
+ userSub.unsubscribe();
+ }
+ },
+ err => console.error(err),
+ () => console.log('complete')
+ );
+ } else {
+ cb([]);
+ }
+}
+
+function communitySearch(text: string, cb: any) {
+ if (text) {
+ let form: SearchForm = {
+ q: text,
+ type_: SearchType.Communities,
+ sort: SortType.TopAll,
+ page: 1,
+ limit: mentionDropdownFetchLimit,
+ };
+
+ WebSocketService.Instance.search(form);
+
+ let communitySub = WebSocketService.Instance.subject.subscribe(
+ msg => {
+ let res = wsJsonToRes(msg);
+ if (res.op == UserOperation.Search) {
+ let data = res.data as SearchResponse;
+ let communities = data.communities.map(c => {
+ return {
+ key: `!${c.name}@${hostname(c.actor_id)}`,
+ name: c.name,
+ local: c.local,
+ id: c.id,
+ };
+ });
+ cb(communities);
+ communitySub.unsubscribe();
+ }
+ },
+ err => console.error(err),
+ () => console.log('complete')
+ );
+ } else {
+ cb([]);
+ }
+}
+
+export function getListingTypeFromProps(props: any): ListingType {
+ // TODO
+ return ListingType.All;
+ // return props.match.params.listing_type
+ // ? routeListingTypeToEnum(props.match.params.listing_type)
+ // : UserService.Instance.user
+ // ? Object.values(ListingType)[UserService.Instance.user.default_listing_type]
+ // : ListingType.All;
+}
+
+// TODO might need to add a user setting for this too
+export function getDataTypeFromProps(props: any): DataType {
+ // TODO
+ return DataType.Post;
+ // return props.match.params.data_type
+ // ? routeDataTypeToEnum(props.match.params.data_type)
+ // : DataType.Post;
+}
+
+export function getSortTypeFromProps(props: any): SortType {
+ // TODO
+ return SortType.Active;
+ // return props.match.params.sort
+ // ? routeSortTypeToEnum(props.match.params.sort)
+ // : UserService.Instance.user
+ // ? Object.values(SortType)[UserService.Instance.user.default_sort_type]
+ // : SortType.Active;
+}
+
+export function getPageFromProps(props: any): number {
+ // TODO
+ return 1;
+ // return props.match.params.page ? Number(props.match.params.page) : 1;
+}
+
+export function editCommentRes(
+ data: CommentResponse,
+ comments: Comment[]
+) {
+ let found = comments.find(c => c.id == data.comment.id);
+ if (found) {
+ found.content = data.comment.content;
+ found.updated = data.comment.updated;
+ found.removed = data.comment.removed;
+ found.deleted = data.comment.deleted;
+ found.upvotes = data.comment.upvotes;
+ found.downvotes = data.comment.downvotes;
+ found.score = data.comment.score;
+ }
+}
+
+export function saveCommentRes(
+ data: CommentResponse,
+ comments: Comment[]
+) {
+ let found = comments.find(c => c.id == data.comment.id);
+ if (found) {
+ found.saved = data.comment.saved;
+ }
+}
+
+export function createCommentLikeRes(
+ data: CommentResponse,
+ comments: Comment[]
+) {
+ let found: Comment = comments.find(c => c.id === data.comment.id);
+ if (found) {
+ found.score = data.comment.score;
+ found.upvotes = data.comment.upvotes;
+ found.downvotes = data.comment.downvotes;
+ if (data.comment.my_vote !== null) {
+ found.my_vote = data.comment.my_vote;
+ }
+ }
+}
+
+export function createPostLikeFindRes(data: PostResponse, posts: Post[]) {
+ let found = posts.find(c => c.id == data.post.id);
+ if (found) {
+ createPostLikeRes(data, found);
+ }
+}
+
+export function createPostLikeRes(data: PostResponse, post: Post) {
+ if (post) {
+ post.score = data.post.score;
+ post.upvotes = data.post.upvotes;
+ post.downvotes = data.post.downvotes;
+ if (data.post.my_vote !== null) {
+ post.my_vote = data.post.my_vote;
+ }
+ }
+}
+
+export function editPostFindRes(data: PostResponse, posts: Post[]) {
+ let found = posts.find(c => c.id == data.post.id);
+ if (found) {
+ editPostRes(data, found);
+ }
+}
+
+export function editPostRes(data: PostResponse, post: Post) {
+ if (post) {
+ post.url = data.post.url;
+ post.name = data.post.name;
+ post.nsfw = data.post.nsfw;
+ post.deleted = data.post.deleted;
+ post.removed = data.post.removed;
+ post.stickied = data.post.stickied;
+ post.body = data.post.body;
+ post.locked = data.post.locked;
+ }
+}
+
+export function commentsToFlatNodes(
+ comments: Comment[]
+): CommentNodeI[] {
+ let nodes: CommentNodeI[] = [];
+ for (let comment of comments) {
+ nodes.push({ comment: comment });
+ }
+ return nodes;
+}
+
+export function commentSort(tree: CommentNodeI[], sort: CommentSortType) {
+ // First, put removed and deleted comments at the bottom, then do your other sorts
+ if (sort == CommentSortType.Top) {
+ tree.sort(
+ (a, b) =>
+ +a.comment.removed - +b.comment.removed ||
+ +a.comment.deleted - +b.comment.deleted ||
+ b.comment.score - a.comment.score
+ );
+ } else if (sort == CommentSortType.New) {
+ tree.sort(
+ (a, b) =>
+ +a.comment.removed - +b.comment.removed ||
+ +a.comment.deleted - +b.comment.deleted ||
+ b.comment.published.localeCompare(a.comment.published)
+ );
+ } else if (sort == CommentSortType.Old) {
+ tree.sort(
+ (a, b) =>
+ +a.comment.removed - +b.comment.removed ||
+ +a.comment.deleted - +b.comment.deleted ||
+ a.comment.published.localeCompare(b.comment.published)
+ );
+ } else if (sort == CommentSortType.Hot) {
+ tree.sort(
+ (a, b) =>
+ +a.comment.removed - +b.comment.removed ||
+ +a.comment.deleted - +b.comment.deleted ||
+ hotRankComment(b.comment) - hotRankComment(a.comment)
+ );
+ }
+
+ // Go through the children recursively
+ for (let node of tree) {
+ if (node.children) {
+ commentSort(node.children, sort);
+ }
+ }
+}
+
+export function commentSortSortType(tree: CommentNodeI[], sort: SortType) {
+ commentSort(tree, convertCommentSortType(sort));
+}
+
+function convertCommentSortType(sort: SortType): CommentSortType {
+ if (
+ sort == SortType.TopAll ||
+ sort == SortType.TopDay ||
+ sort == SortType.TopWeek ||
+ sort == SortType.TopMonth ||
+ sort == SortType.TopYear
+ ) {
+ return CommentSortType.Top;
+ } else if (sort == SortType.New) {
+ return CommentSortType.New;
+ } else if (sort == SortType.Hot || sort == SortType.Active) {
+ return CommentSortType.Hot;
+ } else {
+ return CommentSortType.Hot;
+ }
+}
+
+export function postSort(
+ posts: Post[],
+ sort: SortType,
+ communityType: boolean
+) {
+ // First, put removed and deleted comments at the bottom, then do your other sorts
+ if (
+ sort == SortType.TopAll ||
+ sort == SortType.TopDay ||
+ sort == SortType.TopWeek ||
+ sort == SortType.TopMonth ||
+ sort == SortType.TopYear
+ ) {
+ posts.sort(
+ (a, b) =>
+ +a.removed - +b.removed ||
+ +a.deleted - +b.deleted ||
+ (communityType && +b.stickied - +a.stickied) ||
+ b.score - a.score
+ );
+ } else if (sort == SortType.New) {
+ posts.sort(
+ (a, b) =>
+ +a.removed - +b.removed ||
+ +a.deleted - +b.deleted ||
+ (communityType && +b.stickied - +a.stickied) ||
+ b.published.localeCompare(a.published)
+ );
+ } else if (sort == SortType.Hot) {
+ posts.sort(
+ (a, b) =>
+ +a.removed - +b.removed ||
+ +a.deleted - +b.deleted ||
+ (communityType && +b.stickied - +a.stickied) ||
+ b.hot_rank - a.hot_rank
+ );
+ } else if (sort == SortType.Active) {
+ posts.sort(
+ (a, b) =>
+ +a.removed - +b.removed ||
+ +a.deleted - +b.deleted ||
+ (communityType && +b.stickied - +a.stickied) ||
+ b.hot_rank_active - a.hot_rank_active
+ );
+ }
+}
+
+export const colorList: string[] = [
+ hsl(0),
+ hsl(100),
+ hsl(150),
+ hsl(200),
+ hsl(250),
+ hsl(300),
+];
+
+function hsl(num: number) {
+ return `hsla(${num}, 35%, 50%, 1)`;
+}
+
+function randomHsl() {
+ return `hsla(${Math.random() * 360}, 100%, 50%, 1)`;
+}
+
+export function previewLines(
+ text: string,
+ maxChars: number = 300,
+ maxLines: number = 1
+): string {
+ return (
+ text
+ .slice(0, maxChars)
+ .split('\n')
+ // Use lines * 2 because markdown requires 2 lines
+ .slice(0, maxLines * 2)
+ .join('\n') + '...'
+ );
+}
+
+export function hostname(url: string): string {
+ let cUrl = new URL(url);
+ // TODO
+ return `${cUrl.hostname}:${cUrl.port}`;
+ // return window.location.port
+ // ? `${cUrl.hostname}:${cUrl.port}`
+ // : `${cUrl.hostname}`;
+}
+
+function canUseWebP() {
+ // TODO pictshare might have a webp conversion bug, try disabling this
+ return false;
+
+ // var elem = document.createElement('canvas');
+ // if (!!(elem.getContext && elem.getContext('2d'))) {
+ // var testString = !(window.mozInnerScreenX == null) ? 'png' : 'webp';
+ // // was able or not to get WebP representation
+ // return (
+ // elem.toDataURL('image/webp').startsWith('data:image/' + testString)
+ // );
+ // }
+
+ // // very old browser like IE 8, canvas not supported
+ // return false;
+}
+
+export function validTitle(title?: string): boolean {
+ // Initial title is null, minimum length is taken care of by textarea's minLength={3}
+ if (title === null || title.length < 3) return true;
+
+ const regex = new RegExp(/.*\S.*/, 'g');
+
+ return regex.test(title);
+}
+
+export function siteBannerCss(banner: string): string {
+ return ` \
+ background-image: linear-gradient( rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.8) ) ,url("${banner}"); \
+ background-attachment: fixed; \
+ background-position: top; \
+ background-repeat: no-repeat; \
+ background-size: 100% cover; \
+
+ width: 100%; \
+ max-height: 100vh; \
+ `;
+}
+
+export function isBrowser() {
+ return typeof window !== 'undefined';
+}
{\r
- "compilerOptions": {\r
- "module": "commonjs",\r
- "target": "esnext",\r
- "sourceMap": true,\r
- "jsx": "preserve",\r
- "importHelpers": true,\r
- "emitDecoratorMetadata": true,\r
- "experimentalDecorators": true\r
- },\r
- "exclude": ["node_modules", "fuse.ts"]\r
+ "compilerOptions": {\r
+ "module": "commonjs",\r
+ "target": "esnext",\r
+ "sourceMap": true,\r
+ "inlineSources": true,\r
+ "jsx": "preserve",\r
+ "importHelpers": true,\r
+ "emitDecoratorMetadata": true,\r
+ "experimentalDecorators": true,\r
+ "esModuleInterop": true\r
+ },\r
+ "exclude": ["node_modules", "fuse.ts"]\r
}\r
core-js-pure "^3.0.0"
regenerator-runtime "^0.13.4"
-"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2":
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2":
version "7.11.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
exec-sh "^0.3.2"
minimist "^1.2.0"
+"@iarna/cli@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@iarna/cli/-/cli-1.2.0.tgz#0f7af5e851afe895104583c4ca07377a8094d641"
+ integrity sha512-ukITQAqVs2n9HGmn3car/Ir7d3ta650iXhrG7pjr3EWdFmJuuOVWgYsu7ftsSe5VifEFFhjxVuX9+8F7L8hwcA==
+ dependencies:
+ signal-exit "^3.0.2"
+ update-notifier "^2.2.0"
+ yargs "^8.0.2"
+
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
+"@popperjs/core@^2.4.4":
+ version "2.4.4"
+ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.4.tgz#11d5db19bd178936ec89cd84519c4de439574398"
+ integrity sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg==
+
"@sinonjs/commons@^1.7.0":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217"
dependencies:
"@sinonjs/commons" "^1.7.0"
+"@types/autosize@^3.0.6":
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/@types/autosize/-/autosize-3.0.7.tgz#f5da28d7ea4532c8b60573d67ec04fc866fa13db"
+ integrity sha512-D46m3aBNg81QKk9ZigmDFuhXUkD4IpBSrkGUKpYo2QBETbUjqEe8msXNCcECaXLXv1O4ppdMpizgFRzpfrgOxA==
+ dependencies:
+ "@types/jquery" "*"
+
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
version "7.1.9"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d"
jest-diff "^25.2.1"
pretty-format "^25.2.1"
+"@types/jquery@*":
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.1.tgz#cebb057acf5071c40e439f30e840c57a30d406c3"
+ integrity sha512-Tyctjh56U7eX2b9udu3wG853ASYP0uagChJcQJXLUXEU6C/JiW5qt5dl8ao01VRj1i5pgXPAf8f1mq4+FDLRQg==
+ dependencies:
+ "@types/sizzle" "*"
+
"@types/json-schema@^7.0.3":
version "7.0.5"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==
-"@types/node@*", "@types/node@^14.6.0":
+"@types/node-fetch@^2.5.7":
+ version "2.5.7"
+ resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c"
+ integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==
+ dependencies:
+ "@types/node" "*"
+ form-data "^3.0.0"
+
+"@types/node@*":
version "14.6.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499"
integrity sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA==
+"@types/node@^14.6.0":
+ version "14.6.2"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.2.tgz#264b44c5a28dfa80198fc2f7b6d3c8a054b9491f"
+ integrity sha512-onlIwbaeqvZyniGPfdw/TEhKIh79pz66L1q06WUQqJLnAb6wbjvOtepLYTGHTqzdXgBYIE3ZdmqHDGsRsbBz7A==
+
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
"@types/express-serve-static-core" "*"
"@types/mime" "*"
+"@types/sizzle@*":
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
+ integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==
+
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
dependencies:
"@types/yargs-parser" "*"
-"@typescript-eslint/eslint-plugin@3.9.0":
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.9.0.tgz#0fe529b33d63c9a94f7503ca2bb12c84b9477ff3"
- integrity sha512-UD6b4p0/hSe1xdTvRCENSx7iQ+KR6ourlZFfYuPC7FlXEzdHuLPrEmuxZ23b2zW96KJX9Z3w05GE/wNOiEzrVg==
+"@typescript-eslint/eslint-plugin@3.10.1":
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz#7e061338a1383f59edc204c605899f93dc2e2c8f"
+ integrity sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==
dependencies:
- "@typescript-eslint/experimental-utils" "3.9.0"
+ "@typescript-eslint/experimental-utils" "3.10.1"
debug "^4.1.1"
functional-red-black-tree "^1.0.1"
regexpp "^3.0.0"
semver "^7.3.2"
tsutils "^3.17.1"
-"@typescript-eslint/experimental-utils@3.9.0":
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.9.0.tgz#3171d8ddba0bf02a8c2034188593630914fcf5ee"
- integrity sha512-/vSHUDYizSOhrOJdjYxPNGfb4a3ibO8zd4nUKo/QBFOmxosT3cVUV7KIg8Dwi6TXlr667G7YPqFK9+VSZOorNA==
+"@typescript-eslint/experimental-utils@3.10.1":
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686"
+ integrity sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==
dependencies:
"@types/json-schema" "^7.0.3"
- "@typescript-eslint/types" "3.9.0"
- "@typescript-eslint/typescript-estree" "3.9.0"
+ "@typescript-eslint/types" "3.10.1"
+ "@typescript-eslint/typescript-estree" "3.10.1"
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
-"@typescript-eslint/parser@3.9.0":
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.9.0.tgz#344978a265d9a5c7c8f13e62c78172a4374dabea"
- integrity sha512-rDHOKb6uW2jZkHQniUQVZkixQrfsZGUCNWWbKWep4A5hGhN5dLHMUCNAWnC4tXRlHedXkTDptIpxs6e4Pz8UfA==
+"@typescript-eslint/parser@3.10.1":
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.10.1.tgz#1883858e83e8b442627e1ac6f408925211155467"
+ integrity sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==
dependencies:
"@types/eslint-visitor-keys" "^1.0.0"
- "@typescript-eslint/experimental-utils" "3.9.0"
- "@typescript-eslint/types" "3.9.0"
- "@typescript-eslint/typescript-estree" "3.9.0"
+ "@typescript-eslint/experimental-utils" "3.10.1"
+ "@typescript-eslint/types" "3.10.1"
+ "@typescript-eslint/typescript-estree" "3.10.1"
eslint-visitor-keys "^1.1.0"
-"@typescript-eslint/types@3.9.0":
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.9.0.tgz#be9d0aa451e1bf3ce99f2e6920659e5b2e6bfe18"
- integrity sha512-rb6LDr+dk9RVVXO/NJE8dT1pGlso3voNdEIN8ugm4CWM5w5GimbThCMiMl4da1t5u3YwPWEwOnKAULCZgBtBHg==
+"@typescript-eslint/types@3.10.1":
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727"
+ integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==
"@typescript-eslint/typescript-estree@2.34.0":
version "2.34.0"
semver "^7.3.2"
tsutils "^3.17.1"
-"@typescript-eslint/typescript-estree@3.9.0":
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.9.0.tgz#c6abbb50fa0d715cab46fef67ca6378bf2eaca13"
- integrity sha512-N+158NKgN4rOmWVfvKOMoMFV5n8XxAliaKkArm/sOypzQ0bUL8MSnOEBW3VFIeffb/K5ce/cAV0yYhR7U4ALAA==
+"@typescript-eslint/typescript-estree@3.10.1":
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853"
+ integrity sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==
dependencies:
- "@typescript-eslint/types" "3.9.0"
- "@typescript-eslint/visitor-keys" "3.9.0"
+ "@typescript-eslint/types" "3.10.1"
+ "@typescript-eslint/visitor-keys" "3.10.1"
debug "^4.1.1"
glob "^7.1.6"
is-glob "^4.0.1"
semver "^7.3.2"
tsutils "^3.17.1"
-"@typescript-eslint/visitor-keys@3.9.0":
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.9.0.tgz#44de8e1b1df67adaf3b94d6b60b80f8faebc8dd3"
- integrity sha512-O1qeoGqDbu0EZUC/MZ6F1WHTIzcBVhGqDj3LhTnj65WUA548RXVxUHbYhAW9bZWfb2rnX9QsbbP5nmeJ5Z4+ng==
+"@typescript-eslint/visitor-keys@3.10.1":
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931"
+ integrity sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==
dependencies:
eslint-visitor-keys "^1.1.0"
+JSONStream@^1.3.2:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
+ integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==
+ dependencies:
+ jsonparse "^1.2.0"
+ through ">=2.2.7 <3"
+
abab@^2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.4.tgz#6dfa57b417ca06d21b2478f0e638302f99c2405c"
integrity sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==
+abbrev@1, abbrev@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+ integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c"
integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==
+agent-base@4, agent-base@^4.1.0, agent-base@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
+ integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
+ dependencies:
+ es6-promisify "^5.0.0"
+
+agent-base@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
+ integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
+ dependencies:
+ es6-promisify "^5.0.0"
+
+agentkeepalive@^3.3.0, agentkeepalive@^3.4.1:
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67"
+ integrity sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==
+ dependencies:
+ humanize-ms "^1.2.1"
+
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
+ansi-align@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f"
+ integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=
+ dependencies:
+ string-width "^2.0.0"
+
ansi-colors@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
dependencies:
type-fest "^0.11.0"
-ansi-regex@^2.1.1:
+ansi-regex@^2.0.0, ansi-regex@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
-ansi-regex@^3.0.0:
+ansi-regex@^3.0.0, ansi-regex@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21"
integrity sha1-DELU+xcWDVqa8eSEus4cZpIsGyE=
+ansicolors@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979"
+ integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=
+
+ansistyles@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539"
+ integrity sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk=
+
anymatch@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.2.1.tgz#d0df4a682ee408273583d43f6f79e9892624bc9a"
integrity sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==
+aproba@^1.0.3, aproba@^1.1.1, aproba@^1.1.2, aproba@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+ integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+
+"aproba@^1.1.2 || 2":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
+ integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
+
+archy@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40"
+ integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=
+
+are-we-there-yet@~1.1.2:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
+ integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
es-abstract "^1.17.0-next.1"
function-bind "^1.1.1"
+asap@^2.0.0:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+ integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
+
asn1@~0.2.3:
version "0.2.4"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+autosize@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.2.tgz#073cfd07c8bf45da4b9fd153437f5bafbba1e4c9"
+ integrity sha512-jnSyH2d+qdfPGpWlcuhGiHmqBJ6g3X+8T+iRwFrHPLVcdoGJE/x6Qicm6aDHfTsbgZKxyV8UU/YB2p4cjKDRRA==
+
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
dependencies:
tweetnacl "^0.14.3"
+bin-links@^1.1.0, bin-links@^1.1.2:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-1.1.8.tgz#bd39aadab5dc4bdac222a07df5baf1af745b2228"
+ integrity sha512-KgmVfx+QqggqP9dA3iIc5pA4T1qEEEL+hOhOhNPaUm77OTrJoOXE/C05SJLNJe6m/2wUK7F1tDSou7n5TfCDzQ==
+ dependencies:
+ bluebird "^3.5.3"
+ cmd-shim "^3.0.0"
+ gentle-fs "^2.3.0"
+ graceful-fs "^4.1.15"
+ npm-normalize-package-bin "^1.0.0"
+ write-file-atomic "^2.3.0"
+
binary-extensions@^1.0.0:
version "1.13.1"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
dependencies:
file-uri-to-path "1.0.0"
+block-stream@*:
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+ integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=
+ dependencies:
+ inherits "~2.0.0"
+
+bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5:
+ version "3.7.2"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+ integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
+bluebird@~3.5.1:
+ version "3.5.5"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
+ integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
+
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.10.0.tgz#be3736f161c4bb8b10958027ab99465d2a811198"
integrity sha512-OCsqTQboTEWWsUjcp5jLSw2ZHsBiv2C105iFs61bOT0Hnwi9p7/uuXdd7mu8RYcarREfdjNN+8LitmEHATsLYg==
+boxen@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
+ integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==
+ dependencies:
+ ansi-align "^2.0.0"
+ camelcase "^4.0.0"
+ chalk "^2.0.1"
+ cli-boxes "^1.0.0"
+ string-width "^2.0.0"
+ term-size "^1.2.0"
+ widest-line "^2.0.0"
+
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+builtin-modules@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+ integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
+
+builtins@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88"
+ integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og=
+
+byline@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
+ integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=
+
+byte-size@^4.0.2:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-4.0.4.tgz#29d381709f41aae0d89c631f1c81aec88cd40b23"
+ integrity sha512-82RPeneC6nqCdSwCX2hZUz3JPOvN5at/nTEw/CMf05Smu3Hrpo9Psb7LjN+k+XndNArG1EY8L4+BM3aTM4BCvw==
+
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+cacache@^10.0.0, cacache@^10.0.4:
+ version "10.0.4"
+ resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460"
+ integrity sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==
+ dependencies:
+ bluebird "^3.5.1"
+ chownr "^1.0.1"
+ glob "^7.1.2"
+ graceful-fs "^4.1.11"
+ lru-cache "^4.1.1"
+ mississippi "^2.0.0"
+ mkdirp "^0.5.1"
+ move-concurrently "^1.0.1"
+ promise-inflight "^1.0.1"
+ rimraf "^2.6.2"
+ ssri "^5.2.4"
+ unique-filename "^1.1.0"
+ y18n "^4.0.0"
+
+cacache@^11.0.2, cacache@^11.3.3:
+ version "11.3.3"
+ resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.3.tgz#8bd29df8c6a718a6ebd2d010da4d7972ae3bbadc"
+ integrity sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==
+ dependencies:
+ bluebird "^3.5.5"
+ chownr "^1.1.1"
+ figgy-pudding "^3.5.1"
+ glob "^7.1.4"
+ graceful-fs "^4.1.15"
+ lru-cache "^5.1.1"
+ mississippi "^3.0.0"
+ mkdirp "^0.5.1"
+ move-concurrently "^1.0.1"
+ promise-inflight "^1.0.1"
+ rimraf "^2.6.3"
+ ssri "^6.0.1"
+ unique-filename "^1.1.1"
+ y18n "^4.0.0"
+
cache-base@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
union-value "^1.0.0"
unset-value "^1.0.0"
+call-limit@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/call-limit/-/call-limit-1.1.1.tgz#ef15f2670db3f1992557e2d965abc459e6e358d4"
+ integrity sha512-5twvci5b9eRBw2wCfPtN0GmlR2/gadZqyFpPhOK6CvMFoFgA+USnZ6Jpu1lhG9h85pQ3Ouil3PfXWRD4EUaRiQ==
+
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+camelcase@^4.0.0, camelcase@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+ integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
+
camelcase@^5.0.0, camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
dependencies:
rsvp "^4.8.4"
+capture-stack-trace@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d"
+ integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==
+
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
resolved "https://registry.yarnpkg.com/chain-able/-/chain-able-3.0.0.tgz#dcffe8b04f3da210941a23843bc1332bb288ca9f"
integrity sha512-26MoELhta86n7gCsE2T1hGRyncZvPjFXTkB/DEp4+i/EJVSxXQNwXMDZZb2+SWcbPuow18wQtztaW7GXOel9DA==
-chalk@^2.0.0, chalk@^2.4.1:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
lodash "^4.15.0"
parse5 "^3.0.1"
+choices.js@^9.0.1:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/choices.js/-/choices.js-9.0.1.tgz#745fb29af8670428fdc0bf1cc9dfaa404e9d0510"
+ integrity sha512-JgpeDY0Tmg7tqY6jaW/druSklJSt7W68tXFJIw0GSGWmO37SDAL8o60eICNGbzIODjj02VNNtf5h6TgoHDtCsA==
+ dependencies:
+ deepmerge "^4.2.0"
+ fuse.js "^3.4.5"
+ redux "^4.0.4"
+
chokidar@^1.6.1:
version "1.7.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
optionalDependencies:
fsevents "^1.0.0"
+chownr@^1.0.1, chownr@^1.1.1, chownr@^1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
+ integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+
+chownr@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
+ integrity sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=
+
+ci-info@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
+ integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==
+
ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
+cidr-regex@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1"
+ integrity sha1-dKv9YZ3zcLnVSrFEdVaOl91kwME=
+
class-utils@^0.3.5:
version "0.3.6"
resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
isobject "^3.0.0"
static-extend "^0.1.1"
+classcat@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/classcat/-/classcat-4.1.0.tgz#e8fd8623e5625187b58adf49bb669a13b6c520f4"
+ integrity sha512-RA8O5oCi1I1CF6rR4cRBROh8MtZzM4w7xKLm0jd+S6UN2G4FIto+9DVOeFc46JEZFN5PVe/EZWLQO1VU/AUH4A==
+
clean-css@^4.1.9:
version "4.2.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
+cli-boxes@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
+ integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM=
+
+cli-columns@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/cli-columns/-/cli-columns-3.1.2.tgz#6732d972979efc2ae444a1f08e08fa139c96a18e"
+ integrity sha1-ZzLZcpee/CrkRKHwjgj6E5yWoY4=
+ dependencies:
+ string-width "^2.0.0"
+ strip-ansi "^3.0.1"
+
cli-cursor@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
dependencies:
restore-cursor "^3.1.0"
-cli-truncate@2.1.0, cli-truncate@^2.1.0:
+cli-table2@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97"
+ integrity sha1-LR738hig54biFFQFYtS9F3/jLZc=
+ dependencies:
+ lodash "^3.10.1"
+ string-width "^1.0.1"
+ optionalDependencies:
+ colors "^1.1.2"
+
+cli-table3@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202"
+ integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==
+ dependencies:
+ object-assign "^4.1.0"
+ string-width "^2.1.1"
+ optionalDependencies:
+ colors "^1.1.2"
+
+cli-truncate@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
+cliui@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+ integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wrap-ansi "^2.0.0"
+
+cliui@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
+ integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
+ dependencies:
+ string-width "^3.1.0"
+ strip-ansi "^5.2.0"
+ wrap-ansi "^5.1.0"
+
cliui@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
strip-ansi "^6.0.0"
wrap-ansi "^6.2.0"
+clone@^1.0.2:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
+ integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
+
+cmd-shim@^3.0.0, cmd-shim@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-3.0.3.tgz#2c35238d3df37d98ecdd7d5f6b8dc6b21cadc7cb"
+ integrity sha512-DtGg+0xiFhQIntSBRzL2fRQBnmtAVwXIDo4Qq46HPpObYquxMaZS4sb82U9nH91qJrlosC1wa9gwr0QyL/HypA==
+ dependencies:
+ graceful-fs "^4.1.2"
+ mkdirp "~0.5.0"
+
+cmd-shim@~2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb"
+ integrity sha1-b8vamUg6j9FdfTChlspp1oii79s=
+ dependencies:
+ graceful-fs "^4.1.2"
+ mkdirp "~0.5.0"
+
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+ integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+
collect-v8-coverage@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
-combined-stream@^1.0.6, combined-stream@~1.0.6:
+columnify@~1.5.4:
+ version "1.5.4"
+ resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb"
+ integrity sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=
+ dependencies:
+ strip-ansi "^3.0.0"
+ wcwidth "^1.0.0"
+
+combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
-commander@^2.19.0:
+commander@^2.19.0, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
-commander@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
- integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
+commander@^6.0.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc"
+ integrity sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA==
compare-versions@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+concat-stream@^1.5.0, concat-stream@^1.5.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
+ integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
+ dependencies:
+ buffer-from "^1.0.0"
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+config-chain@~1.1.11:
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
+ integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==
+ dependencies:
+ ini "^1.3.4"
+ proto-list "~1.2.1"
+
+configstore@^3.0.0:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.5.tgz#e9af331fadc14dabd544d3e7e76dc446a09a530f"
+ integrity sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==
+ dependencies:
+ dot-prop "^4.2.1"
+ graceful-fs "^4.1.2"
+ make-dir "^1.0.0"
+ unique-string "^1.0.0"
+ write-file-atomic "^2.0.0"
+ xdg-basedir "^3.0.0"
+
+console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+ integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
+
contains-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
+cookie@^0.1.2:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.1.5.tgz#6ab9948a4b1ae21952cd2588530a4722d4044d7c"
+ integrity sha1-armUiksa4hlSzSWIUwpHItQETXw=
+
+copy-concurrently@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
+ integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==
+ dependencies:
+ aproba "^1.1.1"
+ fs-write-stream-atomic "^1.0.8"
+ iferr "^0.1.5"
+ mkdirp "^0.5.1"
+ rimraf "^2.5.4"
+ run-queue "^1.0.0"
+
copy-descriptor@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
path-type "^4.0.0"
yaml "^1.7.2"
+cosmiconfig@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3"
+ integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==
+ dependencies:
+ "@types/parse-json" "^4.0.0"
+ import-fresh "^3.2.1"
+ parse-json "^5.0.0"
+ path-type "^4.0.0"
+ yaml "^1.10.0"
+
+create-error-class@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
+ integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=
+ dependencies:
+ capture-stack-trace "^1.0.0"
+
+cross-spawn@^5.0.1:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
cross-spawn@^6.0.0:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
shebang-command "^2.0.0"
which "^2.0.1"
+crypto-random-string@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
+ integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
+
css-select@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.2.tgz#ee5ff8f208c8cd613b389f7b222c9801ca62b3f7"
integrity sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw==
+cyclist@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
+ integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
+
damerau-levenshtein@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791"
dependencies:
ms "2.0.0"
+debug@3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+ integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+ dependencies:
+ ms "2.0.0"
+
+debug@^3.1.0:
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+ integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+ dependencies:
+ ms "^2.1.1"
+
debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
dependencies:
ms "^2.1.1"
-decamelize@^1.2.0:
+debuglog@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
+ integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
+
+decamelize@^1.1.1, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
+deep-equal@^1.0.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
+ integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
+ dependencies:
+ is-arguments "^1.0.4"
+ is-date-object "^1.0.1"
+ is-regex "^1.0.4"
+ object-is "^1.0.1"
+ object-keys "^1.1.1"
+ regexp.prototype.flags "^1.2.0"
+
+deep-extend@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+ integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
deep-is@^0.1.3, deep-is@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
-deepmerge@^4.2.2:
+deepmerge@^4.2.0, deepmerge@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
+defaults@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
+ integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=
+ dependencies:
+ clone "^1.0.2"
+
define-properties@^1.1.2, define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+ integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
+
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+detect-indent@~5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
+ integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50=
+
+detect-newline@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
+ integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=
+
detect-newline@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
+dezalgo@^1.0.0, dezalgo@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456"
+ integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=
+ dependencies:
+ asap "^2.0.0"
+ wrappy "1"
+
diff-sequences@^25.2.6:
version "25.2.6"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd"
dom-serializer "0"
domelementtype "1"
+dot-prop@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.1.tgz#45884194a71fc2cda71cbb4bceb3a4dd2f433ba4"
+ integrity sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==
+ dependencies:
+ is-obj "^1.0.0"
+
+dotenv@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef"
+ integrity sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==
+
+duplexer3@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
+ integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
+
+duplexify@^3.4.2, duplexify@^3.6.0:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"
+ integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==
+ dependencies:
+ end-of-stream "^1.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+ stream-shift "^1.0.0"
+
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
jsbn "~0.1.0"
safer-buffer "^2.1.0"
+editor@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/editor/-/editor-1.0.0.tgz#60c7f87bd62bcc6a894fa8ccd6afb7823a24f742"
+ integrity sha1-YMf4e9YrzGqJT6jM1q+3gjok90I=
+
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.0.0.tgz#48a2309cc8a1d2e9d23bc6a67c39b63032e76ea4"
integrity sha512-6p1NII1Vm62wni/VR/cUMauVQoxmLVb9csqQlvLz+hO2gk8U2UYDfXHQSUYIBKmZwAKz867IDqG7B+u0mj+M6w==
+emoji-short-name@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/emoji-short-name/-/emoji-short-name-1.0.0.tgz#82e6f543b6c68984d69bdc80eac735104fdd4af8"
+ integrity sha512-+tiniHvgRR7XMI1jAaGveumWg5LALE/nWkFD6CcOn6M5IDM9w4PkMs8UwzLTMoZtDLdTdQmzxGvLOxHVIjPzjg==
+
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
-end-of-stream@^1.1.0:
+encoding@^0.1.11:
+ version "0.1.13"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
+ integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
+ dependencies:
+ iconv-lite "^0.6.2"
+
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
dependencies:
once "^1.4.0"
-enquirer@^2.3.5:
+enquirer@^2.3.5, enquirer@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
-entities@^2.0.0:
+entities@^2.0.0, entities@~2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f"
integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==
rst-selector-parser "^2.2.3"
string.prototype.trim "^1.2.1"
+err-code@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960"
+ integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=
+
+errno@~0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
+ integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
+ dependencies:
+ prr "~1.0.1"
+
error-ex@^1.2.0, error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
+es-abstract@^1.18.0-next.0:
+ version "1.18.0-next.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.0.tgz#b302834927e624d8e5837ed48224291f2c66e6fc"
+ integrity sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==
+ dependencies:
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.1"
+ is-callable "^1.2.0"
+ is-negative-zero "^2.0.0"
+ is-regex "^1.1.1"
+ object-inspect "^1.8.0"
+ object-keys "^1.1.1"
+ object.assign "^4.1.0"
+ string.prototype.trimend "^1.0.1"
+ string.prototype.trimstart "^1.0.1"
+
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
is-date-object "^1.0.1"
is-symbol "^1.0.2"
+es6-promise@^4.0.3:
+ version "4.2.8"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
+ integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
+
+es6-promisify@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
+ integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
+ dependencies:
+ es6-promise "^4.0.3"
+
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
tsconfig-paths "^3.9.0"
eslint-plugin-jane@^8.0.4:
- version "8.1.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-jane/-/eslint-plugin-jane-8.1.1.tgz#0e673a4219690f01787553af2365c1fcd5e5363a"
- integrity sha512-5nrNbAgqVNq2P+tkxE8bd6Lr7YrU4hRqH+h6kmrEfnH4SRptVwswY103ppe5ThSUntZSygzn5NDpji5FLJenDg==
+ version "8.1.2"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-jane/-/eslint-plugin-jane-8.1.2.tgz#7281da8377922ea0972766f5ec9a5a0add1d0429"
+ integrity sha512-NUZKavJB7jg+J/i4+/zyT10VY4htC6iLbt2Yyig8JevsgchhW/8RIidnHfyp0OipRABTI0mZqW1p7Fu827f48g==
dependencies:
- "@typescript-eslint/eslint-plugin" "3.9.0"
- "@typescript-eslint/parser" "3.9.0"
+ "@typescript-eslint/eslint-plugin" "3.10.1"
+ "@typescript-eslint/parser" "3.10.1"
babel-eslint "10.1.0"
eslint-config-prettier "6.11.0"
eslint-plugin-babel "5.3.1"
eslint-plugin-node "11.1.0"
eslint-plugin-prettier "3.1.4"
eslint-plugin-promise "4.2.1"
- eslint-plugin-react "7.20.5"
- eslint-plugin-react-hooks "4.0.8"
+ eslint-plugin-react "7.20.6"
+ eslint-plugin-react-hooks "4.1.0"
eslint-plugin-unicorn "21.0.0"
eslint-plugin-jest@23.20.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==
-eslint-plugin-react-hooks@4.0.8:
- version "4.0.8"
- resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.0.8.tgz#a9b1e3d57475ccd18276882eff3d6cba00da7a56"
- integrity sha512-6SSb5AiMCPd8FDJrzah+Z4F44P2CdOaK026cXFV+o/xSRzfOiV1FNFeLl2z6xm3yqWOQEZ5OfVgiec90qV2xrQ==
+eslint-plugin-react-hooks@4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.1.0.tgz#6323fbd5e650e84b2987ba76370523a60f4e7925"
+ integrity sha512-36zilUcDwDReiORXmcmTc6rRumu9JIM3WjSvV0nclHoUQ0CNrX866EwONvLR/UqaeqFutbAnVu8PEmctdo2SRQ==
-eslint-plugin-react@7.20.5:
- version "7.20.5"
- resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.5.tgz#29480f3071f64a04b2c3d99d9b460ce0f76fb857"
- integrity sha512-ajbJfHuFnpVNJjhyrfq+pH1C0gLc2y94OiCbAXT5O0J0YCKaFEHDV8+3+mDOr+w8WguRX+vSs1bM2BDG0VLvCw==
+eslint-plugin-react@7.20.6:
+ version "7.20.6"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.6.tgz#4d7845311a93c463493ccfa0a19c9c5d0fd69f60"
+ integrity sha512-kidMTE5HAEBSLu23CUDvj8dc3LdBU0ri1scwHBZjI41oDv4tjsWZKU7MQccFzH1QYPYhsnTF2ovh7JlcIcmxgg==
dependencies:
array-includes "^3.1.1"
array.prototype.flatmap "^1.2.3"
resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5"
integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==
+execa@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
+ integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=
+ dependencies:
+ cross-spawn "^5.0.1"
+ get-stream "^3.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
execa@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
signal-exit "^3.0.0"
strip-eof "^1.0.0"
-execa@^4.0.0, execa@^4.0.1:
+execa@^4.0.0, execa@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.3.tgz#0a34dabbad6d66100bd6f2c576c8669403f317f2"
integrity sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==
signal-exit "^3.0.2"
strip-final-newline "^2.0.0"
+exenv@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
+ integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
+
exit@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
dependencies:
bser "2.1.1"
+figgy-pudding@^3.0.0, figgy-pudding@^3.5.1:
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
+ integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
+
figures@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
statuses "~1.5.0"
unpipe "~1.0.0"
+find-npm-prefix@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/find-npm-prefix/-/find-npm-prefix-1.0.2.tgz#8d8ce2c78b3b4b9e66c8acc6a37c231eb841cfdf"
+ integrity sha512-KEftzJ+H90x6pcKtdXZEPsQse8/y/UnvzRKrOSQFprnrGaFuJ62fVkP34Iu2IYuMvyauCyoLTNkJZgrrGA2wkA==
+
find-up@^2.0.0, find-up@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
dependencies:
locate-path "^2.0.0"
+find-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+ integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+ dependencies:
+ locate-path "^3.0.0"
+
find-up@^4.0.0, find-up@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
dependencies:
chain-able "^1.0.1"
+flush-write-stream@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
+ integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.3.6"
+
for-in@^1.0.1, for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+form-data@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
+ integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ mime-types "^2.1.12"
+
form-data@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+from2@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/from2/-/from2-1.3.0.tgz#88413baaa5f9a597cfde9221d86986cd3c061dfd"
+ integrity sha1-iEE7qqX5pZfP3pIh2GmGzTwGHf0=
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "~1.1.10"
+
+from2@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
+ integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+
fs-extra@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
jsonfile "^4.0.0"
universalify "^0.1.0"
+fs-minipass@^1.2.5:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
+ integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==
+ dependencies:
+ minipass "^2.6.0"
+
+fs-vacuum@^1.2.10, fs-vacuum@~1.2.10:
+ version "1.2.10"
+ resolved "https://registry.yarnpkg.com/fs-vacuum/-/fs-vacuum-1.2.10.tgz#b7629bec07a4031a2548fdf99f5ecf1cc8b31e36"
+ integrity sha1-t2Kb7AekAxolSP35n17PHMizHjY=
+ dependencies:
+ graceful-fs "^4.1.2"
+ path-is-inside "^1.0.1"
+ rimraf "^2.5.2"
+
+fs-write-stream-atomic@^1.0.8, fs-write-stream-atomic@~1.0.10:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
+ integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=
+ dependencies:
+ graceful-fs "^4.1.2"
+ iferr "^0.1.5"
+ imurmurhash "^0.1.4"
+ readable-stream "1 || 2"
+
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
+fstream@^1.0.0, fstream@^1.0.12:
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"
+ integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==
+ dependencies:
+ graceful-fs "^4.1.2"
+ inherits "~2.0.0"
+ mkdirp ">=0.5 0"
+ rimraf "2"
+
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
pretty-format "^20.0.3"
realm-utils "^1.0.7"
+fuse.js@^3.4.5:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.6.1.tgz#7de85fdd6e1b3377c23ce010892656385fd9b10c"
+ integrity sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+genfun@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537"
+ integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==
+
gensync@^1.0.0-beta.1:
version "1.0.0-beta.1"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
+gentle-fs@^2.0.1, gentle-fs@^2.3.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/gentle-fs/-/gentle-fs-2.3.1.tgz#11201bf66c18f930ddca72cf69460bdfa05727b1"
+ integrity sha512-OlwBBwqCFPcjm33rF2BjW+Pr6/ll2741l+xooiwTCeaX2CA1ZuclavyMBe0/KlR21/XGsgY6hzEQZ15BdNa13Q==
+ dependencies:
+ aproba "^1.1.2"
+ chownr "^1.1.2"
+ cmd-shim "^3.0.3"
+ fs-vacuum "^1.2.10"
+ graceful-fs "^4.1.11"
+ iferr "^0.1.5"
+ infer-owner "^1.0.4"
+ mkdirp "^0.5.1"
+ path-is-inside "^1.0.2"
+ read-cmd-shim "^1.0.1"
+ slide "^1.1.6"
+
+get-caller-file@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
+ integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
+
get-caller-file@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==
+get-stream@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+ integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
+
get-stream@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
dependencies:
is-glob "^4.0.1"
-glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
+glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.2:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
once "^1.3.0"
path-is-absolute "^1.0.0"
+global-dirs@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
+ integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=
+ dependencies:
+ ini "^1.3.4"
+
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
dependencies:
type-fest "^0.8.1"
-graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.4:
+got@^6.7.1:
+ version "6.7.1"
+ resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0"
+ integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=
+ dependencies:
+ create-error-class "^3.0.0"
+ duplexer3 "^0.1.4"
+ get-stream "^3.0.0"
+ is-redirect "^1.0.0"
+ is-retry-allowed "^1.0.0"
+ is-stream "^1.0.0"
+ lowercase-keys "^1.0.0"
+ safe-buffer "^5.0.1"
+ timed-out "^4.0.0"
+ unzip-response "^2.0.1"
+ url-parse-lax "^1.0.0"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.4:
version "4.2.4"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
+graceful-fs@~4.1.11:
+ version "4.1.15"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
+ integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
+
growly@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+has-unicode@^2.0.0, has-unicode@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+ integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
+
has-value@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
resolved "https://registry.yarnpkg.com/hoist-non-inferno-statics/-/hoist-non-inferno-statics-1.1.3.tgz#7d870f4160bfb6a59269b45c343c027f0e30ab35"
integrity sha1-fYcPQWC/tqWSabRcNDwCfw4wqzU=
-hosted-git-info@^2.1.4:
+hosted-git-info@^2.1.4, hosted-git-info@^2.6.0, hosted-git-info@^2.7.1:
version "2.8.8"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
+html-parse-stringify2@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a"
+ integrity sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=
+ dependencies:
+ void-elements "^2.0.1"
+
htmlparser2@^3.9.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
inherits "^2.0.1"
readable-stream "^3.1.1"
+http-cache-semantics@^3.8.0, http-cache-semantics@^3.8.1:
+ version "3.8.1"
+ resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
+ integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==
+
http-errors@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
+http-proxy-agent@^2.0.0, http-proxy-agent@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
+ integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==
+ dependencies:
+ agent-base "4"
+ debug "3.1.0"
+
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
jsprim "^1.2.2"
sshpk "^1.7.0"
+https-proxy-agent@^2.1.0, https-proxy-agent@^2.2.0, https-proxy-agent@^2.2.1:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
+ integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==
+ dependencies:
+ agent-base "^4.3.0"
+ debug "^3.1.0"
+
human-signals@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
+humanize-ms@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
+ integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=
+ dependencies:
+ ms "^2.0.0"
+
husky@^4.2.5:
version "4.2.5"
resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.5.tgz#2b4f7622673a71579f901d9885ed448394b5fa36"
slash "^3.0.0"
which-pm-runs "^1.0.0"
+i18next@^19.4.1:
+ version "19.7.0"
+ resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.7.0.tgz#e637bbbf36481d34b7d5e6d3b04e1bb654bf2a26"
+ integrity sha512-sxZhj6u7HbEYOMx81oGwq5MiXISRBVg2wRY3n6YIbe+HtU8ydzlGzv6ErHdrRKYxATBFssVXYbc3lNZoyB4vfA==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+
iconv-lite@0.4.24, iconv-lite@^0.4.17:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
dependencies:
safer-buffer ">= 2.1.2 < 3"
+iconv-lite@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01"
+ integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
ieee754@^1.1.8:
version "1.1.13"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
+iferr@^0.1.5, iferr@~0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
+ integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
+
+ignore-walk@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37"
+ integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==
+ dependencies:
+ minimatch "^3.0.4"
+
ignore@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
-import-fresh@^3.0.0, import-fresh@^3.1.0:
+import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
parent-module "^1.0.0"
resolve-from "^4.0.0"
+import-lazy@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
+ integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=
+
import-local@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6"
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
-inferno-create-element@^7.4.3:
+infer-owner@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
+ integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
+
+inferno-clone-vnode@^7.4.2:
+ version "7.4.3"
+ resolved "https://registry.yarnpkg.com/inferno-clone-vnode/-/inferno-clone-vnode-7.4.3.tgz#4372ddfb2fea6797945fd27b8590cc59c3b85e53"
+ integrity sha512-vNAw3kDREcEEL0GzTvZAHSwqPxB7o+H9J42czfYTEI6eJmXe78gbR75Q77sSG24K2U0CpYUQmtv3KtFHvIG/Ow==
+ dependencies:
+ inferno "7.4.3"
+
+inferno-create-element@^7.4.2, inferno-create-element@^7.4.3:
version "7.4.3"
resolved "https://registry.yarnpkg.com/inferno-create-element/-/inferno-create-element-7.4.3.tgz#48eb82f500d270bff17f0ba406c06813af8da763"
integrity sha512-eO21kZ/lCHJMNr6+2/RlAJbXxTqGuc+XhQJNFmVAj0SuvK1peXYrzxjgrb2sX7JnscAfaMxRN68QP2U+d8WcYQ==
dependencies:
inferno "7.4.3"
+inferno-helmet@^5.2.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/inferno-helmet/-/inferno-helmet-5.2.1.tgz#3717f325760aa14abeae82a78af7213f9055a3dc"
+ integrity sha512-9xzUGENVoz8qk67s0UhHlGNGZKG9Ia0mk5KoCNgkkIcGNhk7mNIINm7jJ5OOigVetz2DwI94jHzouTggb49AJg==
+ dependencies:
+ deep-equal "^1.0.1"
+ inferno-side-effect "^1.1.5"
+ object-assign "^4.1.1"
+
inferno-hydrate@^7.4.3:
version "7.4.3"
resolved "https://registry.yarnpkg.com/inferno-hydrate/-/inferno-hydrate-7.4.3.tgz#17ecf12309fe2f91cc05308ffb7c91594c963540"
dependencies:
inferno "7.4.3"
+"inferno-i18next@github:nimbusec-oss/inferno-i18next#semver:^7.4.2":
+ version "7.4.2"
+ resolved "https://codeload.github.com/nimbusec-oss/inferno-i18next/tar.gz/54b9be591ccd62c53799ad23e35f17144a62f909"
+ dependencies:
+ html-parse-stringify2 "^2.0.1"
+ inferno "^7.4.2"
+ inferno-clone-vnode "^7.4.2"
+ inferno-create-element "^7.4.2"
+ inferno-shared "^7.4.2"
+ inferno-vnode-flags "^7.4.2"
+
inferno-router@^7.4.3:
version "7.4.3"
resolved "https://registry.yarnpkg.com/inferno-router/-/inferno-router-7.4.3.tgz#a69d1b328247c68e6b774e49d6deda0ab4799e89"
dependencies:
inferno "7.4.3"
-inferno-shared@7.4.3:
+inferno-shared@7.4.3, inferno-shared@^7.4.2:
version "7.4.3"
resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-7.4.3.tgz#9fbc8cf66f1d6e3d5ad0d5db9a1d90f0bbe38e9f"
integrity sha512-g2e50wtzreOZflFAoMLkOF9uCjqHx7582vEA6SV+fxS8Hp8BCXcbD85ovbrRaUNtmlzRVFfvbFN1G/yb+ZINbQ==
+inferno-side-effect@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/inferno-side-effect/-/inferno-side-effect-1.1.5.tgz#a874c80dbc73602aafc1e0f3f3f1ec216a916271"
+ integrity sha512-Q2O2qExGjBTPRfwM1suQPBN5FBHhANccCcEvz/3Dr7VcMFelqxnE0qjnlXVQ248S409nA6VtpiBwT7xBz4WyqA==
+ dependencies:
+ exenv "^1.2.1"
+ npm "^5.8.0"
+ shallowequal "^1.0.1"
+
inferno-test-utils@^7.4.3:
version "7.4.3"
resolved "https://registry.yarnpkg.com/inferno-test-utils/-/inferno-test-utils-7.4.3.tgz#57d92f7a9cc39fa4d47fe5fa598297bd4232695f"
dependencies:
inferno "7.4.3"
-inferno-vnode-flags@7.4.3:
+inferno-vnode-flags@7.4.3, inferno-vnode-flags@^7.4.2:
version "7.4.3"
resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-7.4.3.tgz#267879b18ea89579be40a2b8d9a2221c19126ab0"
integrity sha512-NRaE5O64w2GZltBc2Eh0sof2BKOE19BxCj2xRdE6q9lHlQoirtPhMttYgWVBHDbXEy1BWesLla/IMj3MEca48g==
resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-5.6.1.tgz#43974687cc54f22c24ecee116376f6c1bd697499"
integrity sha512-DDb6R17FX0Fs1SH+7SSVfHEFiYcN2U/DlPHB6qk2HOP149qtYgeRi5h0AAJswJCzBkV/QcBSyRirn0SAzl1NAQ==
-inferno@7.4.3, inferno@^7.4.3:
+inferno@7.4.3, inferno@^7.4.2, inferno@^7.4.3:
version "7.4.3"
resolved "https://registry.yarnpkg.com/inferno/-/inferno-7.4.3.tgz#91f9f22227963f010ef2b6fa37e17f0c9897e2a6"
integrity sha512-ZUk7dUKGQRlkU8ssGEHTuRmzJP0X0BYjn+xQg26uTUeFZZarjmQGXLtzJZvQY7r7uvzZjSDMvz0xwg/Ai+T8Rg==
inferno-vnode-flags "7.4.3"
opencollective-postinstall "^2.0.2"
-inflight@^1.0.4:
+inflight@^1.0.4, inflight@~1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
+ integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+
+init-package-json@^1.10.3:
+ version "1.10.3"
+ resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-1.10.3.tgz#45ffe2f610a8ca134f2bd1db5637b235070f6cbe"
+ integrity sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==
+ dependencies:
+ glob "^7.1.1"
+ npm-package-arg "^4.0.0 || ^5.0.0 || ^6.0.0"
+ promzard "^0.3.0"
+ read "~1.0.1"
+ read-package-json "1 || 2"
+ semver "2.x || 3.x || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+ validate-npm-package-name "^3.0.0"
+
inquirer@^3.0.6:
version "3.3.0"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
has "^1.0.3"
side-channel "^1.0.2"
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+ integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
+
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
+ip@1.1.5, ip@^1.1.4:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
+ integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
+
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
dependencies:
kind-of "^6.0.0"
+is-arguments@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
+ integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
+
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74=
+ dependencies:
+ builtin-modules "^1.0.0"
+
is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb"
integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==
+is-ci@^1.0.10:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c"
+ integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==
+ dependencies:
+ ci-info "^1.5.0"
+
is-ci@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
dependencies:
ci-info "^2.0.0"
+is-cidr@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-1.0.0.tgz#fb5aacf659255310359da32cae03e40c6a1c2afc"
+ integrity sha1-+1qs9lklUxA1naMsrgPkDGocKvw=
+ dependencies:
+ cidr-regex "1.0.6"
+
is-data-descriptor@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
+ dependencies:
+ number-is-nan "^1.0.0"
+
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
dependencies:
is-extglob "^2.1.1"
+is-installed-globally@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80"
+ integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=
+ dependencies:
+ global-dirs "^0.1.0"
+ is-path-inside "^1.0.0"
+
+is-negative-zero@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
+ integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
+
+is-npm@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
+ integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ=
+
is-number-object@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
-is-obj@^1.0.1:
+is-obj@^1.0.0, is-obj@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
+is-path-inside@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
+ integrity sha1-jvW33lBDej/cprToZe96pVy0gDY=
+ dependencies:
+ path-is-inside "^1.0.1"
+
is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU=
-is-regex@^1.0.5, is-regex@^1.1.0:
+is-redirect@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
+ integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=
+
+is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk=
-is-stream@^1.1.0:
+is-retry-allowed@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
+ integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==
+
+is-stream@^1.0.0, is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+isomorphic-cookie@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/isomorphic-cookie/-/isomorphic-cookie-1.2.4.tgz#b23cc170b4430be1af7cef659bcb77776c8cc351"
+ integrity sha512-Ua/H7NL/NqJyhM14gOisDwPQt5HCNohl23/i8g68EoItOoPQEydG+ZJ0A0i815FSiUQ+0ImtSLdo6d1psus/IQ==
+ dependencies:
+ cookie "^0.1.2"
+ lodash.pick "^4.4.0"
+
+isomorphic-ws@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
+ integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
+
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
import-local "^3.0.2"
jest-cli "^26.4.2"
+js-cookie@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
+ integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
+
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
+json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+ integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
json-parse-even-better-errors@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.0.tgz#371873c5ffa44304a6ba12419bcfa95f404ae081"
optionalDependencies:
graceful-fs "^4.1.6"
+jsonparse@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
+ integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
+
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
array-includes "^3.1.1"
object.assign "^4.1.0"
+jwt-decode@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
+ integrity sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=
+
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
dependencies:
language-subtag-registry "~0.3.2"
+latest-version@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
+ integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=
+ dependencies:
+ package-json "^4.0.0"
+
+lazy-property@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lazy-property/-/lazy-property-1.0.0.tgz#84ddc4b370679ba8bd4cdcfa4c06b43d57111147"
+ integrity sha1-hN3Es3Bnm6i9TNz6TAa0PVcREUc=
+
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=
+ dependencies:
+ invert-kv "^1.0.0"
+
lego-api@^1.0.7:
version "1.0.8"
resolved "https://registry.yarnpkg.com/lego-api/-/lego-api-1.0.8.tgz#5e26be726c5e11d540f89e7c6b1abf8c5834bd01"
dependencies:
chain-able "^3.0.0"
+lemmy-js-client@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-1.0.8.tgz#98e34c8e3cd07427f883f60fad376dc4d6f46e7f"
+ integrity sha512-YZxD3+8RGz7cRKdI8EIe5iQqQIMm5WzdNz6zZ7/CdkMtXUv6YuMOEv8HLTvBoGuaWIJwlMJ+23NIarxlT26IEw==
+
leven@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
prelude-ls "~1.1.2"
type-check "~0.3.2"
+libcipm@^1.6.2:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/libcipm/-/libcipm-1.6.3.tgz#dc4052d710941547782d85bbdb3c77eedec733ff"
+ integrity sha512-WUEjQk1aZDECb2MFnAbx6o7sJbBJWrWwt9rbinOmpc0cLKWgYJOvKNqCUN3sl2P9LFqPsnVT4Aj5SPw4/xKI5A==
+ dependencies:
+ bin-links "^1.1.2"
+ bluebird "^3.5.1"
+ find-npm-prefix "^1.0.2"
+ graceful-fs "^4.1.11"
+ lock-verify "^2.0.2"
+ npm-lifecycle "^2.0.3"
+ npm-logical-tree "^1.2.1"
+ npm-package-arg "^6.1.0"
+ pacote "^8.1.6"
+ protoduck "^5.0.0"
+ read-package-json "^2.0.13"
+ rimraf "^2.6.2"
+ worker-farm "^1.6.0"
+
+libnpx@^10.2.0:
+ version "10.2.4"
+ resolved "https://registry.yarnpkg.com/libnpx/-/libnpx-10.2.4.tgz#ef0e3258e29aef2ec7ee3276115e20e67f67d4ee"
+ integrity sha512-BPc0D1cOjBeS8VIBKUu5F80s6njm0wbVt7CsGMrIcJ+SI7pi7V0uVPGpEMH9H5L8csOcclTxAXFE2VAsJXUhfA==
+ dependencies:
+ dotenv "^5.0.1"
+ npm-package-arg "^6.0.0"
+ rimraf "^2.6.2"
+ safe-buffer "^5.1.0"
+ update-notifier "^2.3.0"
+ which "^1.3.0"
+ y18n "^4.0.0"
+ yargs "^14.2.3"
+
lines-and-columns@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+linkify-it@^3.0.1:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8"
+ integrity sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==
+ dependencies:
+ uc.micro "^1.0.1"
+
lint-staged@^10.1.3:
- version "10.2.11"
- resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.2.11.tgz#713c80877f2dc8b609b05bc59020234e766c9720"
- integrity sha512-LRRrSogzbixYaZItE2APaS4l2eJMjjf5MbclRZpLJtcQJShcvUzKXsNeZgsLIZ0H0+fg2tL4B59fU9wHIHtFIA==
+ version "10.2.13"
+ resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.2.13.tgz#b9c504683470edfc464b7d3fe3845a5a1efcd814"
+ integrity sha512-conwlukNV6aL9SiMWjFtDp5exeDnTMekdNPDZsKGnpfQuHcO0E3L3Bbf58lcR+M7vk6LpCilxDAVks/DDVBYlA==
dependencies:
- chalk "^4.0.0"
- cli-truncate "2.1.0"
- commander "^5.1.0"
- cosmiconfig "^6.0.0"
+ chalk "^4.1.0"
+ cli-truncate "^2.1.0"
+ commander "^6.0.0"
+ cosmiconfig "^7.0.0"
debug "^4.1.1"
dedent "^0.7.0"
- enquirer "^2.3.5"
- execa "^4.0.1"
- listr2 "^2.1.0"
+ enquirer "^2.3.6"
+ execa "^4.0.3"
+ listr2 "^2.6.0"
log-symbols "^4.0.0"
micromatch "^4.0.2"
normalize-path "^3.0.0"
string-argv "0.3.1"
stringify-object "^3.3.0"
-listr2@^2.1.0:
- version "2.6.0"
- resolved "https://registry.yarnpkg.com/listr2/-/listr2-2.6.0.tgz#788a3d202978a1b8582062952cbc49272c8e206a"
- integrity sha512-nwmqTJYQQ+AsKb4fCXH/6/UmLCEDL1jkRAdSn9M6cEUzoRGrs33YD/3N86gAZQnGZ6hxV18XSdlBcJ1GTmetJA==
+listr2@^2.6.0:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/listr2/-/listr2-2.6.2.tgz#4912eb01e1e2dd72ec37f3895a56bf2622d6f36a"
+ integrity sha512-6x6pKEMs8DSIpA/tixiYY2m/GcbgMplMVmhQAaLFxEtNSKLeWTGjtmU57xvv6QCm2XcqzyNXL/cTSVf4IChCRA==
dependencies:
chalk "^4.1.0"
cli-truncate "^2.1.0"
p-locate "^2.0.0"
path-exists "^3.0.0"
+locate-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+ integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+ dependencies:
+ p-locate "^3.0.0"
+ path-exists "^3.0.0"
+
locate-path@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
dependencies:
p-locate "^4.1.0"
+lock-verify@^2.0.2:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/lock-verify/-/lock-verify-2.2.1.tgz#81107948c51ed16f97b96ff8b60675affb243fc1"
+ integrity sha512-n0Zw2DVupKfZMazy/HIFVNohJ1z8fIoZ77WBnyyBGG6ixw83uJNyrbiJvvHWe1QKkGiBCjj8RCPlymltliqEww==
+ dependencies:
+ "@iarna/cli" "^1.2.0"
+ npm-package-arg "^6.1.0"
+ semver "^5.4.1"
+
+lockfile@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.4.tgz#07f819d25ae48f87e538e6578b6964a4981a5609"
+ integrity sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==
+ dependencies:
+ signal-exit "^3.0.2"
+
+lodash._baseuniq@~4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
+ integrity sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg=
+ dependencies:
+ lodash._createset "~4.0.0"
+ lodash._root "~3.0.0"
+
+lodash._createset@~4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
+ integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
+
+lodash._root@~3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
+ integrity sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=
+
+lodash.clonedeep@~4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+ integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
+
lodash.escape@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+lodash.pick@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
+ integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
+
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
+lodash.union@~4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
+ integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=
+
+lodash.uniq@~4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+ integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+
+lodash.without@~4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
+ integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
+
lodash.zip@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.zip/-/lodash.zip-4.2.0.tgz#ec6662e4896408ed4ab6c542a3990b72cc080020"
integrity sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=
+lodash@^3.10.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
+ integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
+
lodash@^4.15.0, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.3.0:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
+lowercase-keys@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
+ integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
+
+lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.3:
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
+ integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+lru-cache@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
+make-dir@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
+ integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==
+ dependencies:
+ pify "^3.0.0"
+
make-dir@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+"make-fetch-happen@^2.5.0 || 3 || 4", make-fetch-happen@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-4.0.2.tgz#2d156b11696fb32bffbafe1ac1bc085dd6c78a79"
+ integrity sha512-YMJrAjHSb/BordlsDEcVcPyTbiJKkzqMf48N8dAJZT9Zjctrkb6Yg4TY9Sq2AwSIQJFn5qBBKVTYt3vP5FMIHA==
+ dependencies:
+ agentkeepalive "^3.4.1"
+ cacache "^11.3.3"
+ http-cache-semantics "^3.8.1"
+ http-proxy-agent "^2.1.0"
+ https-proxy-agent "^2.2.1"
+ lru-cache "^5.1.1"
+ mississippi "^3.0.0"
+ node-fetch-npm "^2.0.2"
+ promise-retry "^1.1.1"
+ socks-proxy-agent "^4.0.0"
+ ssri "^6.0.0"
+
+make-fetch-happen@^2.6.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-2.6.0.tgz#8474aa52198f6b1ae4f3094c04e8370d35ea8a38"
+ integrity sha512-FFq0lNI0ax+n9IWzWpH8A4JdgYiAp2DDYIZ3rsaav8JDe8I+72CzK6PQW/oom15YDZpV5bYW/9INd6nIJ2ZfZw==
+ dependencies:
+ agentkeepalive "^3.3.0"
+ cacache "^10.0.0"
+ http-cache-semantics "^3.8.0"
+ http-proxy-agent "^2.0.0"
+ https-proxy-agent "^2.1.0"
+ lru-cache "^4.1.1"
+ mississippi "^1.2.0"
+ node-fetch-npm "^2.0.2"
+ promise-retry "^1.1.1"
+ socks-proxy-agent "^3.0.1"
+ ssri "^5.0.0"
+
+make-fetch-happen@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-3.0.0.tgz#7b661d2372fc4710ab5cc8e1fa3c290eea69a961"
+ integrity sha512-FmWY7gC0mL6Z4N86vE14+m719JKE4H0A+pyiOH18B025gF/C113pyfb4gHDDYP5cqnRMHOz06JGdmffC/SES+w==
+ dependencies:
+ agentkeepalive "^3.4.1"
+ cacache "^10.0.4"
+ http-cache-semantics "^3.8.1"
+ http-proxy-agent "^2.1.0"
+ https-proxy-agent "^2.2.0"
+ lru-cache "^4.1.2"
+ mississippi "^3.0.0"
+ node-fetch-npm "^2.0.2"
+ promise-retry "^1.1.1"
+ socks-proxy-agent "^3.0.1"
+ ssri "^5.2.4"
+
makeerror@1.0.x:
version "1.0.11"
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
dependencies:
object-visit "^1.0.0"
+markdown-it-container@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/markdown-it-container/-/markdown-it-container-3.0.0.tgz#1d19b06040a020f9a827577bb7dbf67aa5de9a5b"
+ integrity sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw==
+
+markdown-it-emoji@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz#9bee0e9a990a963ba96df6980c4fddb05dfb4dcc"
+ integrity sha1-m+4OmpkKljupbfaYDE/dsF37Tcw=
+
+markdown-it-sub@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz#375fd6026eae7ddcb012497f6411195ea1e3afe8"
+ integrity sha1-N1/WAm6ufdywEkl/ZBEZXqHjr+g=
+
+markdown-it-sup@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz#cb9c9ff91a5255ac08f3fd3d63286e15df0a1fc3"
+ integrity sha1-y5yf+RpSVawI8/09YyhuFd8KH8M=
+
+markdown-it@^11.0.0:
+ version "11.0.0"
+ resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-11.0.0.tgz#dbfc30363e43d756ebc52c38586b91b90046b876"
+ integrity sha512-+CvOnmbSubmQFSA9dKz1BRiaSMV7rhexl3sngKqFyXSagoA3fBdJQ8oZWtRy2knXdpDXaBw44euz37DeJQ9asg==
+ dependencies:
+ argparse "^1.0.7"
+ entities "~2.0.0"
+ linkify-it "^3.0.1"
+ mdurl "^1.0.1"
+ uc.micro "^1.0.5"
+
math-random@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c"
integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==
+mdurl@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
+ integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
+
+meant@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.2.tgz#5d0c78310a3d8ae1408a16be0fe0bd42a969f560"
+ integrity sha512-KN+1uowN/NK+sT/Lzx7WSGIj2u+3xe5n2LbwObfjOhPZiA+cCfCm6idVl0RkEfjThkw5XJ96CyRcanq6GmKtUg==
+
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+mem@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
+ integrity sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=
+ dependencies:
+ mimic-fn "^1.0.0"
+
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+minipass@^2.3.3, minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
+ integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
+ dependencies:
+ safe-buffer "^5.1.2"
+ yallist "^3.0.0"
+
+minizlib@^1.2.1:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
+ integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
+ dependencies:
+ minipass "^2.9.0"
+
+mississippi@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-1.3.1.tgz#2a8bb465e86550ac8b36a7b6f45599171d78671e"
+ integrity sha512-/6rB8YXFbAtsUVRphIRQqB0+9c7VaPHCjVtvto+JqwVxgz8Zz+I+f68/JgQ+Pb4VlZb2svA9OtdXnHHsZz7ltg==
+ dependencies:
+ concat-stream "^1.5.0"
+ duplexify "^3.4.2"
+ end-of-stream "^1.1.0"
+ flush-write-stream "^1.0.0"
+ from2 "^2.1.0"
+ parallel-transform "^1.1.0"
+ pump "^1.0.0"
+ pumpify "^1.3.3"
+ stream-each "^1.1.0"
+ through2 "^2.0.0"
+
+mississippi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
+ integrity sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==
+ dependencies:
+ concat-stream "^1.5.0"
+ duplexify "^3.4.2"
+ end-of-stream "^1.1.0"
+ flush-write-stream "^1.0.0"
+ from2 "^2.1.0"
+ parallel-transform "^1.1.0"
+ pump "^2.0.1"
+ pumpify "^1.3.3"
+ stream-each "^1.1.0"
+ through2 "^2.0.0"
+
+mississippi@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
+ integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==
+ dependencies:
+ concat-stream "^1.5.0"
+ duplexify "^3.4.2"
+ end-of-stream "^1.1.0"
+ flush-write-stream "^1.0.0"
+ from2 "^2.1.0"
+ parallel-transform "^1.1.0"
+ pump "^3.0.0"
+ pumpify "^1.3.3"
+ stream-each "^1.1.0"
+ through2 "^2.0.0"
+
mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
for-in "^1.0.2"
is-extendable "^1.0.1"
-mkdirp@^0.5.1:
+"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
dependencies:
minimist "^1.2.5"
+moment@^2.24.0:
+ version "2.27.0"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
+ integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==
+
moo@^0.5.0:
version "0.5.1"
resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4"
integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==
-ms@2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+move-concurrently@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
+ integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=
+ dependencies:
+ aproba "^1.1.1"
+ copy-concurrently "^1.0.0"
+ fs-write-stream-atomic "^1.0.8"
+ mkdirp "^0.5.1"
+ rimraf "^2.5.4"
+ run-queue "^1.0.3"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
-ms@^2.1.1:
+ms@^2.0.0, ms@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
+mute-stream@~0.0.4:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
+ integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
+
nan@^2.12.1:
version "2.14.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+node-fetch-npm@^2.0.2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz#6507d0e17a9ec0be3bec516958a497cec54bf5a4"
+ integrity sha512-iOuIQDWDyjhv9qSDrj9aq/klt6F9z1p2otB3AV7v3zBDcL/x+OfGsvGQZZCcMZbUf4Ujw1xGNQkjvGnVT22cKg==
+ dependencies:
+ encoding "^0.1.11"
+ json-parse-better-errors "^1.0.0"
+ safe-buffer "^5.1.1"
+
+node-fetch@^2.6.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
+ integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
+
+node-gyp@^3.6.2:
+ version "3.8.0"
+ resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
+ integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==
+ dependencies:
+ fstream "^1.0.0"
+ glob "^7.0.3"
+ graceful-fs "^4.1.2"
+ mkdirp "^0.5.0"
+ nopt "2 || 3"
+ npmlog "0 || 1 || 2 || 3 || 4"
+ osenv "0"
+ request "^2.87.0"
+ rimraf "2"
+ semver "~5.3.0"
+ tar "^2.0.0"
+ which "1"
+
+node-gyp@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-4.0.0.tgz#972654af4e5dd0cd2a19081b4b46fe0442ba6f45"
+ integrity sha512-2XiryJ8sICNo6ej8d0idXDEMKfVfFK7kekGCtJAuelGsYHQxhj13KTf95swTCN2dZ/4lTfZ84Fu31jqJEEgjWA==
+ dependencies:
+ glob "^7.0.3"
+ graceful-fs "^4.1.2"
+ mkdirp "^0.5.0"
+ nopt "2 || 3"
+ npmlog "0 || 1 || 2 || 3 || 4"
+ osenv "0"
+ request "^2.87.0"
+ rimraf "2"
+ semver "~5.3.0"
+ tar "^4.4.8"
+ which "1"
+
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
uuid "^8.3.0"
which "^2.0.2"
-normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
+"nopt@2 || 3":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
+ integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k=
+ dependencies:
+ abbrev "1"
+
+nopt@~4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
+ integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0, "normalize-package-data@~1.0.1 || ^2.0.0":
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
+normalize-package-data@~2.4.0:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.2.tgz#6b2abd85774e51f7936f1395e45acb905dc849b2"
+ integrity sha512-YcMnjqeoUckXTPKZSAsPjUPLxH85XotbpqK3w4RyCwdFQSU5FxxBys8buehkSfg0j9fKvV1hn7O0+8reEgkAiw==
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+npm-audit-report@^1.0.9:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/npm-audit-report/-/npm-audit-report-1.3.3.tgz#8226deeb253b55176ed147592a3995442f2179ed"
+ integrity sha512-8nH/JjsFfAWMvn474HB9mpmMjrnKb1Hx/oTAdjv4PT9iZBvBxiZ+wtDUapHCJwLqYGQVPaAfs+vL5+5k9QndXw==
+ dependencies:
+ cli-table3 "^0.5.0"
+ console-control-strings "^1.1.0"
+
+npm-bundled@^1.0.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b"
+ integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==
+ dependencies:
+ npm-normalize-package-bin "^1.0.1"
+
+npm-cache-filename@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/npm-cache-filename/-/npm-cache-filename-1.0.2.tgz#ded306c5b0bfc870a9e9faf823bc5f283e05ae11"
+ integrity sha1-3tMGxbC/yHCp6fr4I7xfKD4FrhE=
+
+npm-install-checks@~3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-3.0.2.tgz#ab2e32ad27baa46720706908e5b14c1852de44d9"
+ integrity sha512-E4kzkyZDIWoin6uT5howP8VDvkM+E8IQDcHAycaAxMbwkqhIg5eEYALnXOl3Hq9MrkdQB/2/g1xwBINXdKSRkg==
+ dependencies:
+ semver "^2.3.0 || 3.x || 4 || 5"
+
+npm-lifecycle@^2.0.1, npm-lifecycle@^2.0.3:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-2.1.1.tgz#0027c09646f0fd346c5c93377bdaba59c6748fdf"
+ integrity sha512-+Vg6I60Z75V/09pdcH5iUo/99Q/vop35PaI99elvxk56azSVVsdsSsS/sXqKDNwbRRNN1qSxkcO45ZOu0yOWew==
+ dependencies:
+ byline "^5.0.0"
+ graceful-fs "^4.1.15"
+ node-gyp "^4.0.0"
+ resolve-from "^4.0.0"
+ slide "^1.1.6"
+ uid-number "0.0.6"
+ umask "^1.1.0"
+ which "^1.3.1"
+
+npm-logical-tree@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/npm-logical-tree/-/npm-logical-tree-1.2.1.tgz#44610141ca24664cad35d1e607176193fd8f5b88"
+ integrity sha512-AJI/qxDB2PWI4LG1CYN579AY1vCiNyWfkiquCsJWqntRu/WwimVrC8yXeILBFHDwxfOejxewlmnvW9XXjMlYIg==
+
+npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2"
+ integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==
+
+"npm-package-arg@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.1.tgz#02168cb0a49a2b75bf988a28698de7b529df5cb7"
+ integrity sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==
+ dependencies:
+ hosted-git-info "^2.7.1"
+ osenv "^0.1.5"
+ semver "^5.6.0"
+ validate-npm-package-name "^3.0.0"
+
+npm-packlist@^1.1.10:
+ version "1.4.8"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e"
+ integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==
+ dependencies:
+ ignore-walk "^3.0.1"
+ npm-bundled "^1.0.1"
+ npm-normalize-package-bin "^1.0.1"
+
+npm-packlist@~1.1.10:
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.12.tgz#22bde2ebc12e72ca482abd67afc51eb49377243a"
+ integrity sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==
+ dependencies:
+ ignore-walk "^3.0.1"
+ npm-bundled "^1.0.1"
+
+npm-pick-manifest@^2.1.0:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-2.2.3.tgz#32111d2a9562638bb2c8f2bf27f7f3092c8fae40"
+ integrity sha512-+IluBC5K201+gRU85vFlUwX3PFShZAbAgDNp2ewJdWMVSppdo/Zih0ul2Ecky/X7b51J7LrrUAP+XOmOCvYZqA==
+ dependencies:
+ figgy-pudding "^3.5.1"
+ npm-package-arg "^6.0.0"
+ semver "^5.4.1"
+
+npm-profile@^3.0.1:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-3.0.2.tgz#58d568f1b56ef769602fd0aed8c43fa0e0de0f57"
+ integrity sha512-rEJOFR6PbwOvvhGa2YTNOJQKNuc6RovJ6T50xPU7pS9h/zKPNCJ+VHZY2OFXyZvEi+UQYtHRTp8O/YM3tUD20A==
+ dependencies:
+ aproba "^1.1.2 || 2"
+ make-fetch-happen "^2.5.0 || 3 || 4"
+
+npm-registry-client@^8.5.1:
+ version "8.6.0"
+ resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-8.6.0.tgz#7f1529f91450732e89f8518e0f21459deea3e4c4"
+ integrity sha512-Qs6P6nnopig+Y8gbzpeN/dkt+n7IyVd8f45NTMotGk6Qo7GfBmzwYx6jRLoOOgKiMnaQfYxsuyQlD8Mc3guBhg==
+ dependencies:
+ concat-stream "^1.5.2"
+ graceful-fs "^4.1.6"
+ normalize-package-data "~1.0.1 || ^2.0.0"
+ npm-package-arg "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
+ once "^1.3.3"
+ request "^2.74.0"
+ retry "^0.10.0"
+ safe-buffer "^5.1.1"
+ semver "2 >=2.2.1 || 3.x || 4 || 5"
+ slide "^1.1.3"
+ ssri "^5.2.4"
+ optionalDependencies:
+ npmlog "2 || ^3.1.0 || ^4.0.0"
+
+npm-registry-fetch@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-1.1.1.tgz#710bc5947d9ee2c549375072dab6d5d17baf2eb2"
+ integrity sha512-ev+zxOXsgAqRsR8Rk+ErjgWOlbrXcqGdme94/VNdjDo1q8TSy10Pp8xgDv/ZmMk2jG/KvGtXUNG4GS3+l6xbDw==
+ dependencies:
+ bluebird "^3.5.1"
+ figgy-pudding "^3.0.0"
+ lru-cache "^4.1.2"
+ make-fetch-happen "^3.0.0"
+ npm-package-arg "^6.0.0"
+ safe-buffer "^5.1.1"
+
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
dependencies:
path-key "^3.0.0"
+npm-user-validate@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-1.0.0.tgz#8ceca0f5cea04d4e93519ef72d0557a75122e951"
+ integrity sha1-jOyg9c6gTU6TUZ73LQVXp1Ei6VE=
+
+npm@^5.8.0:
+ version "5.10.0"
+ resolved "https://registry.yarnpkg.com/npm/-/npm-5.10.0.tgz#3bec62312c94a9b0f48f208e00b98bf0304b40db"
+ integrity sha512-lvjvjgR5wG2RJ2uqak1xtZcVAWMwVOzN5HkUlUj/n8rU1f3A0fNn+7HwOzH9Lyf0Ppyu9ApgsEpHczOSnx1cwA==
+ dependencies:
+ JSONStream "^1.3.2"
+ abbrev "~1.1.1"
+ ansi-regex "~3.0.0"
+ ansicolors "~0.3.2"
+ ansistyles "~0.1.3"
+ aproba "~1.2.0"
+ archy "~1.0.0"
+ bin-links "^1.1.0"
+ bluebird "~3.5.1"
+ byte-size "^4.0.2"
+ cacache "^10.0.4"
+ call-limit "~1.1.0"
+ chownr "~1.0.1"
+ cli-columns "^3.1.2"
+ cli-table2 "~0.2.0"
+ cmd-shim "~2.0.2"
+ columnify "~1.5.4"
+ config-chain "~1.1.11"
+ detect-indent "~5.0.0"
+ detect-newline "^2.1.0"
+ dezalgo "~1.0.3"
+ editor "~1.0.0"
+ find-npm-prefix "^1.0.2"
+ fs-vacuum "~1.2.10"
+ fs-write-stream-atomic "~1.0.10"
+ gentle-fs "^2.0.1"
+ glob "~7.1.2"
+ graceful-fs "~4.1.11"
+ has-unicode "~2.0.1"
+ hosted-git-info "^2.6.0"
+ iferr "~0.1.5"
+ inflight "~1.0.6"
+ inherits "~2.0.3"
+ ini "^1.3.5"
+ init-package-json "^1.10.3"
+ is-cidr "~1.0.0"
+ json-parse-better-errors "^1.0.2"
+ lazy-property "~1.0.0"
+ libcipm "^1.6.2"
+ libnpx "^10.2.0"
+ lock-verify "^2.0.2"
+ lockfile "^1.0.4"
+ lodash._baseuniq "~4.6.0"
+ lodash.clonedeep "~4.5.0"
+ lodash.union "~4.6.0"
+ lodash.uniq "~4.5.0"
+ lodash.without "~4.4.0"
+ lru-cache "^4.1.2"
+ meant "~1.0.1"
+ mississippi "^3.0.0"
+ mkdirp "~0.5.1"
+ move-concurrently "^1.0.1"
+ node-gyp "^3.6.2"
+ nopt "~4.0.1"
+ normalize-package-data "~2.4.0"
+ npm-audit-report "^1.0.9"
+ npm-cache-filename "~1.0.2"
+ npm-install-checks "~3.0.0"
+ npm-lifecycle "^2.0.1"
+ npm-package-arg "^6.1.0"
+ npm-packlist "~1.1.10"
+ npm-profile "^3.0.1"
+ npm-registry-client "^8.5.1"
+ npm-registry-fetch "^1.1.0"
+ npm-user-validate "~1.0.0"
+ npmlog "~4.1.2"
+ once "~1.4.0"
+ opener "~1.4.3"
+ osenv "^0.1.5"
+ pacote "^7.6.1"
+ path-is-inside "~1.0.2"
+ promise-inflight "~1.0.1"
+ qrcode-terminal "^0.12.0"
+ query-string "^6.1.0"
+ qw "~1.0.1"
+ read "~1.0.7"
+ read-cmd-shim "~1.0.1"
+ read-installed "~4.0.3"
+ read-package-json "^2.0.13"
+ read-package-tree "^5.2.1"
+ readable-stream "^2.3.6"
+ request "^2.85.0"
+ retry "^0.12.0"
+ rimraf "~2.6.2"
+ safe-buffer "^5.1.2"
+ semver "^5.5.0"
+ sha "~2.0.1"
+ slide "~1.1.6"
+ sorted-object "~2.0.1"
+ sorted-union-stream "~2.1.3"
+ ssri "^5.3.0"
+ strip-ansi "~4.0.0"
+ tar "^4.4.2"
+ text-table "~0.2.0"
+ tiny-relative-date "^1.3.0"
+ uid-number "0.0.6"
+ umask "~1.1.0"
+ unique-filename "~1.1.0"
+ unpipe "~1.0.0"
+ update-notifier "^2.5.0"
+ uuid "^3.2.1"
+ validate-npm-package-license "^3.0.3"
+ validate-npm-package-name "~3.0.0"
+ which "~1.3.0"
+ worker-farm "^1.6.0"
+ wrappy "~1.0.2"
+ write-file-atomic "^2.3.0"
+
+"npmlog@0 || 1 || 2 || 3 || 4", "npmlog@2 || ^3.1.0 || ^4.0.0", npmlog@~4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
nth-check@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
dependencies:
boolbase "~1.0.0"
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+ integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
+
nwsapi@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
-object-assign@^4.1.1:
+object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
define-property "^0.2.5"
kind-of "^3.0.3"
-object-inspect@^1.7.0:
+object-inspect@^1.7.0, object-inspect@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
-object-is@^1.0.2, object-is@^1.1.2:
+object-is@^1.0.1, object-is@^1.0.2, object-is@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
function-bind "^1.1.1"
has "^1.0.3"
+object.getownpropertydescriptors@^2.0.3:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
+ integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+
object.omit@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
dependencies:
ee-first "1.1.1"
-once@^1.3.0, once@^1.3.1, once@^1.4.0:
+once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0, once@~1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
+opener@~1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
+ integrity sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=
+
optionator@^0.8.1:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f"
integrity sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=
-os-tmpdir@~1.0.2:
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+ integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
+
+os-locale@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
+ integrity sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==
+ dependencies:
+ execa "^0.7.0"
+ lcid "^1.0.0"
+ mem "^1.1.0"
+
+os-tmpdir@^1.0.0, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+osenv@0, osenv@^0.1.4, osenv@^0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
+ integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
p-each-series@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48"
dependencies:
p-try "^1.0.0"
-p-limit@^2.2.0:
+p-limit@^2.0.0, p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
dependencies:
p-limit "^1.1.0"
+p-locate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+ integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+ dependencies:
+ p-limit "^2.0.0"
+
p-locate@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+package-json@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed"
+ integrity sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=
+ dependencies:
+ got "^6.7.1"
+ registry-auth-token "^3.0.1"
+ registry-url "^3.0.3"
+ semver "^5.1.0"
+
+pacote@^7.6.1:
+ version "7.6.1"
+ resolved "https://registry.yarnpkg.com/pacote/-/pacote-7.6.1.tgz#d44621c89a5a61f173989b60236757728387c094"
+ integrity sha512-2kRIsHxjuYC1KRUIK80AFIXKWy0IgtFj76nKcaunozKAOSlfT+DFh3EfeaaKvNHCWixgi0G0rLg11lJeyEnp/Q==
+ dependencies:
+ bluebird "^3.5.1"
+ cacache "^10.0.4"
+ get-stream "^3.0.0"
+ glob "^7.1.2"
+ lru-cache "^4.1.1"
+ make-fetch-happen "^2.6.0"
+ minimatch "^3.0.4"
+ mississippi "^3.0.0"
+ mkdirp "^0.5.1"
+ normalize-package-data "^2.4.0"
+ npm-package-arg "^6.0.0"
+ npm-packlist "^1.1.10"
+ npm-pick-manifest "^2.1.0"
+ osenv "^0.1.5"
+ promise-inflight "^1.0.1"
+ promise-retry "^1.1.1"
+ protoduck "^5.0.0"
+ rimraf "^2.6.2"
+ safe-buffer "^5.1.1"
+ semver "^5.5.0"
+ ssri "^5.2.4"
+ tar "^4.4.0"
+ unique-filename "^1.1.0"
+ which "^1.3.0"
+
+pacote@^8.1.6:
+ version "8.1.6"
+ resolved "https://registry.yarnpkg.com/pacote/-/pacote-8.1.6.tgz#8e647564d38156367e7a9dc47a79ca1ab278d46e"
+ integrity sha512-wTOOfpaAQNEQNtPEx92x9Y9kRWVu45v583XT8x2oEV2xRB74+xdqMZIeGW4uFvAyZdmSBtye+wKdyyLaT8pcmw==
+ dependencies:
+ bluebird "^3.5.1"
+ cacache "^11.0.2"
+ get-stream "^3.0.0"
+ glob "^7.1.2"
+ lru-cache "^4.1.3"
+ make-fetch-happen "^4.0.1"
+ minimatch "^3.0.4"
+ minipass "^2.3.3"
+ mississippi "^3.0.0"
+ mkdirp "^0.5.1"
+ normalize-package-data "^2.4.0"
+ npm-package-arg "^6.1.0"
+ npm-packlist "^1.1.10"
+ npm-pick-manifest "^2.1.0"
+ osenv "^0.1.5"
+ promise-inflight "^1.0.1"
+ promise-retry "^1.1.1"
+ protoduck "^5.0.0"
+ rimraf "^2.6.2"
+ safe-buffer "^5.1.2"
+ semver "^5.5.0"
+ ssri "^6.0.0"
+ tar "^4.4.3"
+ unique-filename "^1.1.0"
+ which "^1.3.0"
+
+parallel-transform@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"
+ integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==
+ dependencies:
+ cyclist "^1.0.1"
+ inherits "^2.0.3"
+ readable-stream "^2.1.5"
+
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+ integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
+
path-key@^2.0.0, path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+ integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+
pirates@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+prepend-http@^1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
+ integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
+
preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
fast-diff "^1.1.2"
prettier@^2.0.4:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4"
- integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6"
+ integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw==
pretty-format@^20.0.3:
version "20.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+promise-inflight@^1.0.1, promise-inflight@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
+ integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+
+promise-retry@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d"
+ integrity sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=
+ dependencies:
+ err-code "^1.0.0"
+ retry "^0.10.0"
+
prompts@^2.0.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.2.tgz#480572d89ecf39566d2bd3fe2c9fccb7c4c0b068"
kleur "^3.0.3"
sisteransi "^1.0.4"
+promzard@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee"
+ integrity sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=
+ dependencies:
+ read "1"
+
prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
object-assign "^4.1.1"
react-is "^16.8.1"
+proto-list@~1.2.1:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
+ integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
+
+protoduck@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/protoduck/-/protoduck-5.0.1.tgz#03c3659ca18007b69a50fd82a7ebcc516261151f"
+ integrity sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==
+ dependencies:
+ genfun "^5.0.0"
+
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
forwarded "~0.1.2"
ipaddr.js "1.9.1"
+prr@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+ integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+ integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
+
psl@^1.1.28:
version "1.8.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
+pump@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954"
+ integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+pump@^2.0.0, pump@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
+ integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
end-of-stream "^1.1.0"
once "^1.3.1"
+pumpify@^1.3.3:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
+ integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==
+ dependencies:
+ duplexify "^3.6.0"
+ inherits "^2.0.3"
+ pump "^2.0.0"
+
punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+qrcode-terminal@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819"
+ integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==
+
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+query-string@^6.1.0:
+ version "6.13.1"
+ resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.1.tgz#d913ccfce3b4b3a713989fe6d39466d92e71ccad"
+ integrity sha512-RfoButmcK+yCta1+FuU8REvisx1oEzhMKwhLUNcepQTPGcNMp1sIqjnfCtfnvGSQZQEhaBHvccujtWoUV3TTbA==
+ dependencies:
+ decode-uri-component "^0.2.0"
+ split-on-first "^1.0.0"
+ strict-uri-encode "^2.0.0"
+
+qw@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4"
+ integrity sha1-77/cdA+a0FQwRCassYNBLMi5ltQ=
+
raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
iconv-lite "0.4.24"
unpipe "1.0.0"
+rc@^1.0.1, rc@^1.1.6:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+ integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+ dependencies:
+ deep-extend "^0.6.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
react-is@^16.12.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+read-cmd-shim@^1.0.1, read-cmd-shim@~1.0.1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16"
+ integrity sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA==
+ dependencies:
+ graceful-fs "^4.1.2"
+
+read-installed@~4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/read-installed/-/read-installed-4.0.3.tgz#ff9b8b67f187d1e4c29b9feb31f6b223acd19067"
+ integrity sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc=
+ dependencies:
+ debuglog "^1.0.1"
+ read-package-json "^2.0.0"
+ readdir-scoped-modules "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ slide "~1.1.3"
+ util-extend "^1.0.1"
+ optionalDependencies:
+ graceful-fs "^4.1.2"
+
+"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@^2.0.13:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.1.2.tgz#6992b2b66c7177259feb8eaac73c3acd28b9222a"
+ integrity sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==
+ dependencies:
+ glob "^7.1.1"
+ json-parse-even-better-errors "^2.3.0"
+ normalize-package-data "^2.0.0"
+ npm-normalize-package-bin "^1.0.0"
+
+read-package-tree@^5.2.1:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636"
+ integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==
+ dependencies:
+ read-package-json "^2.0.0"
+ readdir-scoped-modules "^1.0.0"
+ util-promisify "^2.1.0"
+
read-pkg-up@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
parse-json "^5.0.0"
type-fest "^0.6.0"
-readable-stream@^2.0.2:
+read@1, read@~1.0.1, read@~1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
+ integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=
+ dependencies:
+ mute-stream "~0.0.4"
+
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.6, readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
+readable-stream@~1.1.10:
+ version "1.1.14"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
+ integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "0.0.1"
+ string_decoder "~0.10.x"
+
+readdir-scoped-modules@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309"
+ integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==
+ dependencies:
+ debuglog "^1.0.1"
+ dezalgo "^1.0.0"
+ graceful-fs "^4.1.2"
+ once "^1.3.0"
+
readdirp@^2.0.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
app-root-path "^1.3.0"
mkdirp "^0.5.1"
+reconnecting-websocket@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
+ integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
+
+redux@^4.0.4:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
+ integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
+ dependencies:
+ loose-envify "^1.4.0"
+ symbol-observable "^1.2.0"
+
regenerate-unicode-properties@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.21.tgz#55e2246b7f7d36f1b461490942fa780299c400d7"
integrity sha512-kUUXjX4AnqnR8KRTCrayAo9PzYMRKmVoGgaz2tBuz0MF3g1ZbGebmtW0yFHfFK9CmBjQKeYIgoL22pFLBJY7sw==
-regexp.prototype.flags@^1.3.0:
+regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
unicode-match-property-ecmascript "^1.0.4"
unicode-match-property-value-ecmascript "^1.2.0"
+registry-auth-token@^3.0.1:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e"
+ integrity sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==
+ dependencies:
+ rc "^1.1.6"
+ safe-buffer "^5.0.1"
+
+registry-url@^3.0.3:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942"
+ integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI=
+ dependencies:
+ rc "^1.0.1"
+
regjsgen@^0.5.1:
version "0.5.2"
resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733"
stealthy-require "^1.1.1"
tough-cookie "^2.3.3"
-request@^2.79.0, request@^2.88.2:
+request@^2.74.0, request@^2.79.0, request@^2.85.0, request@^2.87.0, request@^2.88.2:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+ integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
+
require-main-filename@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
-rimraf@2.6.3:
+retry@^0.10.0:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
+ integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=
+
+retry@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
+ integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
+
+rimraf@2, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+ integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+ dependencies:
+ glob "^7.1.3"
+
+rimraf@2.6.3, rimraf@~2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
+run-queue@^1.0.0, run-queue@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
+ integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=
+ dependencies:
+ aproba "^1.1.1"
+
rx-lite-aggregates@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=
-rxjs@^6.6.2:
+rxjs@^6.5.5, rxjs@^6.6.2:
version "6.6.2"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2"
integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
dependencies:
regexp-tree "~0.1.1"
-"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
+semver-diff@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
+ integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=
+ dependencies:
+ semver "^5.0.3"
+
semver-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338"
integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==
-"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0:
+"semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
+semver@~5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+ integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8=
+
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
parseurl "~1.3.3"
send "0.17.1"
-set-blocking@^2.0.0:
+set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
+sha@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/sha/-/sha-2.0.1.tgz#6030822fbd2c9823949f8f72ed6411ee5cf25aae"
+ integrity sha1-YDCCL70smCOUn49y7WQR7lzyWq4=
+ dependencies:
+ graceful-fs "^4.1.2"
+ readable-stream "^2.0.2"
+
+shallowequal@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
+ integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
+
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
integrity sha1-WbJo7sveWQOLMNogK8+93rLEpOs=
side-channel@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947"
- integrity sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
+ integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==
dependencies:
- es-abstract "^1.17.0-next.1"
- object-inspect "^1.7.0"
+ es-abstract "^1.18.0-next.0"
+ object-inspect "^1.8.0"
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3"
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
+slide@^1.1.3, slide@^1.1.6, slide@~1.1.3, slide@~1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
+ integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=
+
+smart-buffer@^1.0.13:
+ version "1.1.15"
+ resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16"
+ integrity sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY=
+
+smart-buffer@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba"
+ integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==
+
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
source-map-resolve "^0.5.0"
use "^3.1.0"
+socks-proxy-agent@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-3.0.1.tgz#2eae7cf8e2a82d34565761539a7f9718c5617659"
+ integrity sha512-ZwEDymm204mTzvdqyUqOdovVr2YRd2NYskrYrF2LXyZ9qDiMAoFESGK8CRphiO7rtbo2Y757k2Nia3x2hGtalA==
+ dependencies:
+ agent-base "^4.1.0"
+ socks "^1.1.10"
+
+socks-proxy-agent@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386"
+ integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==
+ dependencies:
+ agent-base "~4.2.1"
+ socks "~2.3.2"
+
+socks@^1.1.10:
+ version "1.1.10"
+ resolved "https://registry.yarnpkg.com/socks/-/socks-1.1.10.tgz#5b8b7fc7c8f341c53ed056e929b7bf4de8ba7b5a"
+ integrity sha1-W4t/x8jzQcU+0FbpKbe/Tei6e1o=
+ dependencies:
+ ip "^1.1.4"
+ smart-buffer "^1.0.13"
+
+socks@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.3.tgz#01129f0a5d534d2b897712ed8aceab7ee65d78e3"
+ integrity sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==
+ dependencies:
+ ip "1.1.5"
+ smart-buffer "^4.1.0"
+
+sorted-object@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/sorted-object/-/sorted-object-2.0.1.tgz#7d631f4bd3a798a24af1dffcfbfe83337a5df5fc"
+ integrity sha1-fWMfS9OnmKJK8d/8+/6DM3pd9fw=
+
+sorted-union-stream@~2.1.3:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/sorted-union-stream/-/sorted-union-stream-2.1.3.tgz#c7794c7e077880052ff71a8d4a2dbb4a9a638ac7"
+ integrity sha1-x3lMfgd4gAUv9xqNSi27Sppjisc=
+ dependencies:
+ from2 "^1.3.0"
+ stream-iterate "^1.1.0"
+
sortpack@^2.1.4:
version "2.1.7"
resolved "https://registry.yarnpkg.com/sortpack/-/sortpack-2.1.7.tgz#240837033b00e0c671048d98bbd0d710d979bb2e"
source-map-url "^0.4.0"
urix "^0.1.0"
-source-map-support@^0.5.17, source-map-support@^0.5.6:
+source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.12:
version "0.5.19"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
+split-on-first@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
+ integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
+
split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
+ssri@^5.0.0, ssri@^5.2.4, ssri@^5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06"
+ integrity sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==
+ dependencies:
+ safe-buffer "^5.1.1"
+
+ssri@^6.0.0, ssri@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
+ integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
+ dependencies:
+ figgy-pudding "^3.5.1"
+
stack-utils@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.2.tgz#5cf48b4557becb4638d0bc4f21d23f5d19586593"
inherits "~2.0.1"
readable-stream "^2.0.2"
+stream-each@^1.1.0:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
+ integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==
+ dependencies:
+ end-of-stream "^1.1.0"
+ stream-shift "^1.0.0"
+
+stream-iterate@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/stream-iterate/-/stream-iterate-1.2.0.tgz#2bd7c77296c1702a46488b8ad41f79865eecd4e1"
+ integrity sha1-K9fHcpbBcCpGSIuK1B95hl7s1OE=
+ dependencies:
+ readable-stream "^2.1.5"
+ stream-shift "^1.0.0"
+
+stream-shift@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
+ integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
+
+strict-uri-encode@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
+ integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
+
string-argv@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
char-regex "^1.0.2"
strip-ansi "^6.0.0"
-string-width@^2.1.0:
+string-width@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
-string-width@^3.0.0:
+string-width@^3.0.0, string-width@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
dependencies:
safe-buffer "~5.2.0"
+string_decoder@~0.10.x:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+ integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
+
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
is-obj "^1.0.1"
is-regexp "^1.0.0"
-strip-ansi@^4.0.0:
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0, strip-ansi@~4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
dependencies:
ansi-regex "^3.0.0"
-strip-ansi@^5.1.0:
+strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+ integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
+
supports-color@^5.3.0, supports-color@^5.4.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
dependencies:
has-flag "^3.0.0"
-supports-color@^7.0.0, supports-color@^7.1.0:
+supports-color@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
dependencies:
has-flag "^4.0.0"
+supports-color@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+ integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+ dependencies:
+ has-flag "^4.0.0"
+
supports-hyperlinks@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47"
has-flag "^4.0.0"
supports-color "^7.0.0"
+symbol-observable@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
+ integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
+
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
slice-ansi "^2.1.0"
string-width "^3.0.0"
+tar@^2.0.0:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40"
+ integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==
+ dependencies:
+ block-stream "*"
+ fstream "^1.0.12"
+ inherits "2"
+
+tar@^4.4.0, tar@^4.4.2, tar@^4.4.3, tar@^4.4.8:
+ version "4.4.13"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
+ integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
+ dependencies:
+ chownr "^1.1.1"
+ fs-minipass "^1.2.5"
+ minipass "^2.8.6"
+ minizlib "^1.2.1"
+ mkdirp "^0.5.0"
+ safe-buffer "^5.1.2"
+ yallist "^3.0.3"
+
+term-size@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
+ integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=
+ dependencies:
+ execa "^0.7.0"
+
terminal-link@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
ansi-escapes "^4.2.1"
supports-hyperlinks "^2.0.0"
+terser@^4.6.11:
+ version "4.8.0"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17"
+ integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==
+ dependencies:
+ commander "^2.20.0"
+ source-map "~0.6.1"
+ source-map-support "~0.5.12"
+
test-exclude@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
glob "^7.1.4"
minimatch "^3.0.4"
-text-table@^0.2.0:
+text-table@^0.2.0, text-table@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
-through@^2.3.6, through@^2.3.8:
+through2@^2.0.0:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
+ integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
+ dependencies:
+ readable-stream "~2.3.6"
+ xtend "~4.0.1"
+
+"through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+timed-out@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
+ integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=
+
tiny-invariant@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
+tiny-relative-date@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07"
+ integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A==
+
tiny-warning@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+tippy.js@^6.1.1:
+ version "6.2.6"
+ resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.2.6.tgz#4991bbe8f75e741fb92b5ccfeebcd072d71f8345"
+ integrity sha512-0tTL3WQNT0nWmpslhDryRahoBm6PT9fh1xXyDfOsvZpDzq52by2rF2nvsW0WX2j9nUZP/jSGDqfKJGjCtoGFKg==
+ dependencies:
+ "@popperjs/core" "^2.4.4"
+
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
regex-not "^1.0.2"
safe-regex "^1.1.0"
+toastify-js@^1.7.0:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/toastify-js/-/toastify-js-1.9.1.tgz#95bd5bc03a6144ea5e736a2e39fb0730195818b3"
+ integrity sha512-B3LTJURySMix/xWqVHyj2XGVsIHesb4euGVuIaFfKxfmjM4F6HMgbW9V66DHUEt98jGlGpeTWSiSJ78UfrJVbA==
+
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
dependencies:
punycode "^2.1.1"
+tributejs@^5.1.3:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-5.1.3.tgz#980600fc72865be5868893078b4bfde721129eae"
+ integrity sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==
+
ts-node@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.0.0.tgz#e7699d2a110cc8c0d3b831715e417688683460b3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
-tslint-react-recommended@^1.0.15:
- version "1.0.15"
- resolved "https://registry.yarnpkg.com/tslint-react-recommended/-/tslint-react-recommended-1.0.15.tgz#4166dc7d87b57280110673c99315a35ac5a76a7e"
- integrity sha1-QWbcfYe1coARBnPJkxWjWsWnan4=
-
tsutils@^3.17.1:
version "3.17.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
dependencies:
is-typedarray "^1.0.0"
+typedarray@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+ integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+
typescript@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2"
integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==
+uc.micro@^1.0.1, uc.micro@^1.0.5:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
+ integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
+
+uid-number@0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+ integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=
+
ultron@1.0.x:
version "1.0.2"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
integrity sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=
+umask@^1.1.0, umask@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
+ integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=
+
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
is-extendable "^0.1.1"
set-value "^2.0.1"
+unique-filename@^1.1.0, unique-filename@^1.1.1, unique-filename@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
+ integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
+ dependencies:
+ unique-slug "^2.0.0"
+
+unique-slug@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
+ integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
+ dependencies:
+ imurmurhash "^0.1.4"
+
+unique-string@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a"
+ integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=
+ dependencies:
+ crypto-random-string "^1.0.0"
+
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
has-value "^0.3.1"
isobject "^3.0.0"
+unzip-response@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
+ integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=
+
+update-notifier@^2.2.0, update-notifier@^2.3.0, update-notifier@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6"
+ integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==
+ dependencies:
+ boxen "^1.2.1"
+ chalk "^2.0.1"
+ configstore "^3.0.0"
+ import-lazy "^2.1.0"
+ is-ci "^1.0.10"
+ is-installed-globally "^0.1.0"
+ is-npm "^1.0.0"
+ latest-version "^3.0.0"
+ semver-diff "^2.0.0"
+ xdg-basedir "^3.0.0"
+
uri-js@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+url-parse-lax@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73"
+ integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=
+ dependencies:
+ prepend-http "^1.0.1"
+
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+util-extend@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f"
+ integrity sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=
+
+util-promisify@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/util-promisify/-/util-promisify-2.1.0.tgz#3c2236476c4d32c5ff3c47002add7c13b9a82a53"
+ integrity sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=
+ dependencies:
+ object.getownpropertydescriptors "^2.0.3"
+
utils-extend@^1.0.4, utils-extend@^1.0.6, utils-extend@^1.0.7:
version "1.0.8"
resolved "https://registry.yarnpkg.com/utils-extend/-/utils-extend-1.0.8.tgz#ccfd7b64540f8e90ee21eec57769d0651cab8a5f"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
-uuid@^3.3.2:
+uuid@^3.2.1, uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
convert-source-map "^1.6.0"
source-map "^0.7.3"
-validate-npm-package-license@^3.0.1:
+validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
+validate-npm-package-name@^3.0.0, validate-npm-package-name@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e"
+ integrity sha1-X6kS2B630MdK/BQN5zF/DKffQ34=
+ dependencies:
+ builtins "^1.0.3"
+
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
core-util-is "1.0.2"
extsprintf "^1.2.0"
+void-elements@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+ integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
+
w3c-hr-time@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
exec-sh "^0.2.0"
minimist "^1.2.0"
+wcwidth@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
+ integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
+ dependencies:
+ defaults "^1.0.3"
+
webidl-conversions@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
-which@^1.2.9:
+which@1, which@^1.2.9, which@^1.3.0, which@^1.3.1, which@~1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
dependencies:
isexe "^2.0.0"
+wide-align@^1.1.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
+ integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
+ dependencies:
+ string-width "^1.0.2 || 2"
+
+widest-line@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc"
+ integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==
+ dependencies:
+ string-width "^2.1.1"
+
word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+worker-farm@^1.6.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
+ integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==
+ dependencies:
+ errno "~0.1.7"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrap-ansi@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
+ integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
+ dependencies:
+ ansi-styles "^3.2.0"
+ string-width "^3.0.0"
+ strip-ansi "^5.0.0"
+
wrap-ansi@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
string-width "^4.1.0"
strip-ansi "^6.0.0"
-wrappy@1:
+wrappy@1, wrappy@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+write-file-atomic@^2.0.0, write-file-atomic@^2.3.0:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481"
+ integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ signal-exit "^3.0.2"
+
write-file-atomic@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
options ">=0.0.5"
ultron "1.0.x"
-ws@^7.2.3:
+ws@^7.2.3, ws@^7.3.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
+xdg-basedir@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
+ integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=
+
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+xtend@~4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+ integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
+y18n@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+ integrity sha1-bRX7qITAhnnA136I53WegR4H+kE=
+
y18n@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
-yaml@^1.7.2:
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+ integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
+
+yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+ integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
+yaml@^1.10.0, yaml@^1.7.2:
version "1.10.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
+yargs-parser@^15.0.1:
+ version "15.0.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.1.tgz#54786af40b820dcb2fb8025b11b4d659d76323b3"
+ integrity sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
camelcase "^5.0.0"
decamelize "^1.2.0"
+yargs-parser@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
+ integrity sha1-jQrELxbqVd69MyyvTEA4s+P139k=
+ dependencies:
+ camelcase "^4.1.0"
+
+yargs@^14.2.3:
+ version "14.2.3"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414"
+ integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==
+ dependencies:
+ cliui "^5.0.0"
+ decamelize "^1.2.0"
+ find-up "^3.0.0"
+ get-caller-file "^2.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^2.0.0"
+ set-blocking "^2.0.0"
+ string-width "^3.0.0"
+ which-module "^2.0.0"
+ y18n "^4.0.0"
+ yargs-parser "^15.0.1"
+
yargs@^15.3.1:
version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
y18n "^4.0.0"
yargs-parser "^18.1.2"
+yargs@^8.0.2:
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
+ integrity sha1-YpmpBVsc78lp/355wdkY3Osiw2A=
+ dependencies:
+ camelcase "^4.1.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^2.0.0"
+ read-pkg-up "^2.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^7.0.0"
+
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"