diff --git a/.drone.yml b/.drone.yml index 79b2835..981c632 100644 --- a/.drone.yml +++ b/.drone.yml @@ -6,16 +6,40 @@ steps: - name: build image: node:current-alpine3.12 commands: + - apk update + - apk add git - npm install -D - npm run rebuild - name: package image: fuww/alpine-zip + when: + event: + - push commands: - SHORTREV=`echo $DRONE_COMMIT | cut -b 1-8` - - echo FILENAME=`date +%Y%m%d%H%m`-$SHORTREV.zip >> environment + - echo NAME=`date +%Y%m%d%H%m`-$SHORTREV >> environment - source environment - - zip -r $FILENAME build + - zip -r $NAME.zip build + + - name: dev.carbon.chat + image: drillster/drone-rsync + when: + event: + - push + settings: + hosts: + from_secret: SSH_HOST + port: + from_secret: SSH_PORT + user: + from_secret: SSH_USERNAME + key: + from_secret: SSH_KEY + source: ./build/* + target: ${DRONE_COMMIT_SHA:0:8} + recursive: true + - name: b2 image: tianon/backblaze-b2:2 @@ -28,8 +52,9 @@ steps: from_secret: b2_account_id KEY: from_secret: b2_application_key + COMMIT: ${DRONE_COMMIT_SHA:0:8} commands: - source environment - b2 authorize-account $ACCOUNT $KEY - - b2 upload-file $BUCKET $FILENAME $FILENAME - - echo Build artifacts avaliable at `b2 make-friendly-url $BUCKET $FILENAME` + - b2 upload-file $BUCKET $NAME.zip $NAME.zip + - echo Build artifacts avaliable at `b2 make-friendly-url $BUCKET $NAME.zip` and at https://dev.carbon.chat/$COMMIT diff --git a/.gitignore b/.gitignore index 5cac20b..1d83ade 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,297 @@ -# Generated files -node_modules -build +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig -# Editor artifacts -.vscode +# Created by https://www.toptal.com/developers/gitignore/api/node,vscode,webstorm,webstorm+all +# Edit at https://www.toptal.com/developers/gitignore?templates=node,vscode,webstorm,webstorm+all + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env*.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +### vscode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +### WebStorm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### WebStorm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +### WebStorm+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### WebStorm+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +# End of https://www.toptal.com/developers/gitignore/api/node,vscode,webstorm,webstorm+all + +# Emacs *~ \#*# -.#* -._* + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) + +/build/ diff --git a/README.md b/README.md index 07ca920..77dabec 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,35 @@ Carbon is the Matrix client for Discord and Guilded refugees. +Visit the hosted instance on +[https://carbon.chat](https://carbon.chat). + +## Status + +Carbon is **abandoned** by its author, but it is still solid code to build on for anyone with the time and inclination to pick it up. + +## Report bugs and suggest features + +Please briefly check this README and the issues page first to make +sure that the issue/feature is not already known! + +- If you already have an account on Gitdab, use the issues page. +- If you don't have an account, and don't wish to create one, you can +send an email to the [mailing list]. + +If something in the interface isn't working as you think it should, +please provide a screenshot of any messages from the browser devtools +console. If using the mailing list, attachments aren't supported, so +you'll have to upload to some image host and post the link. + +[mailing list]: https://lists.sr.ht/~cadence/carbon-discuss + ## The dream Carbon's planned features, compared to Discord and Guilded: - End to end encryption -- Free of charge per-account custom emojis and custom emoji packs +- Free of charge, per-account, custom emojis and custom emoji packs - No limit to number of groups you can join at a time - Uses the open Matrix and Mumble systems - Much better IRC layout @@ -35,25 +58,39 @@ Carbon is currently _technically_ usable as a chat app, but is very early in development. These important features still need to be implemented: -- Login GUI -- Unreads -- Chat history -- Formatting - Emojis - Reactions +- Encryption - Groups v2 - Group management - Pinned channels - Mumble integration +For more information, see [issue +#10.](https://gitdab.com/cadence/Carbon/issues/10) + ## The code -### Building +### Downloading a CI build + +Visit [drone CI](https://drone.badat.dev/cadence/Carbon/branches), +select the branch you want to use, select `b2` on the left, scroll +down, and open the URL on the last line to download the build. + +### Building from source yourself + +Dependencies: + +- git +- node +- npm (bundled with node) + +Build: npm install -D npm run rebuild -### Hosting +### Hosting a build Send the files from the `build` folder to a static file server. Apply a long cache-control header to everything served under `/static`, and @@ -64,3 +101,8 @@ no cache-control header to everything else. npm run watch Files will be rebuilt as you save them. + +Use `python3 -m http.server -d build` to serve the build on +[http://localhost:8000](http://localhost:8000). + +(Avoid `npx http-server`, it caches too much stuff.) diff --git a/build.js b/build.js index 4932101..964af20 100644 --- a/build.js +++ b/build.js @@ -1,6 +1,6 @@ const pug = require("pug") const sass = require("sass") -const fs = require("fs").promises +const fs = require("fs") const os = require("os") const crypto = require("crypto") const path = require("path") @@ -9,6 +9,8 @@ const babel = require("@babel/core") const fetch = require("node-fetch") const chalk = require("chalk") const hint = require("jshint").JSHINT +const browserify = require("browserify") +const {Transform} = require("stream") process.chdir(pj(__dirname, "src")) @@ -16,10 +18,10 @@ const buildDir = "../build" const validationQueue = [] const validationHost = os.hostname() === "future" ? "http://localhost:8888/" : "http://validator.w3.org/nu/" -const static = new Map() +const staticFiles = new Map() const links = new Map() const sources = new Map() -const pugLocals = {static, links} +const pugLocals = {static: staticFiles, links} const spec = require("./spec.js") @@ -94,6 +96,7 @@ function runHint(filename, source) { globals: ["console", "URLSearchParams", "staticFiles"], browser: true, asi: true, + node: true }) const result = hint.data() let problems = 0 @@ -125,26 +128,54 @@ function runHint(filename, source) { } async function addFile(sourcePath, targetPath) { - const contents = await fs.readFile(pj(".", sourcePath), {encoding: null}) - static.set(sourcePath, `${targetPath}?static=${hash(contents)}`) - fs.writeFile(pj(buildDir, targetPath), contents) + const contents = await fs.promises.readFile(pj(".", sourcePath), {encoding: null}); + staticFiles.set(sourcePath, `${targetPath}?static=${hash(contents)}`) + await fs.promises.writeFile(pj(buildDir, targetPath), contents) } async function loadJS(sourcePath, targetPath) { - let content = await fs.readFile(pj(".", sourcePath), {encoding: "utf8"}) + let content = await fs.promises.readFile(pj(".", sourcePath), {encoding: "utf8"}) sources.set(sourcePath, content) - static.set(sourcePath, `${targetPath}?static=${hash(content)}`) + staticFiles.set(sourcePath, `${targetPath}?static=${hash(content)}`) } async function addJS(sourcePath, targetPath) { let content = sources.get(sourcePath) - // resolve imports to hashed paths - content = content.replace(/\$to_relative "([^"]+)"/g, function(_, file) { - if (!static.get(file)) throw new Error(`Tried to relative import ${file} from ${sourcePath}, but import not found`) - return '"' + getRelative(targetPath, static.get(file)) + '"' - }) runHint(sourcePath, content) - fs.writeFile(pj(buildDir, targetPath), content) + await fs.promises.writeFile(pj(buildDir, targetPath), content) +} + +async function addBundle(sourcePath, targetPath, module = false) { + let opts = {} + if (module) opts.standalone = sourcePath + const content = await new Promise(resolve => { + browserify([], opts) + .add(pj(".", sourcePath)) + .transform(file => { + let content = "" + const transform = new Transform({ + transform(chunk, encoding, callback) { + content += chunk.toString() + callback(null, chunk) + } + }) + transform.on("finish", () => { + const relativePath = path.relative(process.cwd(), file).replace(/^\/*/, "/") + runHint(relativePath, content) + }) + return transform + }) + .bundle((err, res) => { + if (err) { + delete err.stream + throw err // Quit; problem parsing file to bundle + } + resolve(res) + }) + }) + const writer = fs.promises.writeFile(pj(buildDir, targetPath), content) + staticFiles.set(sourcePath, `${targetPath}?static=${hash(content)}`) + await writer } async function addSass(sourcePath, targetPath) { @@ -158,7 +189,7 @@ async function addSass(sourcePath, targetPath) { if (!(name instanceof sass.types.String)) { throw "$name: expected a string" } - const result = getRelative(targetPath, static.get(name.getValue())) + const result = getRelative(targetPath, staticFiles.get(name.getValue())) if (typeof result === "string") { return new sass.types.String(result) } else { @@ -166,10 +197,10 @@ async function addSass(sourcePath, targetPath) { } } } - }).css - static.set(sourcePath, `${targetPath}?static=${hash(renderedCSS)}`) + }).css; + staticFiles.set(sourcePath, `${targetPath}?static=${hash(renderedCSS)}`) validate(sourcePath, renderedCSS, "css") - await fs.writeFile(pj(buildDir, targetPath), renderedCSS) + await fs.promises.writeFile(pj(buildDir, targetPath), renderedCSS) } async function addPug(sourcePath, targetPath) { @@ -177,10 +208,10 @@ async function addPug(sourcePath, targetPath) { return getRelative(targetPath, staticTarget) } function getStatic(target) { - return getRelativeHere(static.get(target)) + return getRelativeHere(staticFiles.get(target)) } function getStaticName(target) { - return getRelativeHere(static.get(target)).replace(/\?.*$/, "") + return getRelativeHere(staticFiles.get(target)).replace(/\?.*$/, "") } function getLink(target) { return getRelativeHere(links.get(target)) @@ -188,11 +219,11 @@ async function addPug(sourcePath, targetPath) { const renderedHTML = pug.compileFile(pj(".", sourcePath), {pretty: true})({getStatic, getStaticName, getLink, ...pugLocals}) let renderedWithoutPHP = renderedHTML.replace(/<\?(?:php|=).*?\?>/gsm, "") validate(sourcePath, renderedWithoutPHP, "html") - await fs.writeFile(pj(buildDir, targetPath), renderedHTML) + await fs.promises.writeFile(pj(buildDir, targetPath), renderedHTML) } async function addBabel(sourcePath, targetPath) { - const originalCode = await fs.readFile(pj(".", sourcePath), "utf8") + const originalCode = await fs.promises.readFile(pj(".", sourcePath), "utf8") const compiled = babel.transformSync(originalCode, { sourceMaps: false, @@ -213,14 +244,14 @@ async function addBabel(sourcePath, targetPath) { } }) - const filenameWithQuery = `${targetPath}?static=${hash(compiled.code)}` + const filenameWithQuery = `${targetPath}?static=${hash(compiled.code)}`; - static.set(sourcePath, filenameWithQuery) + staticFiles.set(sourcePath, filenameWithQuery) await Promise.all([ - fs.writeFile(pj(buildDir, targetPath), originalCode), - fs.writeFile(pj(buildDir, minFilename), compiled.code), - fs.writeFile(pj(buildDir, mapFilename), JSON.stringify(compiled.map)) + fs.promises.writeFile(pj(buildDir, targetPath), originalCode), + fs.promises.writeFile(pj(buildDir, minFilename), compiled.code), + fs.promises.writeFile(pj(buildDir, mapFilename), JSON.stringify(compiled.map)) ]) } @@ -241,7 +272,7 @@ async function addBabel(sourcePath, targetPath) { // Stage 3: Create dirs const dirs = [...new Set(spec.map(item => path.dirname(item.target))).values()] - await Promise.all(dirs.map(d => fs.mkdir(pj(buildDir, d), {recursive: true}))) + await Promise.all(dirs.map(d => fs.promises.mkdir(pj(buildDir, d), {recursive: true}))) // Stage 4: Build for (const item of spec) { @@ -255,6 +286,11 @@ async function addBabel(sourcePath, targetPath) { await addBabel(item.source, item.target) } else if (item.type === "pug") { await addPug(item.source, item.target) + } else if (item.type === "bundle") { + await addBundle(item.source, item.target) + } else if (item.type === "module") { + // Creates a standalone bundle that can be imported on runtime + await addBundle(item.source, item.target, true) } else { throw new Error("Unknown item type: "+item.type) } diff --git a/package-lock.json b/package-lock.json index e5806d8..97b56a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "cosc212-assignment-1", + "name": "carbon", "version": "1.0.0", "lockfileVersion": 1, "requires": true, @@ -8,7 +8,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, "requires": { "@babel/highlight": "^7.10.4" } @@ -17,7 +16,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz", "integrity": "sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ==", - "dev": true, "requires": { "browserslist": "^4.12.0", "invariant": "^2.2.4", @@ -28,7 +26,6 @@ "version": "7.11.1", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.1.tgz", "integrity": "sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ==", - "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.11.0", @@ -51,14 +48,12 @@ "@babel/parser": { "version": "7.11.2", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.2.tgz", - "integrity": "sha512-Vuj/+7vLo6l1Vi7uuO+1ngCDNeVmNbTngcJFKCR/oEtz8tKz0CJxZEGmPt9KcIloZhOZ3Zit6xbpXT2MDlS9Vw==", - "dev": true + "integrity": "sha512-Vuj/+7vLo6l1Vi7uuO+1ngCDNeVmNbTngcJFKCR/oEtz8tKz0CJxZEGmPt9KcIloZhOZ3Zit6xbpXT2MDlS9Vw==" }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, "requires": { "ms": "^2.1.1" } @@ -69,7 +64,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz", "integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==", - "dev": true, "requires": { "@babel/types": "^7.11.0", "jsesc": "^2.5.1", @@ -80,7 +74,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", - "dev": true, "requires": { "@babel/types": "^7.10.4" } @@ -89,7 +82,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", - "dev": true, "requires": { "@babel/helper-explode-assignable-expression": "^7.10.4", "@babel/types": "^7.10.4" @@ -99,7 +91,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz", "integrity": "sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ==", - "dev": true, "requires": { "@babel/compat-data": "^7.10.4", "browserslist": "^4.12.0", @@ -112,7 +103,6 @@ "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz", "integrity": "sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==", - "dev": true, "requires": { "@babel/helper-function-name": "^7.10.4", "@babel/helper-member-expression-to-functions": "^7.10.5", @@ -126,7 +116,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz", "integrity": "sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==", - "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-regex": "^7.10.4", @@ -137,7 +126,6 @@ "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", - "dev": true, "requires": { "@babel/helper-function-name": "^7.10.4", "@babel/types": "^7.10.5", @@ -148,7 +136,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz", "integrity": "sha512-4K71RyRQNPRrR85sr5QY4X3VwG4wtVoXZB9+L3r1Gp38DhELyHCtovqydRi7c1Ovb17eRGiQ/FD5s8JdU0Uy5A==", - "dev": true, "requires": { "@babel/traverse": "^7.10.4", "@babel/types": "^7.10.4" @@ -158,7 +145,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", - "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", @@ -169,7 +155,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", - "dev": true, "requires": { "@babel/types": "^7.10.4" } @@ -178,7 +163,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", - "dev": true, "requires": { "@babel/types": "^7.10.4" } @@ -187,7 +171,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", - "dev": true, "requires": { "@babel/types": "^7.11.0" } @@ -196,7 +179,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", - "dev": true, "requires": { "@babel/types": "^7.10.4" } @@ -205,7 +187,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", - "dev": true, "requires": { "@babel/helper-module-imports": "^7.10.4", "@babel/helper-replace-supers": "^7.10.4", @@ -220,7 +201,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", - "dev": true, "requires": { "@babel/types": "^7.10.4" } @@ -228,14 +208,12 @@ "@babel/helper-plugin-utils": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "dev": true + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" }, "@babel/helper-regex": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz", "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==", - "dev": true, "requires": { "lodash": "^4.17.19" } @@ -244,7 +222,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz", "integrity": "sha512-86Lsr6NNw3qTNl+TBcF1oRZMaVzJtbWTyTko+CQL/tvNvcGYEFKbLXDPxtW0HKk3McNOk4KzY55itGWCAGK5tg==", - "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-wrap-function": "^7.10.4", @@ -257,7 +234,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", - "dev": true, "requires": { "@babel/helper-member-expression-to-functions": "^7.10.4", "@babel/helper-optimise-call-expression": "^7.10.4", @@ -269,7 +245,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", - "dev": true, "requires": { "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" @@ -279,7 +254,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz", "integrity": "sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==", - "dev": true, "requires": { "@babel/types": "^7.11.0" } @@ -288,7 +262,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", - "dev": true, "requires": { "@babel/types": "^7.11.0" } @@ -296,14 +269,12 @@ "@babel/helper-validator-identifier": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", - "dev": true + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==" }, "@babel/helper-wrap-function": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz", "integrity": "sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==", - "dev": true, "requires": { "@babel/helper-function-name": "^7.10.4", "@babel/template": "^7.10.4", @@ -315,7 +286,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", - "dev": true, "requires": { "@babel/template": "^7.10.4", "@babel/traverse": "^7.10.4", @@ -326,7 +296,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", @@ -337,7 +306,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -346,7 +314,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -357,7 +324,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -365,20 +331,17 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -388,14 +351,12 @@ "@babel/parser": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.0.tgz", - "integrity": "sha512-qvRvi4oI8xii8NllyEc4MDJjuZiNaRzyb7Y7lup1NqJV8TZHF4O27CcP+72WPn/k1zkgJ6WJfnIbk4jTsVAZHw==", - "dev": true + "integrity": "sha512-qvRvi4oI8xii8NllyEc4MDJjuZiNaRzyb7Y7lup1NqJV8TZHF4O27CcP+72WPn/k1zkgJ6WJfnIbk4jTsVAZHw==" }, "@babel/plugin-proposal-async-generator-functions": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz", "integrity": "sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-remap-async-to-generator": "^7.10.4", @@ -406,7 +367,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz", "integrity": "sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==", - "dev": true, "requires": { "@babel/helper-create-class-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" @@ -416,7 +376,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz", "integrity": "sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-dynamic-import": "^7.8.0" @@ -426,7 +385,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz", "integrity": "sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" @@ -436,7 +394,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz", "integrity": "sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.0" @@ -446,7 +403,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz", "integrity": "sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" @@ -456,7 +412,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz", "integrity": "sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" @@ -466,7 +421,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz", "integrity": "sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-numeric-separator": "^7.10.4" @@ -476,7 +430,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz", "integrity": "sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", @@ -487,7 +440,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz", "integrity": "sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" @@ -497,7 +449,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz", "integrity": "sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0", @@ -508,7 +459,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz", "integrity": "sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw==", - "dev": true, "requires": { "@babel/helper-create-class-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" @@ -518,7 +468,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz", "integrity": "sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==", - "dev": true, "requires": { "@babel/helper-create-regexp-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" @@ -528,7 +477,6 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -537,7 +485,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -546,7 +493,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -555,7 +501,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" } @@ -564,7 +509,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -573,7 +517,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -582,7 +525,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -591,7 +533,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -600,7 +541,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -609,7 +549,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -618,7 +557,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -627,7 +565,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz", "integrity": "sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -636,7 +573,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz", "integrity": "sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -645,7 +581,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz", "integrity": "sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==", - "dev": true, "requires": { "@babel/helper-module-imports": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4", @@ -656,7 +591,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz", "integrity": "sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -665,7 +599,6 @@ "version": "7.11.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz", "integrity": "sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -674,7 +607,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz", "integrity": "sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==", - "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-define-map": "^7.10.4", @@ -690,7 +622,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz", "integrity": "sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -699,7 +630,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz", "integrity": "sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -708,7 +638,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz", "integrity": "sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==", - "dev": true, "requires": { "@babel/helper-create-regexp-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" @@ -718,7 +647,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz", "integrity": "sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -727,7 +655,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz", "integrity": "sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==", - "dev": true, "requires": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" @@ -737,7 +664,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz", "integrity": "sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -746,7 +672,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz", "integrity": "sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==", - "dev": true, "requires": { "@babel/helper-function-name": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" @@ -756,7 +681,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz", "integrity": "sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -765,7 +689,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz", "integrity": "sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -774,7 +697,6 @@ "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz", "integrity": "sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==", - "dev": true, "requires": { "@babel/helper-module-transforms": "^7.10.5", "@babel/helper-plugin-utils": "^7.10.4", @@ -785,7 +707,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz", "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==", - "dev": true, "requires": { "@babel/helper-module-transforms": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4", @@ -797,7 +718,6 @@ "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz", "integrity": "sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==", - "dev": true, "requires": { "@babel/helper-hoist-variables": "^7.10.4", "@babel/helper-module-transforms": "^7.10.5", @@ -809,7 +729,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz", "integrity": "sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==", - "dev": true, "requires": { "@babel/helper-module-transforms": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" @@ -819,7 +738,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz", "integrity": "sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==", - "dev": true, "requires": { "@babel/helper-create-regexp-features-plugin": "^7.10.4" } @@ -828,7 +746,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz", "integrity": "sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -837,7 +754,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz", "integrity": "sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-replace-supers": "^7.10.4" @@ -847,7 +763,6 @@ "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz", "integrity": "sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==", - "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" @@ -857,7 +772,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz", "integrity": "sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -866,7 +780,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz", "integrity": "sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==", - "dev": true, "requires": { "regenerator-transform": "^0.14.2" } @@ -875,7 +788,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz", "integrity": "sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -884,7 +796,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz", "integrity": "sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -893,7 +804,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz", "integrity": "sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0" @@ -903,7 +813,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz", "integrity": "sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-regex": "^7.10.4" @@ -913,7 +822,6 @@ "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz", "integrity": "sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==", - "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" @@ -923,7 +831,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz", "integrity": "sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -932,7 +839,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz", "integrity": "sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -941,7 +847,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz", "integrity": "sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==", - "dev": true, "requires": { "@babel/helper-create-regexp-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" @@ -951,7 +856,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.0.tgz", "integrity": "sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg==", - "dev": true, "requires": { "@babel/compat-data": "^7.11.0", "@babel/helper-compilation-targets": "^7.10.4", @@ -1027,7 +931,6 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.3.tgz", "integrity": "sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", @@ -1040,7 +943,6 @@ "version": "7.11.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", - "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } @@ -1049,7 +951,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", - "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", @@ -1060,7 +961,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", - "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.11.0", @@ -1077,7 +977,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, "requires": { "ms": "^2.1.1" } @@ -1088,7 +987,6 @@ "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", @@ -1098,20 +996,55 @@ "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" + }, + "@types/react": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", + "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } }, "acorn": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", - "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==", - "dev": true + "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==" + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, "requires": { "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" @@ -1121,38 +1054,88 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, + "array-filter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", + "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=" + }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + } + } + } }, "assert-never": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz", - "integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==", - "dev": true + "integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==" }, "async": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, "requires": { "lodash": "^4.17.14" } }, + "available-typed-arrays": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", + "integrity": "sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==", + "requires": { + "array-filter": "^1.0.0" + } + }, "babel-plugin-dynamic-import-node": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, "requires": { "object.assign": "^4.1.0" } @@ -1161,7 +1144,6 @@ "version": "3.0.0-canary-5", "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", - "dev": true, "requires": { "@babel/types": "^7.9.6" } @@ -1169,26 +1151,32 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "basic-auth": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", - "integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=", - "dev": true + "integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=" }, "binary-extensions": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", - "dev": true + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" + }, + "bn.js": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", + "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==" }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1198,16 +1186,240 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "requires": { "fill-range": "^7.0.1" } }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browser-pack": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", + "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", + "requires": { + "JSONStream": "^1.0.3", + "combine-source-map": "~0.8.0", + "defined": "^1.0.0", + "safe-buffer": "^5.1.1", + "through2": "^2.0.0", + "umd": "^3.0.0" + } + }, + "browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "requires": { + "resolve": "^1.17.0" + } + }, + "browserify": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-17.0.0.tgz", + "integrity": "sha512-SaHqzhku9v/j6XsQMRxPyBrSP3gnwmE27gLJYZgMT2GeK3J0+0toN+MnuNYDfHwVGQfLiMZ7KSNSIXHemy905w==", + "requires": { + "JSONStream": "^1.0.3", + "assert": "^1.4.0", + "browser-pack": "^6.0.1", + "browser-resolve": "^2.0.0", + "browserify-zlib": "~0.2.0", + "buffer": "~5.2.1", + "cached-path-relative": "^1.0.0", + "concat-stream": "^1.6.0", + "console-browserify": "^1.1.0", + "constants-browserify": "~1.0.0", + "crypto-browserify": "^3.0.0", + "defined": "^1.0.0", + "deps-sort": "^2.0.1", + "domain-browser": "^1.2.0", + "duplexer2": "~0.1.2", + "events": "^3.0.0", + "glob": "^7.1.0", + "has": "^1.0.0", + "htmlescape": "^1.1.0", + "https-browserify": "^1.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "^7.2.1", + "labeled-stream-splicer": "^2.0.0", + "mkdirp-classic": "^0.5.2", + "module-deps": "^6.2.3", + "os-browserify": "~0.3.0", + "parents": "^1.0.1", + "path-browserify": "^1.0.0", + "process": "~0.11.0", + "punycode": "^1.3.2", + "querystring-es3": "~0.2.0", + "read-only-stream": "^2.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.4", + "shasum-object": "^1.0.0", + "shell-quote": "^1.6.1", + "stream-browserify": "^3.0.0", + "stream-http": "^3.0.0", + "string_decoder": "^1.1.1", + "subarg": "^1.0.0", + "syntax-error": "^1.1.1", + "through2": "^2.0.0", + "timers-browserify": "^1.0.1", + "tty-browserify": "0.0.1", + "url": "~0.11.0", + "util": "~0.12.0", + "vm-browserify": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + } + } + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, + "browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "requires": { + "pako": "~1.0.5" + } + }, "browserslist": { "version": "4.14.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.0.tgz", "integrity": "sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ==", - "dev": true, "requires": { "caniuse-lite": "^1.0.30001111", "electron-to-chromium": "^1.3.523", @@ -1215,17 +1427,44 @@ "node-releases": "^1.1.60" } }, + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" + }, + "cached-path-relative": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", + "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==" + }, "caniuse-lite": { "version": "1.0.30001112", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001112.tgz", - "integrity": "sha512-J05RTQlqsatidif/38aN3PGULCLrg8OYQOlJUKbeYVzC2mGZkZLIztwRlB3MtrfLmawUmjFlNJvy/uhwniIe1Q==", - "dev": true + "integrity": "sha512-J05RTQlqsatidif/38aN3PGULCLrg8OYQOlJUKbeYVzC2mGZkZLIztwRlB3MtrfLmawUmjFlNJvy/uhwniIe1Q==" }, "chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1235,7 +1474,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", - "dev": true, "requires": { "is-regex": "^1.0.3" } @@ -1244,7 +1482,6 @@ "version": "3.4.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.1.tgz", "integrity": "sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g==", - "dev": true, "requires": { "anymatch": "~3.1.1", "braces": "~3.0.2", @@ -1256,11 +1493,19 @@ "readdirp": "~3.4.0" } }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "cli": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", - "dev": true, "requires": { "exit": "0.1.2", "glob": "^7.1.1" @@ -1270,7 +1515,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -1278,26 +1522,80 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", + "requires": { + "convert-source-map": "~1.1.0", + "inline-source-map": "~0.6.0", + "lodash.memoize": "~3.0.3", + "source-map": "~0.5.3" + }, + "dependencies": { + "convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=" + } + } }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } }, "console-browserify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true, "requires": { "date-now": "^0.1.4" } @@ -1306,17 +1604,20 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", - "dev": true, "requires": { "@babel/parser": "^7.6.0", "@babel/types": "^7.6.1" } }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" + }, "convert-source-map": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, "requires": { "safe-buffer": "~5.1.1" } @@ -1325,7 +1626,6 @@ "version": "3.6.5", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz", "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==", - "dev": true, "requires": { "browserslist": "^4.8.5", "semver": "7.0.0" @@ -1334,34 +1634,98 @@ "semver": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==" } } }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "corser": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", - "integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=", - "dev": true + "integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=" + }, + "create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "csstype": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", + "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==" + }, + "dash-ast": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", + "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==" }, "date-now": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, "requires": { "ms": "^2.1.1" } @@ -1370,22 +1734,78 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + }, + "deps-sort": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz", + "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==", + "requires": { + "JSONStream": "^1.0.3", + "shasum-object": "^1.0.0", + "subarg": "^1.0.0", + "through2": "^2.0.0" + } + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "detective": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", + "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "requires": { + "acorn-node": "^1.6.1", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, + "discord-markdown": { + "version": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#5ad8046d8d62a7fb8047e1a697c3848744d4e64d", + "from": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#5ad8046d8d62a7fb8047e1a697c3848744d4e64d", + "requires": { + "simple-markdown": "^0.7.2" + } + }, "doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=", - "dev": true + "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" }, "dom-serializer": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dev": true, "requires": { "domelementtype": "^2.0.1", "entities": "^2.0.0" @@ -1394,47 +1814,88 @@ "domelementtype": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", - "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", - "dev": true + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" }, "entities": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", - "dev": true + "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==" } } }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" + }, "domelementtype": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" }, "domhandler": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", - "dev": true, "requires": { "domelementtype": "1" } }, + "dompurify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.0.tgz", + "integrity": "sha512-bqFOQ7XRmmozp0VsKdIEe8UwZYxj0yttz7l80GBtBqdVRY48cOpXH2J/CVO7AEkV51qY0EBVXfilec18mdmQ/w==" + }, "domutils": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, "requires": { "dom-serializer": "0", "domelementtype": "1" } }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "requires": { + "readable-stream": "^2.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "ecstatic": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.2.tgz", "integrity": "sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==", - "dev": true, "requires": { "he": "^1.1.1", "mime": "^1.6.0", @@ -1445,50 +1906,171 @@ "electron-to-chromium": { "version": "1.3.524", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.524.tgz", - "integrity": "sha512-ZUvklIBkfXQyA6IeiEss1nfKRICcdB5afAGZAaPGaExdfrkpUu/WWVO+X7QpNnphaVMllXnAcvKnVPdyM+DCPQ==", - "dev": true + "integrity": "sha512-ZUvklIBkfXQyA6IeiEss1nfKRICcdB5afAGZAaPGaExdfrkpUu/WWVO+X7QpNnphaVMllXnAcvKnVPdyM+DCPQ==" + }, + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } }, "entities": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", - "dev": true + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" + }, + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "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.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "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.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + } + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } }, "escalade": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz", - "integrity": "sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==", - "dev": true + "integrity": "sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==" }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "eventemitter3": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz", - "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==", - "dev": true + "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==" + }, + "events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==" + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" + }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -1496,39 +2078,43 @@ "follow-redirects": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.12.1.tgz", - "integrity": "sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg==", - "dev": true + "integrity": "sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg==" + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, "optional": true }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "gensync": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", - "dev": true + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==" + }, + "get-assigned-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", + "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==" }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1542,7 +2128,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -1550,32 +2135,99 @@ "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "highlight.js": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.3.2.tgz", + "integrity": "sha512-3jRT7OUYsVsKvukNKZCtnvRcFyCJqSEIuIMsEybAXRiFSwpt65qjPd/Pr+UOdYt7WJlt+lj3+ypUsHiySBp/Jw==" + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "htmlescape": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=" }, "htmlparser2": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", - "dev": true, "requires": { "domelementtype": "1", "domhandler": "2.3", @@ -1588,7 +2240,6 @@ "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, "requires": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", @@ -1599,7 +2250,6 @@ "version": "0.12.3", "resolved": "https://registry.npmjs.org/http-server/-/http-server-0.12.3.tgz", "integrity": "sha512-be0dKG6pni92bRjq0kvExtj/NrrAd28/8fCXkaI/4piTwQMSDSLMhWyW0NI1V+DBI3aa1HMlQu46/HjVLfmugA==", - "dev": true, "requires": { "basic-auth": "^1.0.3", "colors": "^1.4.0", @@ -1613,11 +2263,20 @@ "union": "~0.5.0" } }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -1626,32 +2285,73 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "inline-source-map": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "requires": { + "source-map": "~0.5.3" + } + }, + "insert-module-globals": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.1.tgz", + "integrity": "sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==", + "requires": { + "JSONStream": "^1.0.3", + "acorn-node": "^1.5.2", + "combine-source-map": "^0.8.0", + "concat-stream": "^1.6.1", + "is-buffer": "^1.1.0", + "path-is-absolute": "^1.0.1", + "process": "~0.11.0", + "through2": "^2.0.0", + "undeclared-identifiers": "^1.1.2", + "xtend": "^4.0.0" + } }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, "requires": { "loose-envify": "^1.0.0" } }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "requires": { "binary-extensions": "^2.0.0" } }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" + }, "is-expression": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", - "dev": true, "requires": { "acorn": "^7.1.1", "object-assign": "^4.1.1" @@ -1660,68 +2360,87 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, "requires": { "is-extglob": "^2.1.1" } }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=" + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" }, "is-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", - "dev": true, "requires": { "has-symbols": "^1.0.1" } }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typed-array": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.3.tgz", + "integrity": "sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==", + "requires": { + "available-typed-arrays": "^1.0.0", + "es-abstract": "^1.17.4", + "foreach": "^2.0.5", + "has-symbols": "^1.0.1" + } + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=", - "dev": true + "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, "jshint": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.12.0.tgz", "integrity": "sha512-TwuuaUDmra0JMkuqvqy+WGo2xGHSNjv1BA1nTIgtH2K5z1jHuAEeAgp7laaR+hLRmajRjcrM71+vByBDanCyYA==", - "dev": true, "requires": { "cli": "~1.0.0", "console-browserify": "1.1.x", @@ -1737,32 +2456,42 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "dev": true, "requires": { "minimist": "^1.2.5" } }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + }, "jstransformer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", - "dev": true, "requires": { "is-promise": "^2.0.0", "promise": "^7.0.1" } }, + "labeled-stream-splicer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", + "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", + "requires": { + "inherits": "^2.0.1", + "stream-splicer": "^2.0.0" + } + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" }, "levenary": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz", "integrity": "sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==", - "dev": true, "requires": { "leven": "^3.1.0" } @@ -1770,29 +2499,66 @@ "lodash": { "version": "4.17.19", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + }, + "lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=" }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -1800,59 +2566,111 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, "requires": { "minimist": "^1.2.5" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "module-deps": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.3.tgz", + "integrity": "sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==", + "requires": { + "JSONStream": "^1.0.3", + "browser-resolve": "^2.0.0", + "cached-path-relative": "^1.0.2", + "concat-stream": "~1.6.0", + "defined": "^1.0.0", + "detective": "^5.2.0", + "duplexer2": "^0.1.2", + "inherits": "^2.0.1", + "parents": "^1.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.4.0", + "stream-combiner2": "^1.1.1", + "subarg": "^1.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "dev": true + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" }, "node-releases": { "version": "1.1.60", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz", - "integrity": "sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==", - "dev": true + "integrity": "sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==" }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object.assign": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -1864,7 +2682,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -1872,52 +2689,127 @@ "opener": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", - "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==", - "dev": true + "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==" + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "parents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "requires": { + "path-platform": "~0.11.15" + } + }, + "parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "requires": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-platform": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=" + }, + "pbkdf2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" }, "portfinder": { "version": "1.0.28", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", - "dev": true, "requires": { "async": "^2.6.2", "debug": "^3.1.1", "mkdirp": "^0.5.5" } }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, "requires": { "asap": "~2.0.3" } }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, "pug": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.0.tgz", "integrity": "sha512-inmsJyFBSHZaiGLaguoFgJGViX0If6AcfcElimvwj9perqjDpUpw79UIEDZbWFmoGVidh08aoE+e8tVkjVJPCw==", - "dev": true, "requires": { "pug-code-gen": "^3.0.0", "pug-filters": "^4.0.0", @@ -1933,7 +2825,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", - "dev": true, "requires": { "constantinople": "^4.0.1", "js-stringify": "^1.0.2", @@ -1944,7 +2835,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.1.tgz", "integrity": "sha512-xJIGvmXTQlkJllq6hqxxjRWcay2F9CU69TuAuiVZgHK0afOhG5txrQOcZyaPHBvSWCU/QQOqEp5XCH94rRZpBQ==", - "dev": true, "requires": { "constantinople": "^4.0.1", "doctypes": "^1.1.0", @@ -1959,14 +2849,12 @@ "pug-error": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.0.0.tgz", - "integrity": "sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==", - "dev": true + "integrity": "sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==" }, "pug-filters": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", - "dev": true, "requires": { "constantinople": "^4.0.1", "jstransformer": "1.0.0", @@ -1979,7 +2867,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.0.tgz", "integrity": "sha512-52xMk8nNpuyQ/M2wjZBN5gXQLIylaGkAoTk5Y1pBhVqaopaoj8Z0iVzpbFZAqitL4RHNVDZRnJDsqEYe99Ti0A==", - "dev": true, "requires": { "character-parser": "^2.2.0", "is-expression": "^4.0.0", @@ -1990,7 +2877,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", - "dev": true, "requires": { "pug-error": "^2.0.0", "pug-walk": "^2.0.0" @@ -2000,7 +2886,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", - "dev": true, "requires": { "object-assign": "^4.1.1", "pug-walk": "^2.0.0" @@ -2010,7 +2895,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", - "dev": true, "requires": { "pug-error": "^2.0.0", "token-stream": "1.0.0" @@ -2019,14 +2903,12 @@ "pug-runtime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.0.tgz", - "integrity": "sha512-GoEPcmQNnaTsePEdVA05bDpY+Op5VLHKayg08AQiqJBWU/yIaywEYv7TetC5dEQS3fzBBoyb2InDcZEg3mPTIA==", - "dev": true + "integrity": "sha512-GoEPcmQNnaTsePEdVA05bDpY+Op5VLHKayg08AQiqJBWU/yIaywEYv7TetC5dEQS3fzBBoyb2InDcZEg3mPTIA==" }, "pug-strip-comments": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", - "dev": true, "requires": { "pug-error": "^2.0.0" } @@ -2034,20 +2916,86 @@ "pug-walk": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", - "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", - "dev": true + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "qs": { "version": "6.9.4", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", - "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", - "dev": true + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "read-only-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "requires": { + "readable-stream": "^2.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } }, "readable-stream": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -2059,7 +3007,6 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", - "dev": true, "requires": { "picomatch": "^2.2.1" } @@ -2067,14 +3014,12 @@ "regenerate": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz", - "integrity": "sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==", - "dev": true + "integrity": "sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==" }, "regenerate-unicode-properties": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", - "dev": true, "requires": { "regenerate": "^1.4.0" } @@ -2082,14 +3027,12 @@ "regenerator-runtime": { "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", - "dev": true + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" }, "regenerator-transform": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", - "dev": true, "requires": { "@babel/runtime": "^7.8.4" } @@ -2098,7 +3041,6 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", "integrity": "sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==", - "dev": true, "requires": { "regenerate": "^1.4.0", "regenerate-unicode-properties": "^8.2.0", @@ -2111,14 +3053,12 @@ "regjsgen": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", - "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", - "dev": true + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==" }, "regjsparser": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", - "dev": true, "requires": { "jsesc": "~0.5.0" }, @@ -2126,37 +3066,46 @@ "jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" } } }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, "resolve": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "dev": true, "requires": { "path-parse": "^1.0.6" } }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sass": { "version": "1.26.10", "resolved": "https://registry.npmjs.org/sass/-/sass-1.26.10.tgz", "integrity": "sha512-bzN0uvmzfsTvjz0qwccN1sPm2HxxpNI/Xa+7PlUEMS+nQvbyuEK7Y0qFqxlPHhiNHb1Ze8WQJtU31olMObkAMw==", - "dev": true, "requires": { "chokidar": ">=2.0.0 <4.0.0" } @@ -2164,59 +3113,396 @@ "secure-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", - "integrity": "sha1-8aAymzCLIh+uN7mXTz1XjQypmeM=", - "dev": true + "integrity": "sha1-8aAymzCLIh+uN7mXTz1XjQypmeM=" }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shasum-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", + "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==", + "requires": { + "fast-safe-stringify": "^2.0.7" + } + }, + "shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==" }, "shelljs": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", - "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", - "dev": true + "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=" + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-markdown": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/simple-markdown/-/simple-markdown-0.7.2.tgz", + "integrity": "sha512-XfCvqqzMyzRj4L7eIxJgGaQ2Gaxr20GhTFMB+1yuY8q3xffjzmOg4Q5tC0kcaJPV42NNUHCQDaRK6jzi3/RhrA==", + "requires": { + "@types/react": ">=16.0.0" + } }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "requires": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "requires": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-http": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.1.1.tgz", + "integrity": "sha512-S7OqaYu0EkFpgeGFb/NPOoPLxFko7TPqtEeFg5DXPB4v/KETHG0Ln6fRFrNezoelpaDKmycEmmZ81cC9DAwgYg==", + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "stream-splicer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", + "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "string.prototype.trimend": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", + "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "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.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } + } + }, + "string.prototype.trimstart": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", + "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "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.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } + } }, "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, "strip-json-comments": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", - "dev": true + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=" + }, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "requires": { + "minimist": "^1.1.0" + } }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, "requires": { "has-flag": "^4.0.0" } }, + "syntax-error": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", + "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", + "requires": { + "acorn-node": "^1.2.0" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "requires": { + "process": "~0.11.0" + } + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "requires": { "is-number": "^7.0.0" } @@ -2224,20 +3510,44 @@ "token-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", - "integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=", - "dev": true + "integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=" + }, + "tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "umd": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", + "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==" + }, + "undeclared-identifiers": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", + "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", + "requires": { + "acorn-node": "^1.3.0", + "dash-ast": "^1.0.0", + "get-assigned-identifiers": "^1.2.0", + "simple-concat": "^1.0.0", + "xtend": "^4.0.1" + } }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", - "dev": true + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==" }, "unicode-match-property-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", - "dev": true, "requires": { "unicode-canonical-property-names-ecmascript": "^1.0.4", "unicode-property-aliases-ecmascript": "^1.0.4" @@ -2246,41 +3556,87 @@ "unicode-match-property-value-ecmascript": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", - "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", - "dev": true + "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==" }, "unicode-property-aliases-ecmascript": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", - "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", - "dev": true + "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==" }, "union": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", - "dev": true, "requires": { "qs": "^6.4.0" } }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, "url-join": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz", - "integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=", - "dev": true + "integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=" + }, + "util": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.3.tgz", + "integrity": "sha512-I8XkoQwE+fPQEhy9v012V+TSdH2kp9ts29i20TaaDUXsg7x/onePbhFJUExBfv/2ay1ZOp/Vsm3nDlmnFGSAog==", + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "safe-buffer": "^5.1.2", + "which-typed-array": "^1.1.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, "void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=", - "dev": true + "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=" + }, + "which-typed-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.2.tgz", + "integrity": "sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==", + "requires": { + "available-typed-arrays": "^1.0.2", + "es-abstract": "^1.17.5", + "foreach": "^2.0.5", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.1", + "is-typed-array": "^1.1.3" + } }, "with": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", - "dev": true, "requires": { "@babel/parser": "^7.9.6", "@babel/types": "^7.9.6", @@ -2291,8 +3647,12 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" } } } diff --git a/package.json b/package.json index 4d32107..f63bf03 100644 --- a/package.json +++ b/package.json @@ -5,21 +5,25 @@ "main": "build.js", "scripts": { "build": "node build.js", - "watch": "fish -c 'while true; echo -n \"Build started at \"; date; npm run build; inotifywait (find src -type f) build.js -e close_write -qq; end'", + "watch": "sh -c 'while true; do echo -n \"Build started at \"; date; npm run build; inotifywait $(find src -type f) build.js spec.js package.json -e close_write -qq; done'", "rebuild": "rm build -rf && node build.js" }, "keywords": [], "author": "", "license": "AGPL-3.0-only", - "dependencies": {}, - "devDependencies": { + "dependencies": { "@babel/core": "^7.11.1", "@babel/preset-env": "^7.11.0", + "browserify": "^17.0.0", "chalk": "^4.1.0", + "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#5ad8046d8d62a7fb8047e1a697c3848744d4e64d", + "dompurify": "^2.2.0", + "highlight.js": "^10.3.2", "http-server": "^0.12.3", "jshint": "^2.12.0", "node-fetch": "^2.6.0", "pug": "^3.0.0", "sass": "^1.26.10" - } + }, + "devDependencies": {} } diff --git a/spec.js b/spec.js index 38bb96e..4244cdf 100644 --- a/spec.js +++ b/spec.js @@ -2,121 +2,96 @@ module.exports = [ { type: "file", source: "/assets/fonts/whitney-500.woff", - target: "/static/whitney-500.woff" + target: "/static/whitney-500.woff", }, { type: "file", source: "/assets/fonts/whitney-400.woff", - target: "/static/whitney-400.woff" + target: "/static/whitney-400.woff", }, { - type: "js", - source: "/js/basic.js", - target: "/static/basic.js" + type: "bundle", + source: "/js/login.js", + target: "/static/login.js", }, { - type: "js", - source: "/js/groups.js", - target: "/static/groups.js" - }, - { - type: "js", - source: "/js/chat-input.js", - target: "/static/chat-input.js" - }, - { - type: "js", - source: "/js/room-picker.js", - target: "/static/room-picker.js" - }, - { - type: "js", - source: "/js/store/store.js", - target: "/static/store/store.js" - }, - { - type: "js", - source: "/js/store/Subscribable.js", - target: "/static/store/Subscribable.js" - }, - { - type: "js", - source: "/js/store/SubscribeValue.js", - target: "/static/store/SubscribeValue.js" - }, - { - type: "js", - source: "/js/store/SubscribeMapList.js", - target: "/static/store/SubscribeMapList.js" - }, - { - type: "js", - source: "/js/store/SubscribeSet.js", - target: "/static/store/SubscribeSet.js" - }, - { - type: "js", - source: "/js/sync/sync.js", - target: "/static/sync/sync.js" - }, - { - type: "js", - source: "/js/lsm.js", - target: "/static/lsm.js" - }, - { - type: "js", - source: "/js/Timeline.js", - target: "/static/Timeline.js" - }, - { - type: "js", - source: "/js/Anchor.js", - target: "/static/Anchor.js" - }, - { - type: "js", - source: "/js/chat.js", - target: "/static/chat.js" - }, - { - type: "js", - source: "/js/functions.js", - target: "/static/functions.js" + type: "bundle", + source: "/js/main.js", + target: "/static/bundle.js" }, { type: "file", source: "/assets/fonts/whitney-500.woff", - target: "/static/whitney-500.woff" + target: "/static/whitney-500.woff", }, { type: "file", source: "/assets/icons/directs.svg", - target: "/static/directs.svg" + target: "/static/directs.svg", }, { type: "file", source: "/assets/icons/channels.svg", - target: "/static/channels.svg" + target: "/static/channels.svg", }, { type: "file", source: "/assets/icons/join-event.svg", - target: "/static/join-event.svg" + target: "/static/join-event.svg", + }, + { + type: "file", + source: "/assets/icons/leave-event.svg", + target: "/static/leave-event.svg", + }, + { + type: "file", + source: "/assets/icons/invite-event.svg", + target: "/static/invite-event.svg", + }, + { + type: "file", + source: "/assets/icons/profile-event.svg", + target: "/static/profile-event.svg", + }, + { + type: "file", + source: "/assets/icons/call-out.svg", + target: "/static/call-out.svg", + }, + { + type: "file", + source: "/assets/icons/call-in.svg", + target: "/static/call-in.svg", + }, + { + type: "file", + source: "/assets/icons/call-accepted.svg", + target: "/static/call-accepted.svg", + }, + { + type: "file", + source: "/assets/icons/call-rejected.svg", + target: "/static/call-rejected.svg", }, { type: "sass", source: "/sass/main.sass", - target: "/static/main.css" + target: "/static/main.css", + }, + { + type: "sass", + source: "/sass/login.sass", + target: "/static/login.css", }, { type: "pug", source: "/home.pug", - target: "/index.html" + target: "/index.html", }, { type: "pug", source: "/login.pug", - target: "/login.html" - } -] + target: "/login/index.html", + }, +]; diff --git a/src/assets/icons/call-accepted.svg b/src/assets/icons/call-accepted.svg new file mode 100644 index 0000000..fea7031 --- /dev/null +++ b/src/assets/icons/call-accepted.svg @@ -0,0 +1,94 @@ + + + + + + image/svg+xml + + free-icons-solid + + + + + + + + + + free-icons-solid + + + + diff --git a/src/assets/icons/call-in.svg b/src/assets/icons/call-in.svg new file mode 100644 index 0000000..484fe5c --- /dev/null +++ b/src/assets/icons/call-in.svg @@ -0,0 +1,99 @@ + + + + + + image/svg+xml + + free-icons-solid + + + + + + + + + + free-icons-solid + + + + + + diff --git a/src/assets/icons/call-out.svg b/src/assets/icons/call-out.svg new file mode 100644 index 0000000..877bad4 --- /dev/null +++ b/src/assets/icons/call-out.svg @@ -0,0 +1,98 @@ + + + + + + image/svg+xml + + free-icons-solid + + + + + + + + + + free-icons-solid + + + + + + diff --git a/src/assets/icons/call-rejected.svg b/src/assets/icons/call-rejected.svg new file mode 100644 index 0000000..55b3994 --- /dev/null +++ b/src/assets/icons/call-rejected.svg @@ -0,0 +1,98 @@ + + + + + + image/svg+xml + + free-icons-solid + + + + + + + + + + free-icons-solid + + + + + + diff --git a/src/assets/icons/invite-event.svg b/src/assets/icons/invite-event.svg new file mode 100644 index 0000000..fa44732 --- /dev/null +++ b/src/assets/icons/invite-event.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/assets/icons/join-event.svg b/src/assets/icons/join-event.svg index 042e3bd..2b6b901 100644 --- a/src/assets/icons/join-event.svg +++ b/src/assets/icons/join-event.svg @@ -25,9 +25,9 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="1" - inkscape:cx="15.649008" - inkscape:cy="8.3751893" + inkscape:zoom="11.313708" + inkscape:cx="-4.2728481" + inkscape:cy="-2.1951295" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" diff --git a/src/assets/icons/leave-event.svg b/src/assets/icons/leave-event.svg new file mode 100644 index 0000000..f836616 --- /dev/null +++ b/src/assets/icons/leave-event.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/assets/icons/profile-event.svg b/src/assets/icons/profile-event.svg new file mode 100644 index 0000000..6fcdadf --- /dev/null +++ b/src/assets/icons/profile-event.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/home.pug b/src/home.pug index 68bbd21..a9450f3 100644 --- a/src/home.pug +++ b/src/home.pug @@ -33,19 +33,15 @@ doctype html html head meta(charset="utf-8") + title Carbon // var static = !{JSON.stringify([...static.entries()].reduce((a, c) => (a[c[0]] = getRelative(c[1]), a), {}))} script | var staticFiles = new Map( != JSON.stringify([...static.keys()].map(k => [k, getStatic(k)])) | ) link(rel="stylesheet" type="text/css" href=getStatic("/sass/main.sass")) - script(type="module" src=getStatic("/js/groups.js")) - script(type="module" src=getStatic("/js/chat-input.js")) - script(type="module" src=getStatic("/js/room-picker.js")) - script(type="module" src=getStatic("/js/sync/sync.js")) - script(type="module" src=getStatic("/js/chat.js")) - title Carbon - body + script(type="module" src=getStatic("/js/main.js")) + body.show-focus main.main .c-groups .c-groups__display#c-groups-display @@ -53,7 +49,9 @@ html .c-groups__container#c-groups-list .c-rooms#c-rooms .c-chat + .c-chat-banner#c-chat-banner .c-chat__messages#c-chat-messages .c-chat__inner#c-chat .c-chat-input - textarea(placeholder="Send a message..." autocomplete="off").c-chat-input__textarea#c-chat-textarea \ No newline at end of file + textarea(placeholder="Send a message..." autocomplete="off").c-chat-input__textarea#c-chat-textarea + .c-typing#c-typing diff --git a/src/js/Timeline.js b/src/js/Timeline.js deleted file mode 100644 index 94e77bf..0000000 --- a/src/js/Timeline.js +++ /dev/null @@ -1,366 +0,0 @@ -import {ElemJS, ejs} from $to_relative "/js/basic.js" -import {Subscribable} from $to_relative "/js/store/Subscribable.js" -import {store} from $to_relative "/js/store/store.js" -import {Anchor} from $to_relative "/js/Anchor.js" -import * as lsm from $to_relative "/js/lsm.js" -import {resolveMxc} from $to_relative "/js/functions.js" - -const dateFormatter = Intl.DateTimeFormat("default", {hour: "numeric", minute: "numeric", day: "numeric", month: "short", year: "numeric"}) - -let sentIndex = 0 - -function getTxnId() { - return Date.now() + (sentIndex++) -} - -function eventSearch(list, event, min = 0, max = -1) { - if (list.length === 0) return {success: false, i: 0} - - if (max === -1) max = list.length - 1 - let mid = Math.floor((max + min) / 2) - // success condition - if (list[mid] && list[mid].data.event_id === event.data.event_id) return {success: true, i: mid} - // failed condition - if (min >= max) { - while (mid !== -1 && (!list[mid] || list[mid].data.origin_server_ts > event.data.origin_server_ts)) mid-- - return { - success: false, - i: mid + 1 - } - } - // recurse (below) - if (list[mid].data.origin_server_ts > event.data.origin_server_ts) return eventSearch(list, event, min, mid-1) - // recurse (above) - else return eventSearch(list, event, mid+1, max) -} - -class Event extends ElemJS { - constructor(data) { - super("div") - this.class("c-message") - this.data = null - this.group = null - this.editedAt = null - this.update(data) - } - - // predicates - - canGroup() { - return this.data.type === "m.room.message" - } - - // operations - - setGroup(group) { - this.group = group - } - - setEdited(time) { - this.editedAt = time - this.render() - } - - update(data) { - this.data = data - this.render() - } - - removeEvent() { - if (this.group) this.group.removeEvent(this) - else this.remove() - } - - render() { - this.element.classList[this.data.pending ? "add" : "remove"]("c-message--pending") - if (this.data.type === "m.room.message") { - this.text(this.data.content.body) - } else if (this.data.type === "m.room.member") { - if (this.data.content.membership === "join") { - this.child(ejs("i").text("joined the room")) - } else { - this.child(ejs("i").text("left the room")) - } - } else { - this.child(ejs("i").text(`Unsupported event type ${this.data.type}`)) - } - if (this.editedAt) { - this.child(ejs("span").class("c-message__edited").text("(edited)").attribute("title", "at " + dateFormatter.format(this.editedAt))) - } - } -} - -class Sender { - constructor(roomID, mxid) { - this.sender = store.rooms.get(roomID).value().members.get(mxid) - this.sender.subscribe("changeSelf", this.update.bind(this)) - this.name = new ElemJS("div").class("c-message-group__name") - this.avatar = new ElemJS("div").class("c-message-group__avatar") - this.displayingGoodData = false - this.update() - } - - update() { - if (this.sender.exists()) { - // name - if (this.sender.value().content.displayname) { - this.name.text(this.sender.value().content.displayname) - this.displayingGoodData = true - } else if (!this.displayingGoodData) { - this.name.text(this.sender.value().state_key) - } - - // avatar - this.avatar.clearChildren() - if (this.sender.value().content.avatar_url) { - this.avatar.child( - ejs("img").class("c-message-group__icon").attribute("src", resolveMxc(this.sender.value().content.avatar_url, 96, "crop")) - ) - } else { - this.avatar.child( - ejs("div").class("c-message-group__icon", "c-message-group__icon--no-icon") - ) - } - } - } -} - -class EventGroup extends ElemJS { - constructor(reactive, list) { - super("div") - this.class("c-message-group") - this.reactive = reactive - this.list = list - this.data = { - sender: list[0].data.sender, - origin_server_ts: list[0].data.origin_server_ts - } - this.sender = new Sender(this.reactive.id, this.data.sender) - this.child( - this.sender.avatar, - this.messages = ejs("div").class("c-message-group__messages").child( - ejs("div").class("c-message-group__intro").child( - this.sender.name, - ejs("div").class("c-message-group__date").text(dateFormatter.format(this.data.origin_server_ts)) - ), - ...this.list - ) - ) - } - - addEvent(event) { - const index = eventSearch(this.list, event).i - event.setGroup(this) - this.list.splice(index, 0, event) - this.messages.childAt(index + 1, event) - } - - removeEvent(event) { - const search = eventSearch(this.list, event) - if (!search.success) throw new Error(`Event ${event.data.event_id} not found in this group`) - const index = search.i - // actually remove the event - this.list.splice(index, 1) - event.remove() // should get everything else - if (this.list.length === 0) this.reactive.removeGroup(this) - } -} - -class ReactiveTimeline extends ElemJS { - constructor(id, list) { - super("div") - this.class("c-event-groups") - this.id = id - this.list = list - this.render() - } - - addEvent(event) { - const search = eventSearch(this.list, event) - // console.log(search, this.list.map(l => l.data.sender), event.data) - if (!search.success && search.i >= 1) this.tryAddGroups(event, [search.i-1, search.i]) - else this.tryAddGroups(event, [search.i]) - } - - tryAddGroups(event, indices) { - const success = indices.some(i => { - if (!this.list[i]) { - // if (printed++ < 100) console.log("tryadd success, created group") - const group = new EventGroup(this, [event]) - this.list.splice(i, 0, group) - this.childAt(i, group) - event.setGroup(group) - return true - } else if (this.list[i] && this.list[i].data.sender === event.data.sender) { - // if (printed++ < 100) console.log("tryadd success, using existing group") - this.list[i].addEvent(event) - return true - } - }) - if (!success) console.log("tryadd failure", indices, this.list.map(l => l.data.sender), event.data) - } - - removeGroup(group) { - const index = this.list.indexOf(group) - this.list.splice(index, 1) - group.remove() // should get everything else - } - - render() { - this.clearChildren() - this.list.forEach(group => this.child(group)) - this.anchor = new Anchor() - this.child(this.anchor) - } -} - -class Timeline extends Subscribable { - constructor(room) { - super() - Object.assign(this.events, { - beforeChange: [], - afterChange: [] - }) - Object.assign(this.eventDeps, { - beforeChange: [], - afterChange: [] - }) - this.room = room - this.id = this.room.id - this.list = [] - this.map = new Map() - this.reactiveTimeline = new ReactiveTimeline(this.id, []) - this.latest = 0 - this.pending = new Set() - } - - updateStateEvents(events) { - for (const eventData of events) { - let id = eventData.event_id - if (eventData.type === "m.room.member") { - // update members - if (eventData.membership !== "leave") { - this.room.members.get(eventData.state_key).set(eventData) - } - } - } - } - - updateEvents(events) { - this.broadcast("beforeChange") - // handle state events - this.updateStateEvents(events) - for (const eventData of events) { - // set variables - this.latest = Math.max(this.latest, eventData.origin_server_ts) - let id = eventData.event_id - // handle local echoes - if (eventData.sender === lsm.get("mx_user_id") && eventData.content && this.pending.has(eventData.content["chat.carbon.message.pending_id"])) { - const target = this.map.get(eventData.content["chat.carbon.message.pending_id"]) - this.map.set(id, target) - this.map.delete(eventData.content["chat.carbon.message.pending_id"]) - } - // handle timeline events - if (this.map.has(id)) { - // update existing event - this.map.get(id).update(eventData) - } else { - // skip displaying events that we don't know how to - if (eventData.type === "m.reaction") { - continue - } - // skip redacted events - if (eventData.unsigned && eventData.unsigned.redacted_by) { - continue - } - // handle redactions - if (eventData.type === "m.room.redaction") { - if (this.map.has(eventData.redacts)) this.map.get(eventData.redacts).removeEvent() - continue - } - // handle edits - if (eventData.type === "m.room.message" && eventData.content["m.relates_to"] && eventData.content["m.relates_to"].rel_type === "m.replace") { - const replaces = eventData.content["m.relates_to"].event_id - if (this.map.has(replaces)) { - const event = this.map.get(replaces) - event.data.content = eventData.content["m.new_content"] - event.setEdited(eventData.origin_server_ts) - event.update(event.data) - continue - } else { - // uhhhhhhh - console.error(`want to replace event ${replaces} with ${eventData.id} but replaced event not found`) - } - } - // add new event - const event = new Event(eventData) - this.map.set(id, event) - this.reactiveTimeline.addEvent(event) - } - } - this.broadcast("afterChange") - } - - removeEvent(id) { - if (!this.map.has(id)) throw new Error(`Tried to delete event ID ${id} which does not exist`) - this.map.get(id).removeEvent() - this.map.delete(id) - } - - getTimeline() { - return this.reactiveTimeline - } - - send(body) { - const tx = getTxnId() - const id = `pending$${tx}` - this.pending.add(id) - const content = { - msgtype: "m.text", - body, - "chat.carbon.message.pending_id": id - } - const fakeEvent = { - type: "m.room.message", - origin_server_ts: Date.now(), - event_id: id, - sender: lsm.get("mx_user_id"), - content, - pending: true - } - this.updateEvents([fakeEvent]) - return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${this.id}/send/m.room.message/${tx}?access_token=${lsm.get("access_token")}`, { - method: "PUT", - body: JSON.stringify(content), - headers: { - "Content-Type": "application/json" - } - })/*.then(() => { - const subscription = () => { - this.removeEvent(id) - this.unsubscribe("afterChange", subscription) - } - this.subscribe("afterChange", subscription) - })*/ - } -/* - getGroupedEvents() { - let currentSender = Symbol("N/A") - let groups = [] - let currentGroup = [] - for (const event of this.list) { - if (event.sender === currentSender) { - currentGroup.push(event) - } else { - if (currentGroup.length) groups.push(currentGroup) - currentGroup = [event] - currentSender = event.sender - } - } - if (currentGroup.length) groups.push(currentGroup) - return groups - } - */ -} - -export {Timeline} diff --git a/src/js/Anchor.js b/src/js/anchor.js similarity index 74% rename from src/js/Anchor.js rename to src/js/anchor.js index 2adbe5f..498a287 100644 --- a/src/js/Anchor.js +++ b/src/js/anchor.js @@ -1,4 +1,4 @@ -import {ElemJS} from $to_relative "/js/basic.js" +const {ElemJS} = require("./basic.js") class Anchor extends ElemJS { constructor() { @@ -12,4 +12,4 @@ class Anchor extends ElemJS { } } -export {Anchor} +module.exports = {Anchor} diff --git a/src/js/basic.js b/src/js/basic.js index 1f3e695..38d413c 100644 --- a/src/js/basic.js +++ b/src/js/basic.js @@ -19,12 +19,12 @@ const qa = s => document.querySelectorAll(s); */ class ElemJS { constructor(type) { - if (type instanceof HTMLElement) { - // If passed an existing element, bind to it - this.bind(type); - } else { - // Otherwise, create a new detached element to bind to + if (typeof type === "string") { + // Passed a tag name; create an element to bind to this.bind(document.createElement(type)); + } else { + // Passed an existing element; bind to it + this.bind(type); } this.children = []; } @@ -157,4 +157,4 @@ function ejs(tag) { return new ElemJS(tag); } -export {q, qa, ElemJS, ejs} +module.exports = {q, qa, ElemJS, ejs} diff --git a/src/js/chat-input.js b/src/js/chat-input.js index de0eec7..a2fa323 100644 --- a/src/js/chat-input.js +++ b/src/js/chat-input.js @@ -1,28 +1,93 @@ -import {q} from $to_relative "/js/basic.js" -import {store} from $to_relative "/js/store/store.js" -import * as lsm from $to_relative "/js/lsm.js" -import {chat} from $to_relative "/js/chat.js" +const {q} = require("./basic.js") +const {store} = require("./store/store.js") +const lsm = require("./lsm.js") +const {chat} = require("./chat.js") +const {toHTML} = require("discord-markdown") const input = q("#c-chat-textarea") +class TypingManager { + constructor() { + /** How long to appear to type for. */ + this.time = 20000 + /** How long before the end of the timeout to send the request again. */ + this.margin = 5000 + /** The room that we're typing in. We can semantically only type in one room at a time. */ + this.typingRoom = null + this.timeout = null + } + + request(id, typing) { + const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/rooms/${id}/typing/${lsm.get("mx_user_id")}`) + url.searchParams.set("access_token", lsm.get("access_token")) + const body = {typing} + if (typing) body.timeout = this.time + fetch(url.toString(), { + method: "PUT", + body: JSON.stringify(body) + }) + } + + schedule(id) { + this.request(id, true) + this.timeout = setTimeout(() => { + this.schedule(id) + }, this.time - this.margin) + } + + update(id) { + if (id) { // typing somewhere + if (this.typingRoom === id) return // already typing, don't do anything + // state + this.typingRoom = id + // mark and schedule + this.schedule(id) + // add self to typing list now instead of waiting a round trip + const typing = store.rooms.get(id).value().timeline.typing + typing.edit(list => list.concat(lsm.get("mx_user_id"))) + } else { // stopped typing + if (this.typingRoom) { + clearTimeout(this.timeout) + this.request(this.typingRoom, false) + } + this.typingRoom = null + } + } +} + +const typingManager = new TypingManager() + store.activeRoom.subscribe("changeSelf", () => { + // stop typing. you semantically can't type in a room you're not in. + typingManager.update(null) + // focus input box if (store.activeRoom.exists()) { input.focus() } }) input.addEventListener("keydown", event => { + if (!store.activeRoom.exists()) return + // send message? if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey) { event.preventDefault() const body = input.value send(input.value) + typingManager.update(null) // stop typing input.value = "" fixHeight() + return } }) input.addEventListener("input", () => { fixHeight() + // set typing + if (input.value) { + typingManager.update(store.activeRoom.value().id) + } else { + typingManager.update(null) + } }) function fixHeight() { @@ -33,5 +98,13 @@ function fixHeight() { function send(body) { if (!store.activeRoom.exists()) return - return store.activeRoom.value().timeline.send(body) + if (!body.trim().length) return + const content = { + msgtype: "m.text", + format: "org.matrix.custom.html", + body, + formatted_body: toHTML(body), + "chat.carbon.message.input_body": body + } + return store.activeRoom.value().timeline.send("m.room.message", content) } diff --git a/src/js/chat.js b/src/js/chat.js index e3df0eb..6695c3a 100644 --- a/src/js/chat.js +++ b/src/js/chat.js @@ -1,5 +1,5 @@ -import {ElemJS, q, ejs} from $to_relative "/js/basic.js" -import {store} from $to_relative "/js/store/store.js" +const {ElemJS, q, ejs} = require("./basic.js") +const {store} = require("./store/store.js") const chatMessages = q("#c-chat-messages") @@ -27,7 +27,7 @@ class Chat extends ElemJS { // connect to the new room's timeline updater if (store.activeRoom.exists()) { const timeline = store.activeRoom.value().timeline - const subscription = () => { + const beforeChangeSubscription = () => { // scroll anchor does not work if the timeline is scrolled to the top. // at the start, when there are not enough messages for a full screen, this is the case. // once there are enough messages that scrolling is necessary, we initiate a scroll down to activate the scroll anchor. @@ -40,12 +40,29 @@ class Chat extends ElemJS { } }, 0) } - const name = "beforeChange" - this.removableSubscriptions.push({name, target: timeline, subscription}) - timeline.subscribe(name, subscription) + this.addSubscription("beforeChange", timeline, beforeChangeSubscription) + + // Make sure after loading scrollback we don't move the scroll position + const beforeScrollbackLoadSubscription = () => { + const lastScrollHeight = chatMessages.scrollHeight; + + const afterScrollbackLoadSub = () => { + const scrollDiff = chatMessages.scrollHeight - lastScrollHeight; + chatMessages.scrollTop += scrollDiff; + + timeline.unsubscribe("afterScrollbackLoad", afterScrollbackLoadSub) + } + + timeline.subscribe("afterScrollbackLoad", afterScrollbackLoadSub) + } + this.addSubscription("beforeScrollbackLoad", timeline, beforeScrollbackLoadSubscription) } this.render() } + addSubscription(name, target, subscription) { + this.removableSubscriptions.push({name, target, subscription}) + target.subscribe(name, subscription) + } render() { this.clearChildren() @@ -62,4 +79,4 @@ class Chat extends ElemJS { const chat = new Chat() -export {chat} +module.exports = {chat} diff --git a/src/js/date-formatter.js b/src/js/date-formatter.js new file mode 100644 index 0000000..c161d76 --- /dev/null +++ b/src/js/date-formatter.js @@ -0,0 +1,3 @@ +const dateFormatter = Intl.DateTimeFormat("default", {hour: "numeric", minute: "numeric", day: "numeric", month: "short", year: "numeric"}) + +module.exports = {dateFormatter} diff --git a/src/js/events/call.js b/src/js/events/call.js new file mode 100644 index 0000000..a918a48 --- /dev/null +++ b/src/js/events/call.js @@ -0,0 +1,67 @@ +const {UngroupableEvent} = require("./event") +const {ejs} = require("../basic") +const lsm = require("../lsm") +const {extractDisplayName, resolveMxc, extractLocalpart} = require("../functions") + +class CallEvent extends UngroupableEvent { + constructor(data) { + super(data) + this.class("c-message-event") + this.senderName = extractLocalpart(this.data.sender) + this.render() + } + + renderInner(iconURL, elements) { + this.clearChildren() + this.child( + ejs("div").class("c-message-event__inner").child( + iconURL ? ejs("img").class("c-message-event__icon").attribute("width", "20").attribute("height", "20").attribute("src", iconURL) : "", + ...elements + ) + ) + super.render() + } +} + +class CallInviteEvent extends CallEvent { + static canRender(eventData) { + return eventData.type === "m.call.invite" + } + + render() { + const icon = this.data.sender === lsm.get("mx_user_id") ? "static/call-out.svg" : "static/call-in.svg" + this.renderInner(icon, [ + this.senderName, + " started a VOIP call, but Carbon doesn't support VOIP calls" + ]) + } +} + +class CallAnswerEvent extends CallEvent { + static canRender(eventData) { + return eventData.type === "m.call.answer" + } + + render() { + this.renderInner("static/call-accepted.svg", [ + this.senderName, + " answered the call" + ]) + } +} + +class CallHangupEvent extends CallEvent { + static canRender(eventData) { + return eventData.type === "m.call.hangup" + } + + render() { + const reason = this.data.content.reason === "invite_timeout" ? "missed the call" : "hung up the call" + this.renderInner("static/call-rejected.svg", [ + this.senderName, + " " + reason + ]) + } +} + +module.exports = [CallInviteEvent, CallAnswerEvent, CallHangupEvent] diff --git a/src/js/events/components.js b/src/js/events/components.js new file mode 100644 index 0000000..1de9d91 --- /dev/null +++ b/src/js/events/components.js @@ -0,0 +1,36 @@ +const {ElemJS} = require("../basic") +const {lazyLoad} = require("../lazy-load-module") + +class HighlightedCode extends ElemJS { + constructor(element) { + super(element) + if (this.element.tagName === "PRE" && this.element.children.length === 1 && this.element.children[0].tagName === "CODE") { + // we shouldn't nest inside
. put the text in 
 directly.
+			const code = this.element.children[0]
+			this.clearChildren()
+			while (code.firstChild) {
+				this.element.appendChild(code.firstChild)
+			}
+		}
+		let shouldHighlight = (
+			// if there are child _elements_, it's already formatted, we shouldn't mess that up
+			this.element.children.length === 0
+			/*
+			  no need to highlight very short code blocks:
+			  - content inside might not be code, some users still use code blocks
+			  for plaintext quotes
+			  - language detection will almost certainly be incorrect
+			  - even if it's code and the language is detected, the user will
+			  be able to mentally format small amounts of code themselves
+
+			  feel free to change the threshold number
+			*/
+				&& this.element.textContent.length > 80
+		)
+		if (shouldHighlight) {
+			lazyLoad("https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10/build/highlight.min.js").then(hljs => hljs.highlightBlock(this.element))
+		}
+	}
+}
+
+module.exports = {HighlightedCode}
diff --git a/src/js/events/encrypted.js b/src/js/events/encrypted.js
new file mode 100644
index 0000000..efc3ccf
--- /dev/null
+++ b/src/js/events/encrypted.js
@@ -0,0 +1,18 @@
+const {GroupableEvent} = require("./event")
+const {ejs} = require("../basic")
+
+class EncryptedMessage extends GroupableEvent {
+	render() {
+		this.clearChildren()
+		this.child(
+			ejs("i").text("Carbon cannot render encrypted messages yet")
+		)
+		super.render()
+	}
+
+	static canRender(eventData) {
+		return eventData.type === "m.room.encrypted"
+	}
+}
+
+module.exports = [EncryptedMessage]
diff --git a/src/js/events/event.js b/src/js/events/event.js
new file mode 100644
index 0000000..040d94e
--- /dev/null
+++ b/src/js/events/event.js
@@ -0,0 +1,72 @@
+const {ElemJS, ejs} = require("../basic")
+const {dateFormatter} = require("../date-formatter")
+const {SubscribeSet} = require("../store/subscribe_set.js")
+
+class MatrixEvent extends ElemJS {
+	constructor(data) {
+		super("div")
+		this.data = null
+		this.group = null
+		this.editedAt = null
+		this.readBy = new SubscribeSet()
+		this.update(data)
+	}
+
+	// predicates
+
+	canGroup() {
+		return false
+	}
+
+	// operations
+
+	setGroup(group) {
+		this.group = group
+	}
+
+	setEdited(time) {
+		this.editedAt = time
+		this.render()
+	}
+
+	update(data) {
+		this.data = data
+		this.render()
+	}
+
+	removeEvent() {
+		if (this.group) this.group.removeEvent(this)
+		else this.remove()
+	}
+
+	render() {
+		this.element.classList[this.data.pending ? "add" : "remove"]("c-message--pending")
+		if (this.editedAt) {
+			this.child(ejs("span").class("c-message__edited").text("(edited)").attribute("title", "at " + dateFormatter.format(this.editedAt)))
+		}
+		return this
+	}
+
+	static canRender(eventData) {
+		return false
+	}
+}
+
+class GroupableEvent extends MatrixEvent {
+	constructor(data) {
+		super(data)
+		this.class("c-message")
+	}
+
+	canGroup() {
+		return true
+	}
+}
+
+class UngroupableEvent extends MatrixEvent {
+}
+
+module.exports = {
+	GroupableEvent,
+	UngroupableEvent
+}
diff --git a/src/js/events/hidden.js b/src/js/events/hidden.js
new file mode 100644
index 0000000..372bb72
--- /dev/null
+++ b/src/js/events/hidden.js
@@ -0,0 +1,18 @@
+const {UngroupableEvent} = require("./event")
+
+class HiddenEvent extends UngroupableEvent {
+	constructor(data) {
+		super(data)
+		this.class("c-hidden-event")
+		this.clearChildren()
+	}
+
+	static canRender(eventData) {
+		return ["m.reaction", "m.call.candidates"].includes(eventData.type)
+	}
+
+	render() {
+	}
+}
+
+module.exports = [HiddenEvent]
diff --git a/src/js/events/image.js b/src/js/events/image.js
new file mode 100644
index 0000000..1a32bcf
--- /dev/null
+++ b/src/js/events/image.js
@@ -0,0 +1,48 @@
+const {ejs, ElemJS} = require("../basic")
+const {resolveMxc} = require("../functions")
+const {GroupableEvent} = require("./event")
+
+class Image extends GroupableEvent {
+	render() {
+		this.clearChildren()
+		this.class("c-message--media")
+		const image = (
+			ejs("img")
+				.class("c-message__image")
+				.attribute("src", resolveMxc(this.data.content.url))
+		)
+		const info = this.data.content.info
+		if (info && info.w && info.h) {
+			image.attribute("width", info.w)
+			image.attribute("height", info.h)
+		}
+		const wrapper = ejs("div").class("c-media__wrapper").child(
+			image
+		)
+		if (this.data.content.body && this.data.content.body.startsWith("SPOILER")) {
+			wrapper.attribute("tabindex", 0)
+			wrapper.class("c-media--spoiler")
+			const wall = ejs("div").class("c-media__spoiler").text("Spoiler")
+			wrapper.child(wall)
+			const toggle = () => {
+				wrapper.element.classList.toggle("c-media--shown")
+			}
+			wrapper.on("click", toggle)
+			wrapper.on("keydown", event => {
+				if (event.key === "Enter") toggle()
+			})
+		}
+		this.child(wrapper)
+		super.render()
+	}
+
+	static canRender(event) {
+		return event.type === "m.room.message" && event.content.msgtype === "m.image"
+	}
+
+	canGroup() {
+		return true
+	}
+}
+
+module.exports = [Image]
diff --git a/src/js/events/membership.js b/src/js/events/membership.js
new file mode 100644
index 0000000..5617d3e
--- /dev/null
+++ b/src/js/events/membership.js
@@ -0,0 +1,127 @@
+const {UngroupableEvent} = require("./event")
+const {ejs} = require("../basic")
+const {extractDisplayName, resolveMxc, extractLocalpart} = require("../functions")
+
+class MembershipEvent extends UngroupableEvent {
+	constructor(data) {
+		super(data)
+		this.class("c-message-event")
+		this.senderName = extractDisplayName(data)
+		if (data.content.avatar_url) {
+			this.smallAvatar = ejs("img")
+				.attribute("width", "32")
+				.attribute("height", "32")
+				.attribute("src", resolveMxc(data.content.avatar_url, 32, "crop"))
+				.class("c-message-event__avatar")
+		} else {
+			this.smallAvatar = ""
+		}
+		this.render()
+	}
+
+	static canRender(eventData) {
+		return eventData.type === "m.room.member"
+	}
+
+	renderInner(iconURL, elements) {
+		this.clearChildren()
+		this.child(
+			ejs("div").class("c-message-event__inner").child(
+				iconURL ? ejs("img").class("c-message-event__icon").attribute("width", "20").attribute("height", "20").attribute("src", iconURL) : "",
+				...elements
+			)
+		)
+		super.render()
+	}
+}
+
+
+class JoinedEvent extends MembershipEvent {
+	static canRender(eventData) {
+		return super.canRender(eventData) && eventData.content.membership === "join"
+	}
+
+	render() {
+		const changes = []
+		const prev = this.data.unsigned.prev_content
+		if (prev && prev.membership === "join") {
+			if (prev.avatar_url !== this.data.content.avatar_url) {
+				changes.push("changed their avatar")
+			}
+			if (prev.displayname !== this.data.content.displayname) {
+				changes.push(`changed their display name (was ${this.data.unsigned.prev_content.displayname})`)
+			}
+		}
+		let message
+		let iconURL
+		if (changes.length) {
+			message = " " + changes.join(", ")
+			iconURL = "static/profile-event.svg"
+		} else {
+			message = " joined the room"
+			iconURL = "static/join-event.svg"
+		}
+		this.renderInner(iconURL, [
+			this.smallAvatar,
+			this.senderName,
+			message
+		])
+	}
+}
+
+class InvitedEvent extends MembershipEvent {
+	static canRender(eventData) {
+		return super.canRender(eventData) && eventData.content.membership === "invite"
+	}
+
+	render() {
+		this.renderInner("static/invite-event.svg", [
+			this.smallAvatar,
+			`${extractLocalpart(this.data.sender)} invited ${this.data.state_key}` // full mxid for clarity
+		])
+	}
+}
+
+class LeaveEvent extends MembershipEvent {
+	static canRender(eventData) {
+		return super.canRender(eventData) && eventData.content.membership === "leave"
+	}
+
+	render() {
+		this.renderInner("static/leave-event.svg", [
+			this.smallAvatar,
+			this.senderName,
+			" left the room"
+		])
+	}
+}
+
+class BanEvent extends MembershipEvent {
+	static canRender(eventData) {
+		return super.canRender(eventData) && eventData.content.membership === "ban"
+	}
+
+	render() {
+		let message =
+			 ` left (banned by ${this.data.sender}`
+			 + (this.data.content.reason ? `, reason: ${this.data.content.reason}` : "")
+			 + ")"
+		this.renderInner("static/leave-event.svg", [
+			this.smallAvatar,
+			this.senderName,
+			message
+		])
+	}
+}
+
+class UnknownMembership extends MembershipEvent {
+	render() {
+		this.renderInner("", [
+			this.smallAvatar,
+			this.senderName,
+			ejs("i").text(" unknown membership event")
+		])
+	}
+}
+
+module.exports = [JoinedEvent, InvitedEvent, LeaveEvent, BanEvent, UnknownMembership]
diff --git a/src/js/events/message.js b/src/js/events/message.js
new file mode 100644
index 0000000..ad885ff
--- /dev/null
+++ b/src/js/events/message.js
@@ -0,0 +1,164 @@
+const {ejs, ElemJS} = require("../basic")
+const {HighlightedCode} = require("./components")
+const DOMPurify = require("dompurify")
+const {resolveMxc} = require("../functions")
+const {GroupableEvent} = require("./event")
+
+const purifier = DOMPurify()
+
+purifier.addHook("uponSanitizeAttribute", (node, hookevent, config) => {
+	// If purifier already rejected an attribute there is no point in checking it
+	if (hookevent.keepAttr === false) return;
+
+	const allowedElementAttributes = {
+		"FONT": ["data-mx-bg-color", "data-mx-color", "color"],
+		"SPAN": ["data-mx-bg-color", "data-mx-color", "data-mx-spoiler"],
+		"A": ["name", "target", "href"],
+		"IMG": ["width", "height", "alt", "title", "src", "data-mx-emoticon"],
+		"OL": ["start"],
+		"CODE": ["class"],
+	}
+
+	const allowedAttributes = allowedElementAttributes[node.tagName] || []
+	hookevent.keepAttr = allowedAttributes.indexOf(hookevent.attrName) > -1
+})
+
+purifier.addHook("uponSanitizeElement", (node, hookevent, config) => {
+	// Remove bad classes from our code element
+	if (node.tagName === "CODE") {
+		node.classList.forEach(c => {
+			if (!c.startsWith("language-")) {
+				node.classList.remove(c)
+			}
+		})
+	}
+	if (node.tagName === "A") {
+		node.setAttribute("rel", "noopener") // prevent the opening page from accessing carbon
+		node.setAttribute("target", "_blank") // open in a new tab instead of replacing carbon
+	}
+	return node
+})
+
+function cleanHTML(html) {
+	const config = {
+		ALLOWED_TAGS: [
+			"font", "del", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p",
+			"a", "ul", "ol", "sup", "sub", "li", "b", "i", "u", "strong", "em",
+			"strike", "code", "hr", "br", "div", "table", "thead", "tbody", "tr",
+			"th", "td", "caption", "pre", "span", "img",
+			// matrix tags
+			"mx-reply"
+		],
+
+		// In case we mess up in the uponSanitizeAttribute hook
+		ALLOWED_ATTR: [
+			"color", "name", "target", "href", "width", "height", "alt", "title",
+			"src", "start", "class", "noreferrer", "noopener",
+			// matrix attrs
+			"data-mx-emoticon", "data-mx-bg-color", "data-mx-color", "data-mx-spoiler"
+		],
+
+		// Return a DOM fragment instead of a string, avoids potential future mutation XSS
+		// should also be faster than the browser parsing HTML twice
+		// https://research.securitum.com/mutation-xss-via-mathml-mutation-dompurify-2-0-17-bypass/
+		RETURN_DOM_FRAGMENT: true,
+		RETURN_DOM_IMPORT: true
+	}
+	return purifier.sanitize(html, config)
+}
+
+// Here we put all the processing of the messages that isn't as likely to potentially lead to security issues
+function postProcessElements(element) {
+	element.querySelectorAll("pre").forEach(n => {
+		new HighlightedCode(n)
+	})
+
+	element.querySelectorAll("img").forEach(n => {
+		let src = n.getAttribute("src")
+		if (src) src = resolveMxc(src)
+		n.setAttribute("src", src)
+	})
+
+	element.querySelectorAll("font, span").forEach(n => {
+		const color = n.getAttribute("data-mx-color") || n.getAttribute("color")
+		const bgColor = n.getAttribute("data-mx-bg-color")
+		if (color) n.style.color = color
+		if (bgColor) n.style.backgroundColor = bgColor
+	})
+
+	element.querySelectorAll("[data-mx-spoiler]").forEach(spoiler => {
+		spoiler.classList.add("mx-spoiler")
+		spoiler.setAttribute("tabindex", 0)
+		function toggle() {
+			spoiler.classList.toggle("mx-spoiler--shown")
+		}
+		spoiler.addEventListener("click", toggle)
+		spoiler.addEventListener("keydown", event => {
+			if (event.key === "Enter") toggle()
+		})
+	})
+}
+
+
+class HTMLMessage extends GroupableEvent {
+	render() {
+		this.clearChildren()
+
+		let html = this.data.content.formatted_body
+
+		const fragment = cleanHTML(html)
+		postProcessElements(fragment)
+
+		this.child(ejs(fragment))
+
+		super.render()
+	}
+
+	static canRender(event) {
+		const content = event.content
+		return (
+			event.type === "m.room.message"
+				&& (content.msgtype === "m.text" || content.msgtype === "m.notice")
+				&& content.format === "org.matrix.custom.html"
+				&& content.formatted_body
+		)
+	}
+}
+
+function autoLinkText(text) {
+	const fragment = ejs(new DocumentFragment())
+	let lastIndex = 0
+	text.replace(/https?:\/\/(?:[A-Za-z-]+\.)+[A-Za-z]{1,10}(?::[0-9]{1,6})?(?:\/[^ ]*)?/g, (url, index) => {
+		// add text before URL
+		fragment.addText(text.slice(lastIndex, index))
+		// add URL
+		fragment.child(
+			ejs("a")
+				.attribute("target", "_blank")
+				.attribute("noopener", "")
+				.attribute("href", url)
+				.addText(url)
+		)
+		// update state
+		lastIndex = index + url.length
+	})
+	// add final text
+	fragment.addText(text.slice(lastIndex))
+	return fragment
+}
+
+class TextMessage extends GroupableEvent {
+	render() {
+		this.clearChildren()
+		this.class("c-message--plain")
+		const fragment = autoLinkText(this.data.content.body)
+		this.child(fragment)
+		super.render()
+	}
+
+	static canRender(event) {
+		return event.type === "m.room.message"
+	}
+}
+
+module.exports = [HTMLMessage, TextMessage]
diff --git a/src/js/events/render-event.js b/src/js/events/render-event.js
new file mode 100644
index 0000000..620da3e
--- /dev/null
+++ b/src/js/events/render-event.js
@@ -0,0 +1,24 @@
+const imageEvent = require("./image")
+const messageEvent = require("./message")
+const encryptedEvent = require("./encrypted")
+const membershipEvent = require("./membership")
+const unknownEvent = require("./unknown")
+const callEvent = require("./call")
+const hiddenEvent = require("./hidden")
+
+const events = [
+	...imageEvent,
+	...messageEvent,
+	...encryptedEvent,
+	...membershipEvent,
+	...callEvent,
+	...hiddenEvent,
+	...unknownEvent,
+]
+
+function renderEvent(eventData) {
+	const constructor = events.find(e => e.canRender(eventData))
+	return new constructor(eventData)
+}
+
+module.exports = {renderEvent}
diff --git a/src/js/events/unknown.js b/src/js/events/unknown.js
new file mode 100644
index 0000000..5133aa8
--- /dev/null
+++ b/src/js/events/unknown.js
@@ -0,0 +1,19 @@
+const {GroupableEvent} = require("./event")
+const {ejs} = require("../basic")
+
+class UnknownEvent extends GroupableEvent {
+	static canRender() {
+		return true
+	}
+
+	render() {
+		this.clearChildren()
+		this.child(
+			ejs("i").text(`Unknown event of type ${this.data.type}`)
+		)
+		super.render()
+	}
+}
+
+module.exports = [UnknownEvent]
+
diff --git a/src/js/focus.js b/src/js/focus.js
new file mode 100644
index 0000000..2413484
--- /dev/null
+++ b/src/js/focus.js
@@ -0,0 +1,11 @@
+document.body.classList.remove("show-focus")
+
+document.addEventListener("mousedown", () => {
+	document.body.classList.remove("show-focus")
+})
+
+document.addEventListener("keydown", event => {
+	if (event.key === "Tab") {
+		document.body.classList.add("show-focus")
+	}
+})
diff --git a/src/js/functions.js b/src/js/functions.js
index 299d8a9..3c346c4 100644
--- a/src/js/functions.js
+++ b/src/js/functions.js
@@ -1,7 +1,10 @@
-import * as lsm from $to_relative "/js/lsm.js"
+const lsm = require("./lsm.js")
 
 function resolveMxc(url, size, method) {
-	const [server, id] = url.match(/^mxc:\/\/([^/]+)\/(.*)/).slice(1)
+	const match = url.match(/^mxc:\/\/([^/]+)\/(.*)/)
+	if (!match) return url
+	let [server, id] = match.slice(1)
+	id = id.replace(/#.*$/, "")
 	if (size && method) {
 		return `${lsm.get("domain")}/_matrix/media/r0/thumbnail/${server}/${id}?width=${size}&height=${size}&method=${method}`
 	} else {
@@ -9,4 +12,28 @@ function resolveMxc(url, size, method) {
 	}
 }
 
-export {resolveMxc}
+function extractLocalpart(mxid) {
+	// try to extract the localpart from the mxid
+	let match = mxid.match(/^@([^:]+):/)
+	if (match) {
+		return match[1]
+	}
+	// localpart extraction failed, use the whole mxid
+	return mxid
+}
+
+function extractDisplayName(stateEvent) {
+	const mxid = stateEvent.state_key
+	// see if a display name is set
+	if (stateEvent.content.displayname) {
+		return stateEvent.content.displayname
+	}
+	// fall back to the mxid
+	return extractLocalpart(mxid)
+}
+
+module.exports = {
+	resolveMxc,
+	extractLocalpart,
+	extractDisplayName
+}
diff --git a/src/js/groups.js b/src/js/groups.js
index e49ade4..38b6706 100644
--- a/src/js/groups.js
+++ b/src/js/groups.js
@@ -1,4 +1,4 @@
-import {q} from $to_relative "/js/basic.js"
+const {q} = require("./basic.js")
 
 let state = "CLOSED"
 
diff --git a/src/js/lazy-load-module.js b/src/js/lazy-load-module.js
new file mode 100644
index 0000000..efe642d
--- /dev/null
+++ b/src/js/lazy-load-module.js
@@ -0,0 +1,20 @@
+// I hate this with passion
+async function lazyLoad(url) {
+	const cache = window.lazyLoadCache || new Map()
+	window.lazyLoadCache = cache
+	if (cache.get(url)) return cache.get(url)
+
+	const module = loadModuleWithoutCache(url)
+	cache.set(url, module)
+	return module
+}
+
+// Loads the module without caching
+async function loadModuleWithoutCache(url) {
+	const src = await fetch(url).then(r => r.text())
+	let module = {}
+	eval(src)
+	return module.exports
+}
+
+module.exports = {lazyLoad}
diff --git a/src/js/login.js b/src/js/login.js
new file mode 100644
index 0000000..4ae9eae
--- /dev/null
+++ b/src/js/login.js
@@ -0,0 +1,167 @@
+const {q, ElemJS, ejs} = require("./basic.js")
+
+const password = q("#password")
+const homeserver = q("#homeserver")
+
+class Username extends ElemJS {
+	constructor() {
+		super(q("#username"))
+
+		this.on("change", this.updateServer.bind(this))
+	}
+
+	isValid() {
+		return !!this.element.value.match(/^@?[a-z0-9._=\/-]+(?::[a-zA-Z0-9.:\[\]-]+)?$/)
+	}
+
+	getUsername() {
+		return this.element.value.match(/^@?([a-z0-9._=\/-]+)/)[1]
+	}
+
+	getServer() {
+		const server = this.element.value.match(/^@?[a-z0-9._=\?-]+:([a-zA-Z0-9.:\[\]-]+)$/)
+		if (server && server[1]) return server[1]
+		else return null
+	}
+
+	updateServer() {
+		if (!this.isValid()) return
+		if (this.getServer()) homeserver.value = this.getServer()
+	}
+}
+
+const username = new Username()
+
+class Feedback extends ElemJS {
+	constructor() {
+		super(q("#feedback"))
+		this.loading = false
+		this.loadingIcon = ejs("span").class("loading-icon")
+		this.messageSpan = ejs("span")
+		this.child(this.messageSpan)
+	}
+
+	setLoading(state) {
+		if (this.loading && !state) {
+			this.loadingIcon.remove()
+		} else if (!this.loading && state) {
+			this.childAt(0, this.loadingIcon)
+		}
+		this.loading = state
+	}
+
+	message(content, isError) {
+		this.removeClass("form-feedback")
+		this.removeClass("form-error")
+		if (content) this.class("form-feedback")
+		if(isError) this.class("form-error")
+
+		this.messageSpan.text(content)
+	}
+}
+
+const feedback = new Feedback()
+
+class Form extends ElemJS {
+	constructor() {
+		super(q("#form"))
+
+		this.processing = false
+
+		this.on("submit", this.submit.bind(this))
+	}
+
+	async submit() {
+		if (this.processing) return
+		this.processing = true
+		if (!username.isValid()) return this.cancel("Username is not valid.")
+
+		// Resolve homeserver address
+		let domain
+		try {
+			domain = await this.findHomeserver(homeserver.value)
+		} catch(e) {
+			return this.cancel(e.message)
+		}
+
+		// Request access token
+		this.status("Logging in...")
+		const root = await fetch(`${domain}/_matrix/client/r0/login`, {
+			method: "POST",
+			body: JSON.stringify({
+				type: "m.login.password",
+				user: username.getUsername(),
+				password: password.value
+			})
+		}).then(res => res.json())
+
+		if (!root.access_token) {
+			if (root.error) {
+				this.cancel(`Server said: ${root.error}`)
+			} else {
+				this.cancel("Login mysteriously failed.")
+				console.error(root)
+			}
+			return
+		}
+
+		localStorage.setItem("mx_user_id", root.user_id)
+		localStorage.setItem("domain", domain)
+		localStorage.setItem("access_token", root.access_token)
+
+		location.assign("../")
+	}
+
+	async findHomeserver(address, maxDepth = 5) {
+
+		//Protects from servers sending us on a redirect loop
+		maxDepth--
+		if (maxDepth <= 0) throw new Error(`Failed to look up homeserver, maximum search depth reached`)
+	
+		//Normalise the address
+		if (!address.match(/^https?:\/\//)) {
+			console.warn(`${address} doesn't specify the protocol, assuming https`)
+			address = "https://" + address
+		}
+		address = address.replace(/\/*$/, "")
+		
+		this.status(`Looking up homeserver... trying ${address}`)
+	
+		// Check if we found the actual matrix server
+		try {
+			const versionsReq = await fetch(`${address}/_matrix/client/versions`)
+			if (versionsReq.ok) {
+				const versions = await versionsReq.json()
+				if (Array.isArray(versions.versions)) return address
+			}
+		} catch(e) {}
+	
+		// Find the next matrix server in the chain
+		const root = await fetch(`${address}/.well-known/matrix/client`).then(res => res.json()).catch(e => {
+ 			console.error(e)
+			throw new Error(`Failed to look up server ${address}`)
+		})
+
+		let nextAddress = root["m.homeserver"].base_url
+		nextAddress = nextAddress.replace(/\/*$/, "")
+
+		if (address === nextAddress) {
+			throw new Error(`Failed to look up server ${address}, /.well-known/matrix/client found a redirect loop`);
+		}
+
+		return this.findHomeserver(nextAddress, maxDepth)
+	}
+
+	status(message) {
+		feedback.setLoading(true)
+		feedback.message(message)
+	}
+
+	cancel(message) {
+		this.processing = false
+		feedback.setLoading(false)
+		feedback.message(message, true)
+	}
+}
+
+const form = new Form()
diff --git a/src/js/lsm.js b/src/js/lsm.js
index 7338343..7e9ad4d 100644
--- a/src/js/lsm.js
+++ b/src/js/lsm.js
@@ -8,4 +8,4 @@ function set(name, value) {
 
 window.lsm = {get, set}
 
-export {get, set}
+module.exports = {get, set}
diff --git a/src/js/main.js b/src/js/main.js
new file mode 100644
index 0000000..1bc0be0
--- /dev/null
+++ b/src/js/main.js
@@ -0,0 +1,11 @@
+require("./focus.js")
+const groups = require("./groups.js")
+const chat_input = require("./chat-input.js")
+const room_picker = require("./room-picker.js")
+const sync = require("./sync/sync.js")
+const chat = require("./chat.js")
+require("./typing.js")
+
+if (!localStorage.getItem("access_token")) {
+	location.assign("./login/")
+}
diff --git a/src/js/read-marker.js b/src/js/read-marker.js
new file mode 100644
index 0000000..53b4658
--- /dev/null
+++ b/src/js/read-marker.js
@@ -0,0 +1,149 @@
+const {ElemJS, ejs, q} = require("./basic.js")
+const {store} = require("./store/store.js")
+const lsm = require("./lsm.js")
+
+function markFullyRead(roomID, eventID) {
+	return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${roomID}/read_markers?access_token=${lsm.get("access_token")}`, {
+		method: "POST",
+		body: JSON.stringify({
+			"m.fully_read": eventID,
+			"m.read": eventID
+		})
+	})
+}
+
+class ReadBanner extends ElemJS {
+	constructor() {
+		super(q("#c-chat-banner"))
+
+		this.newMessages = ejs("span")
+		this.child(
+			ejs("div").class("c-chat-banner__inner").child(
+				ejs("button").class("c-chat-banner__part").on("click", this.jumpTo.bind(this)).child(
+					ejs("div").class("c-chat-banner__part-inner")
+						.child(this.newMessages)
+						.addText(" new messages")
+				),
+				ejs("button").class("c-chat-banner__part", "c-chat-banner__last").on("click", this.markRead.bind(this)).child(
+					ejs("div").class("c-chat-banner__part-inner").text("Mark as read")
+				)
+			)
+		)
+
+		store.activeRoom.subscribe("changeSelf", this.render.bind(this))
+		store.notificationsChange.subscribe("changeSelf", this.render.bind(this))
+		this.render()
+	}
+
+	async jumpTo() {
+		if (!store.activeRoom.exists()) return
+		const timeline = store.activeRoom.value().timeline
+		const readMarker = timeline.readMarker
+		while (true) {
+			if (readMarker.attached) {
+				readMarker.element.scrollIntoView({behavior: "smooth", block: "center"})
+				return
+			} else {
+				q("#c-chat-messages").scrollTo({
+					top: 0,
+					left: 0,
+					behavior: "smooth"
+				})
+				await new Promise(resolve => {
+					const unsubscribe = timeline.subscribe("afterScrollbackLoad", () => {
+						unsubscribe()
+						resolve()
+					})
+				})
+			}
+		}
+	}
+
+	markRead() {
+		if (!store.activeRoom.exists()) return
+		const timeline = store.activeRoom.value().timeline
+		markFullyRead(timeline.id, timeline.latestEventID)
+	}
+
+	render() {
+		let count = 0
+		if (store.activeRoom.exists()) {
+			count = store.activeRoom.value().number.state.unreads
+		}
+		if (count !== 0) {
+			this.newMessages.text(count)
+			this.class("c-chat-banner--active")
+		} else {
+			this.removeClass("c-chat-banner--active")
+		}
+	}
+}
+const readBanner = new ReadBanner()
+
+class ReadMarker extends ElemJS {
+	constructor(timeline) {
+		super("div")
+
+		this.class("c-read-marker")
+		this.loadingIcon = ejs("div")
+			.class("c-read-marker__loading", "loading-icon")
+			.style("display", "none")
+		this.child(
+			ejs("div").class("c-read-marker__inner").child(
+				ejs("div").class("c-read-marker__text").child(this.loadingIcon).addText("New")
+			)
+		)
+
+		let processing = false
+		const observer = new IntersectionObserver(entries => {
+			const entry = entries[0]
+			if (!entry.isIntersecting) return
+			if (processing) return
+			processing = true
+			this.loadingIcon.style("display", "")
+			markFullyRead(this.timeline.id, this.timeline.latestEventID).then(() => {
+				this.loadingIcon.style("display", "none")
+				processing = false
+			})
+		}, {
+			root: document.getElementById("c-chat-messages"),
+			rootMargin: "-80px 0px 0px 0px", // marker must be this distance inside the top of the screen to be counted as read
+			threshold: 0.01
+		})
+		observer.observe(this.element)
+
+		this.attached = false
+		this.timeline = timeline
+		this.timeline.userReads.get(lsm.get("mx_user_id")).subscribe("changeSelf", (_, eventID) => {
+			// read marker updated, attach to it
+			const event = this.timeline.map.get(eventID)
+			this.attach(event)
+		})
+		this.timeline.subscribe("afterChange", () => {
+			// timeline has new events, attach to last read one
+			const eventID = this.timeline.userReads.get(lsm.get("mx_user_id")).value()
+			const event = this.timeline.map.get(eventID)
+			this.attach(event)
+		})
+	}
+
+	attach(event) {
+		if (event && event.data.origin_server_ts !== this.timeline.latest) {
+			this.class("c-read-marker--attached")
+			event.element.insertAdjacentElement("beforeend", this.element)
+			this.attached = true
+		} else {
+			this.removeClass("c-read-marker--attached")
+			this.attached = false
+		}
+		if (store.activeRoom.value() === this.timeline.room) {
+			readBanner.render()
+		}
+	}
+}
+
+module.exports = {
+	ReadMarker,
+	readBanner,
+	markFullyRead
+}
diff --git a/src/js/room-picker.js b/src/js/room-picker.js
index 8e696de..9192ae9 100644
--- a/src/js/room-picker.js
+++ b/src/js/room-picker.js
@@ -1,10 +1,10 @@
-import {q, ElemJS, ejs} from $to_relative "/js/basic.js"
-import {store} from $to_relative "/js/store/store.js"
-import {SubscribeMapList} from $to_relative "/js/store/SubscribeMapList.js"
-import {SubscribeValue} from $to_relative "/js/store/SubscribeValue.js"
-import {Timeline} from $to_relative "/js/Timeline.js"
-import * as lsm from $to_relative "/js/lsm.js"
-import {resolveMxc} from $to_relative "/js/functions.js"
+const {q, ElemJS, ejs} = require("./basic.js")
+const {store} = require("./store/store.js")
+const {SubscribeMapList} = require("./store/subscribe_map_list.js")
+const {SubscribeValue} = require("./store/subscribe_value.js")
+const {Timeline} = require("./timeline.js")
+const lsm = require("./lsm.js")
+const {resolveMxc, extractLocalpart, extractDisplayName} = require("./functions.js")
 
 class ActiveGroupMarker extends ElemJS {
 	constructor() {
@@ -25,12 +25,43 @@ class ActiveGroupMarker extends ElemJS {
 
 const activeGroupMarker = new ActiveGroupMarker()
 
+class GroupNotifier extends ElemJS {
+	constructor() {
+		super("div")
+
+		this.class("c-group__number")
+		this.state = {}
+		this.render()
+	}
+
+	update(state) {
+		Object.assign(this.state, state)
+		this.render()
+	}
+
+	clear() {
+		this.state = {}
+		this.render()
+	}
+
+	render() {
+		let total = Object.values(this.state).reduce((a, c) => a + c, 0)
+		if (total > 0) {
+			this.text(total)
+			this.class("c-group__number--active")
+		} else {
+			this.removeClass("c-group__number--active")
+		}
+	}
+}
+
 class Group extends ElemJS {
 	constructor(key, data) {
 		super("div")
 
 		this.data = data
 		this.order = this.data.order
+		this.number = new GroupNotifier()
 
 		this.class("c-group")
 		this.child(
@@ -38,6 +69,7 @@ class Group extends ElemJS {
 			 ? ejs("img").class("c-group__icon").attribute("src", this.data.icon)
 			 : ejs("div").class("c-group__icon")
 			),
+			this.number,
 			ejs("div").class("c-group__name").text(this.data.name)
 		)
 
@@ -56,12 +88,73 @@ class Group extends ElemJS {
 	}
 }
 
+class RoomNotifier extends ElemJS {
+	constructor(room) {
+		super("div")
+
+		this.class("c-room__number")
+
+		this.room = room
+		this.classes = [
+			"notifications",
+			"unreads",
+			"none"
+		]
+		this.state = {
+			notifications: 0,
+			unreads: 0
+		}
+		this.render()
+	}
+
+	/**
+	 * @param {object} state
+	 * @param {number} [state.notifications]
+	 * @param {number} [state.unreads]
+	 */
+	update(state) {
+		Object.assign(this.state, state)
+		this.informGroup()
+		this.render()
+	}
+
+	informGroup() {
+		this.room.getGroup().number.update({[this.room.id]: (
+			this.state.notifications || (this.state.unreads ? 1 : 0)
+		)})
+	}
+
+	render() {
+		const display = {
+			number: this.state.notifications || this.state.unreads,
+			kind: this.state.notifications ? "notifications" : "unreads"
+		}
+		// set number
+		if (display.number) {
+			this.text(display.number)
+		} else {
+			this.text("")
+			display.kind = "none"
+		}
+		// set class
+		this.classes.forEach(c => {
+			const name = "c-room__number--" + c
+			if (c === display.kind) {
+				this.class(name)
+			} else {
+				this.removeClass(name)
+			}
+		})
+	}
+}
+
 class Room extends ElemJS {
 	constructor(id, data) {
 		super("div")
 
 		this.id = id
 		this.data = data
+		this.number = new RoomNotifier(this)
 		this.timeline = new Timeline(this)
 		this.group = null
 		this.members = new SubscribeMapList(SubscribeValue)
@@ -75,43 +168,80 @@ class Room extends ElemJS {
 	}
 
 	get order() {
-		if (this.group) {
-			let chars = 36
-			let total = 0
-			const name = this.getName()
-			for (let i = 0; i < name.length; i++) {
-				const c = name[i]
-				let d = 0
-				if (c >= "A" && c <= "Z") d = c.charCodeAt(0) - 65 + 10
-				else if (c >= "a" && c <= "z") d = c.charCodeAt(0) - 97 + 10
-				else if (c >= "0" && c <= "9") d = +c
-				total += d * chars ** (-i)
-			}
-			return total
+		let string = ""
+		if (this.number.state.notifications) {
+			string += "N"
+		} else if (this.number.state.unreads) {
+			string += "U"
 		} else {
-			return -this.timeline.latest
+			string += "_"
+		}
+		if (this.group) {
+			string += this.name
+		} else {
+			string += (4000000000000 - this.timeline.latest) // good until 2065 :)
+		}
+		return string
+	}
+
+	getMemberName(mxid) {
+		if (this.members.has(mxid)) {
+			const state = this.members.get(mxid).value()
+			return extractDisplayName(state)
+		} else {
+			return extractLocalpart(mxid)
+		}
+	}
+
+	getHeroes() {
+		if (this.data.summary) {
+			return this.data.summary["m.heroes"]
+		} else {
+			const me = lsm.get("mx_user_id")
+			return this.data.state.events.filter(e => e.type === "m.room.member" && e.content.membership === "join" && e.state_key !== me).map(e => e.state_key)
 		}
 	}
 
 	getName() {
+		// if the room has a name
 		let name = this.data.state.events.find(e => e.type === "m.room.name")
-		if (name) {
-			name = name.content.name
-		} else {
-			const users = this.data.summary["m.heroes"]
-			const usernames = users.map(u => (u.match(/^@([^:]+):/) || [])[1] || u)
-			name = usernames.join(", ")
+		if (name && name.content.name) {
+			return name.content.name
 		}
-		return name
+		// if the room has no name, use its canonical alias
+		let canonicalAlias = this.data.state.events.find(e => e.type === "m.room.canonical_alias")
+		if (canonicalAlias && canonicalAlias.content.alias) {
+			return canonicalAlias.content.alias
+		}
+		// if the room has no alias, use the names of its members ("heroes")
+		const users = this.getHeroes()
+		if (users && users.length) {
+			const usernames = users.map(mxid => this.getMemberName(mxid))
+			return usernames.join(", ")
+		}
+		// the room is empty
+		return "Empty room"
 	}
 
 	getIcon() {
+		// if the room has a normal avatar
 		const avatar = this.data.state.events.find(e => e.type === "m.room.avatar")
 		if (avatar) {
-			return resolveMxc(avatar.content.url || avatar.content.avatar_url, 32, "crop")
-		} else {
-			return null
+			const url = avatar.content.url || avatar.content.avatar_url
+			if (url) {
+				return resolveMxc(url, 32, "crop")
+			}
 		}
+		// if the room has no avatar set, use a member's avatar
+		const users = this.getHeroes()
+		if (users && users[0] && this.members.has(users[0])) {
+			// console.log(users[0], this.members.get(users[0]))
+			const userAvatar = this.members.get(users[0]).value().content.avatar_url
+			if (userAvatar) {
+				return resolveMxc(userAvatar, 32, "crop")
+			}
+		}
+		return null
 	}
 
 	isDirect() {
@@ -144,6 +274,7 @@ class Room extends ElemJS {
 			this.child(ejs("div").class("c-room__icon", "c-room__icon--no-icon"))
 		}
 		this.child(ejs("div").class("c-room__name").text(this.getName()))
+		this.child(this.number)
 		// active
 		const active = store.activeRoom.value() === this
 		this.element.classList[active ? "add" : "remove"]("c-room--active")
@@ -163,6 +294,7 @@ class Rooms extends ElemJS {
 		store.activeGroup.subscribe("changeSelf", this.render.bind(this))
 		store.directs.subscribe("changeItem", this.render.bind(this))
 		store.newEvents.subscribe("changeSelf", this.sort.bind(this))
+		store.notificationsChange.subscribe("changeSelf", this.sort.bind(this))
 
 		this.render()
 	}
@@ -223,8 +355,12 @@ class Groups extends ElemJS {
 	render() {
 		this.clearChildren()
 		store.groups.forEach((key, item) => {
+			item.value().number.clear()
 			this.child(item.value())
 		})
+		store.rooms.forEach((id, room) => {
+			room.value().number.informGroup() // update group notification number
+		})
 	}
 }
 const groups = new Groups()
diff --git a/src/js/sender.js b/src/js/sender.js
new file mode 100644
index 0000000..55943dc
--- /dev/null
+++ b/src/js/sender.js
@@ -0,0 +1,120 @@
+const {ElemJS, ejs} = require("./basic.js")
+const {store} = require("./store/store.js")
+const {resolveMxc} = require("./functions.js")
+
+function nameToColor(str) {
+	// code from element's react sdk
+	const colors = ["#55a7f0", "#da55ff", "#1bc47c", "#ea657e", "#fd8637", "#22cec6", "#8c8de3", "#71bf22"]
+	let hash = 0
+	let i
+	let chr
+	if (str.length === 0) {
+		return hash
+	}
+	for (i = 0; i < str.length; i++) {
+		chr = str.charCodeAt(i)
+		hash = ((hash << 5) - hash) + chr
+		hash |= 0
+	}
+	hash = Math.abs(hash) % 8
+	return colors[hash]
+}
+
+class Avatar extends ElemJS {
+	constructor() {
+		super("div")
+		this.class("c-message-group__avatar")
+
+		this.mxc = undefined
+		this.image = null
+
+		this.update(null)
+	}
+
+	update(mxc) {
+		if (mxc === this.mxc) return
+		this.mxc = mxc
+		this.hasImage = !!mxc
+		if (this.hasImage) {
+			const size = 96
+			const url = resolveMxc(mxc, size, "crop")
+			this.image = ejs("img").class("c-message-group__icon").attribute("src", url).attribute("width", size).attribute("height", size)
+			this.image.on("error", this.onError.bind(this))
+		}
+		this.render()
+	}
+
+	onError() {
+		this.hasImage = false
+		this.render()
+	}
+
+	render() {
+		this.clearChildren()
+		if (this.hasImage) {
+			this.child(this.image)
+		} else {
+			this.child(
+				ejs("div").class("c-message-group__icon", "c-message-group__icon--no-icon")
+			)
+		}
+	}
+}
+
+/** Must update at least once to render. */
+class Name extends ElemJS {
+	constructor() {
+		super("div")
+		this.class("c-message-group__name")
+
+		/**
+		 * Keeps track of whether we have the proper display name or not.
+		 * If we do, then we shoudn't override it with the mxid if the name becomes unavailable.
+		 */
+		this.hasName = false
+		this.name = ""
+		this.mxid = ""
+	}
+
+	update(event) {
+		this.mxid = event.state_key
+		if (event.content.displayname) {
+			this.hasName = true
+			this.name = event.content.displayname
+		} else if (!this.hasName) {
+			this.name = this.mxid
+		}
+		this.render()
+	}
+
+	render() {
+		// set text
+		this.text(this.name)
+		// set color
+		this.style("color", nameToColor(this.mxid))
+	}
+}
+
+class Sender {
+	constructor(roomID, mxid) {
+		this.sender = store.rooms.get(roomID).value().members.get(mxid)
+		this.name = new Name()
+		this.avatar = new Avatar()
+		this.sender.subscribe("changeSelf", this.update.bind(this))
+		this.update()
+	}
+
+	update() {
+		if (this.sender.exists()) {
+			// name
+			this.name.update(this.sender.value())
+
+			// avatar
+			this.avatar.update(this.sender.value().content.avatar_url)
+		}
+	}
+}
+
+module.exports = {
+	Sender
+}
diff --git a/src/js/store/SubscribeMap.js b/src/js/store/SubscribeMap.js
deleted file mode 100644
index 8b0dc0c..0000000
--- a/src/js/store/SubscribeMap.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import {Subscribable} from $to_relative "/js/store/Subscribable.js"
-import {SubscribeValue} from $to_relative "/js/store/SubscribeValue.js"
-
-class SubscribeMap extends Subscribable {
-	constructor() {
-		super()
-		Object.assign(this.events, {
-			addItem: [],
-			changeItem: [],
-			removeItem: []
-		})
-		this.map = new Map()
-	}
-
-	has(key) {
-		return this.map.has(key) && this.map.get(key).exists()
-	}
-
-	get(key) {
-		if (this.map.has(key)) {
-			return this.map.get(key)
-		} else {
-			this.map.set(key, new SubscribeValue())
-		}
-	}
-
-	set(key, value) {
-		let s
-		if (this.map.has(key)) {
-			s = this.map.get(key).set(value)
-			this.broadcast("changeItem", key)
-		} else {
-			s = new SubscribeValue().set(value)
-			this.map.set(key, s)
-			this.broadcast("addItem", key)
-		}
-		return s
-	}
-}
-
-export {SubscribeMap}
diff --git a/src/js/store/store.js b/src/js/store/store.js
index 1c0552d..8ef4511 100644
--- a/src/js/store/store.js
+++ b/src/js/store/store.js
@@ -1,7 +1,7 @@
-import {Subscribable} from $to_relative "/js/store/Subscribable.js"
-import {SubscribeMapList} from $to_relative "/js/store/SubscribeMapList.js"
-import {SubscribeSet} from $to_relative "/js/store/SubscribeSet.js"
-import {SubscribeValue} from $to_relative "/js/store/SubscribeValue.js"
+const {Subscribable} = require("./subscribable.js")
+const {SubscribeMapList} = require("./subscribe_map_list.js")
+const {SubscribeSet} = require("./subscribe_set.js")
+const {SubscribeValue} = require("./subscribe_value.js")
 
 const store = {
 	groups: new SubscribeMapList(SubscribeValue),
@@ -9,9 +9,10 @@ const store = {
 	directs: new SubscribeSet(),
 	activeGroup: new SubscribeValue(),
 	activeRoom: new SubscribeValue(),
-	newEvents: new Subscribable()
+	newEvents: new Subscribable(),
+	notificationsChange: new Subscribable()
 }
 
 window.store = store
 
-export {store}
+module.exports = {store}
diff --git a/src/js/store/Subscribable.js b/src/js/store/subscribable.js
similarity index 86%
rename from src/js/store/Subscribable.js
rename to src/js/store/subscribable.js
index 6c7640e..56bf971 100644
--- a/src/js/store/Subscribable.js
+++ b/src/js/store/subscribable.js
@@ -20,6 +20,8 @@ class Subscribable {
 		} else {
 			throw new Error(`Cannot subscribe to non-existent event ${event}, available events are: ${Object.keys(this.events).join(", ")}`)
 		}
+		// return a function we can call to easily unsubscribe
+		return () => this.unsubscribe(event, callback)
 	}
 
 	unsubscribe(event, callback) {
@@ -35,4 +37,4 @@ class Subscribable {
 	}
 }
 
-export {Subscribable}
+module.exports = {Subscribable}
diff --git a/src/js/store/subscribe_map.js b/src/js/store/subscribe_map.js
new file mode 100644
index 0000000..6159597
--- /dev/null
+++ b/src/js/store/subscribe_map.js
@@ -0,0 +1,74 @@
+const {Subscribable} = require("./subscribable.js")
+
+class SubscribeMap extends Subscribable {
+	constructor(inner) {
+		super()
+		this.inner = inner
+		Object.assign(this.events, {
+			addItem: [],
+			editItem: [],
+			deleteItem: [],
+			changeItem: [],
+			askSet: []
+		})
+		Object.assign(this.eventDeps, {
+			addItem: ["changeItem"],
+			editItem: ["changeItem"],
+			deleteItem: ["changeItem"],
+			changeItem: [],
+			askSet: []
+		})
+		this.map = new Map()
+	}
+
+	has(key) {
+		return this.map.has(key) && this.map.get(key).exists()
+	}
+
+	get(key) {
+		if (this.map.has(key)) {
+			return this.map.get(key)
+		} else {
+			const item = new this.inner()
+			this.map.set(key, item)
+			return item
+		}
+	}
+
+	forEach(f) {
+		for (const entry of this.map.entries()) {
+			f(entry[0], entry[1])
+		}
+	}
+
+	askSet(key, value) {
+		this.broadcast("askSet", {key, value})
+	}
+
+	set(key, value) {
+		let s
+		if (this.map.has(key)) {
+			const exists = this.map.get(key).exists()
+			s = this.map.get(key).set(value)
+			if (exists) {
+				this.broadcast("editItem", key)
+			} else {
+				this.broadcast("addItem", key)
+			}
+		} else {
+			s = new this.inner().set(value)
+			this.map.set(key, s)
+			this.broadcast("addItem", key)
+		}
+		return s
+	}
+
+	delete(key) {
+		if (this.backing.has(key)) {
+			this.backing.delete(key)
+			this.broadcast("deleteItem", key)
+		}
+	}
+}
+
+module.exports = {SubscribeMap}
diff --git a/src/js/store/SubscribeMapList.js b/src/js/store/subscribe_map_list.js
similarity index 72%
rename from src/js/store/SubscribeMapList.js
rename to src/js/store/subscribe_map_list.js
index 1883303..794a8da 100644
--- a/src/js/store/SubscribeMapList.js
+++ b/src/js/store/subscribe_map_list.js
@@ -1,5 +1,5 @@
-import {Subscribable} from $to_relative "/js/store/Subscribable.js"
-import {SubscribeValue} from $to_relative "/js/store/SubscribeValue.js"
+const {Subscribable} = require("./subscribable.js")
+const {SubscribeValue} = require("./subscribe_value.js")
 
 class SubscribeMapList extends Subscribable {
 	constructor(inner) {
@@ -54,6 +54,15 @@ class SubscribeMapList extends Subscribable {
 	}
 
 	sort() {
+		const key = this.list[0]
+		if (typeof this.map.get(key).value().order === "number") {
+			this.sortByNumber()
+		} else {
+			this.sortByString()
+		}
+	}
+
+	sortByNumber() {
 		this.list.sort((a, b) => {
 			const orderA = this.map.get(a).value().order
 			const orderB = this.map.get(b).value().order
@@ -62,6 +71,17 @@ class SubscribeMapList extends Subscribable {
 		this.broadcast("changeItem")
 	}
 
+	sortByString() {
+		this.list.sort((a, b) => {
+			const orderA = this.map.get(a).value().order
+			const orderB = this.map.get(b).value().order
+			if (orderA < orderB) return -1
+			else if (orderA > orderB) return 1
+			else return 0
+		})
+		this.broadcast("changeItem")
+	}
+
 	_add(key, value, start) {
 		let s
 		if (this.map.has(key)) {
@@ -83,4 +103,4 @@ class SubscribeMapList extends Subscribable {
 	}
 }
 
-export {SubscribeMapList}
+module.exports = {SubscribeMapList}
diff --git a/src/js/store/SubscribeSet.js b/src/js/store/subscribe_set.js
similarity index 88%
rename from src/js/store/SubscribeSet.js
rename to src/js/store/subscribe_set.js
index 789aaaf..32c758c 100644
--- a/src/js/store/SubscribeSet.js
+++ b/src/js/store/subscribe_set.js
@@ -1,4 +1,4 @@
-import {Subscribable} from $to_relative "/js/store/Subscribable.js"
+const {Subscribable} = require("./subscribable.js")
 
 class SubscribeSet extends Subscribable {
 	constructor() {
@@ -47,4 +47,4 @@ class SubscribeSet extends Subscribable {
 	}
 }
 
-export {SubscribeSet}
+module.exports = {SubscribeSet}
diff --git a/src/js/store/SubscribeValue.js b/src/js/store/subscribe_value.js
similarity index 85%
rename from src/js/store/SubscribeValue.js
rename to src/js/store/subscribe_value.js
index 6657e27..9c71959 100644
--- a/src/js/store/SubscribeValue.js
+++ b/src/js/store/subscribe_value.js
@@ -1,4 +1,4 @@
-import {Subscribable} from $to_relative "/js/store/Subscribable.js"
+const {Subscribable} = require("./subscribable.js")
 
 class SubscribeValue extends Subscribable {
 	constructor() {
@@ -30,7 +30,7 @@ class SubscribeValue extends Subscribable {
 
 	edit(f) {
 		if (this.exists()) {
-			f(this.data)
+			this.data = f(this.data)
 			this.set(this.data)
 		} else {
 			throw new Error("Tried to edit a SubscribeValue that had no value")
@@ -44,4 +44,4 @@ class SubscribeValue extends Subscribable {
 	}
 }
 
-export {SubscribeValue}
+module.exports = {SubscribeValue}
diff --git a/src/js/sync/sync.js b/src/js/sync/sync.js
index bbd6b11..f8bfc52 100644
--- a/src/js/sync/sync.js
+++ b/src/js/sync/sync.js
@@ -1,6 +1,6 @@
-import {store} from $to_relative "/js/store/store.js"
-import * as lsm from $to_relative "/js/lsm.js"
-import {resolveMxc} from $to_relative "/js/functions.js"
+const {store} = require("../store/store.js")
+const lsm = require("../lsm.js")
+const {resolveMxc} = require("../functions.js")
 
 let lastBatch = null
 
@@ -11,7 +11,7 @@ function sync() {
 		room: {
 			// pulling more from the timeline massively increases download size
 			timeline: {
-				limit: 5
+				limit: 1
 			},
 			// members are not currently needed
 			state: {
@@ -37,63 +37,88 @@ function sync() {
 function manageSync(root) {
 	try {
 		let newEvents = false
+		let notificationsChange = false
 
 		// set up directs
-		const directs = root.account_data.events.find(e => e.type === "m.direct")
-		if (directs) {
-			Object.values(directs.content).forEach(ids => {
-				ids.forEach(id => store.directs.add(id))
-			})
+		if (root.account_data) {
+			const directs = root.account_data.events.find(e => e.type === "m.direct")
+			if (directs) {
+				Object.values(directs.content).forEach(ids => {
+					ids.forEach(id => store.directs.add(id))
+				})
+			}
 		}
 
 		// set up rooms
-		Object.entries(root.rooms.join).forEach(([id, data]) => {
-			if (!store.rooms.has(id)) {
-				store.rooms.askAdd(id, data)
+		if (root.rooms) {
+			if (root.rooms.join) {
+				Object.entries(root.rooms.join).forEach(([id, data]) => {
+					if (!store.rooms.has(id)) {
+						store.rooms.askAdd(id, data)
+					}
+					const room = store.rooms.get(id).value()
+					const timeline = room.timeline
+					if (data.state && data.state.events) timeline.updateStateEvents(data.state.events)
+					if (data.timeline && data.timeline.events) {
+						if (!timeline.from) timeline.from = data.timeline.prev_batch
+						if (data.timeline.events.length) {
+							newEvents = true
+							timeline.updateEvents(data.timeline.events)
+						}
+					}
+					if (data.ephemeral && data.ephemeral.events) timeline.updateEphemeral(data.ephemeral.events)
+					if (data.unread_notifications) {
+						timeline.updateNotificationCount(data.unread_notifications.notification_count)
+						notificationsChange = true
+					}
+					if (data["org.matrix.msc2654.unread_count"] != undefined) {
+						timeline.updateUnreadCount(data["org.matrix.msc2654.unread_count"])
+						notificationsChange = true
+					}
+				})
 			}
-			const room = store.rooms.get(id).value()
-			const timeline = room.timeline
-			if (data.timeline.events.length) newEvents = true
-			timeline.updateStateEvents(data.state.events)
-			timeline.updateEvents(data.timeline.events)
-		})
+		}
 
 		// set up groups
-		Promise.all(
-			Object.keys(root.groups.join).map(id => {
-				if (!store.groups.has(id)) {
-					return Promise.all(["profile", "rooms"].map(path => {
-						const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/groups/${id}/${path}`)
-						url.searchParams.append("access_token", lsm.get("access_token"))
-						return fetch(url.toString()).then(res => res.json())
-					})).then(([profile, rooms]) => {
-						rooms = rooms.chunk
-						let order = 999
-						let orderEvent = root.account_data.events.find(e => e.type === "im.vector.web.tag_ordering")
-						if (orderEvent) {
-							if (orderEvent.content.tags.includes(id)) {
-								order = orderEvent.content.tags.indexOf(id)
+		if (root.groups) {
+			Promise.all(
+				Object.keys(root.groups.join).map(id => {
+					if (!store.groups.has(id)) {
+						return Promise.all(["profile", "rooms"].map(path => {
+							const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/groups/${id}/${path}`)
+							url.searchParams.append("access_token", lsm.get("access_token"))
+							return fetch(url.toString()).then(res => res.json())
+						})).then(([profile, rooms]) => {
+							rooms = rooms.chunk
+							let order = 999
+							let orderEvent = root.account_data.events.find(e => e.type === "im.vector.web.tag_ordering")
+							if (orderEvent) {
+								if (orderEvent.content.tags.includes(id)) {
+									order = orderEvent.content.tags.indexOf(id)
+								}
 							}
-						}
-						const data = {
-							name: profile.name,
-							icon: resolveMxc(profile.avatar_url, 96, "crop"),
-							order
-						}
-						store.groups.askAdd(id, data)
-						rooms.forEach(groupRoom => {
-							if (store.rooms.has(groupRoom.room_id)) {
-								store.rooms.get(groupRoom.room_id).value().setGroup(id)
+							const data = {
+								name: profile.name,
+								icon: resolveMxc(profile.avatar_url, 96, "crop"),
+								order
 							}
+							store.groups.askAdd(id, data)
+							rooms.forEach(groupRoom => {
+								if (store.rooms.has(groupRoom.room_id)) {
+									store.rooms.get(groupRoom.room_id).value().setGroup(id)
+								}
+							})
+							store.newEvents.broadcast("changeSelf") // trigger a room list update
 						})
-						store.newEvents.broadcast("changeSelf") // trigger a room list update
-					})
-				}
+					}
+				})
+			).then(() => {
+				store.rooms.sort()
 			})
-		).then(() => {
-			store.rooms.sort()
-		})
+		}
+
 		if (newEvents) store.newEvents.broadcast("changeSelf")
+		if (notificationsChange) store.notificationsChange.broadcast("changeSelf")
 	} catch (e) {
 		console.error(root)
 		throw e
@@ -121,4 +146,6 @@ function syncLoop() {
 
 store.activeGroup.set(store.groups.get("directs").value())
 
-syncLoop()
+if (lsm.get("access_token")) {
+	syncLoop()
+}
diff --git a/src/js/timeline.js b/src/js/timeline.js
new file mode 100644
index 0000000..0a36d14
--- /dev/null
+++ b/src/js/timeline.js
@@ -0,0 +1,408 @@
+const {ElemJS, ejs, q} = require("./basic.js")
+const {Subscribable} = require("./store/subscribable.js")
+const {SubscribeValue} = require("./store/subscribe_value.js")
+const {SubscribeMap} = require("./store/subscribe_map.js")
+const {store} = require("./store/store.js")
+const {Anchor} = require("./anchor.js")
+const {Sender} = require("./sender.js")
+const {ReadMarker, markFullyRead} = require("./read-marker.js")
+const lsm = require("./lsm.js")
+const {resolveMxc} = require("./functions.js")
+const {renderEvent} = require("./events/render-event")
+const {dateFormatter} = require("./date-formatter")
+
+let debug = false
+
+const NO_MAX = Symbol("NO_MAX")
+
+let sentIndex = 0
+
+function getTxnId() {
+	return Date.now() + (sentIndex++)
+}
+
+function eventSearch(list, event, min = 0, max = NO_MAX) {
+	if (list.length === 0) return {success: false, i: 0}
+
+	if (max === NO_MAX) max = list.length - 1
+	let mid = Math.floor((max + min) / 2)
+	// success condition
+	if (list[mid] && list[mid].data.event_id === event.data.event_id) return {success: true, i: mid}
+	// failed condition
+	if (min >= max) {
+		while (mid !== -1 && (!list[mid] || list[mid].data.origin_server_ts > event.data.origin_server_ts)) mid--
+		return {
+			success: false,
+			i: mid + 1
+		}
+	}
+	// recurse (below)
+	if (list[mid].data.origin_server_ts > event.data.origin_server_ts) return eventSearch(list, event, min, mid - 1)
+	// recurse (above)
+	else return eventSearch(list, event, mid + 1, max)
+}
+
+class EventGroup extends ElemJS {
+	constructor(reactive, list) {
+		super("div")
+		this.class("c-message-group")
+		this.reactive = reactive
+		this.list = list
+		this.data = {
+			sender: list[0].data.sender,
+			origin_server_ts: list[0].data.origin_server_ts
+		}
+		this.sender = new Sender(this.reactive.id, this.data.sender)
+		this.child(
+			this.sender.avatar,
+			this.messages = ejs("div").class("c-message-group__messages").child(
+				ejs("div").class("c-message-group__intro").child(
+					this.sender.name,
+					ejs("div").class("c-message-group__date").text(dateFormatter.format(this.data.origin_server_ts))
+				),
+				...this.list
+			)
+		)
+	}
+
+	canGroup() {
+		if (this.list.length) return this.list[0].canGroup()
+		else return true
+	}
+
+	addEvent(event) {
+		const index = eventSearch(this.list, event).i
+		event.setGroup(this)
+		this.list.splice(index, 0, event)
+		this.messages.childAt(index + 1, event)
+	}
+
+	removeEvent(event) {
+		const search = eventSearch(this.list, event)
+		if (!search.success) throw new Error(`Event ${event.data.event_id} not found in this group`)
+		const index = search.i
+		// actually remove the event
+		this.list.splice(index, 1)
+		event.remove() // should get everything else
+		if (this.list.length === 0) this.reactive.removeGroup(this)
+	}
+}
+
+/** Displays a spinner and creates an event to notify timeline to load more messages */
+class LoadMore extends ElemJS {
+	constructor(id) {
+		super("div")
+		this.class("c-message-notice")
+		this.id = id
+
+		this.child(
+			ejs("div").class("c-message-notice__inner").child(
+				ejs("span").class("loading-icon"),
+				ejs("span").text("Loading more...")
+			)
+		)
+		const intersection_observer = new IntersectionObserver(e => this.intersectionHandler(e))
+		intersection_observer.observe(this.element)
+	}
+
+	intersectionHandler(e) {
+		if (e.some(e => e.isIntersecting)) {
+			store.rooms.get(this.id).value().timeline.loadScrollback()
+		}
+	}
+}
+
+class ReactiveTimeline extends ElemJS {
+	constructor(id, list) {
+		super("div")
+		this.class("c-event-groups")
+		this.id = id
+		this.list = list
+		this.loadMore = new LoadMore(this.id)
+		this.render()
+	}
+
+	addEvent(event) {
+		this.loadMore.remove()
+		// if (debug) console.log("running search", this.list, event)
+		// if (debug) debugger;
+		const search = eventSearch(this.list, event)
+		// console.log(search, this.list.map(l => l.data.sender), event.data)
+		if (!search.success) {
+			if (search.i >= 1) {
+				// add at end
+				this.tryAddGroups(event, [search.i - 1, search.i])
+			} else {
+				// add at start
+				this.tryAddGroups(event, [0, -1])
+			}
+		} else {
+			this.tryAddGroups(event, [search.i])
+		}
+		this.loadMore = new LoadMore(this.id)
+		this.childAt(0, this.loadMore)
+	}
+
+	tryAddGroups(event, indices) {
+		const createGroupAt = i => {
+			// if (printed++ < 100) console.log("tryadd success, created group")
+			if (i === -1) {
+				// here, -1 means at the start, before the first group
+				i = 0 // jank but it does the trick
+			}
+			if (event.canGroup()) {
+				const group = new EventGroup(this, [event])
+				this.list.splice(i, 0, group)
+				this.childAt(i, group)
+				event.setGroup(group)
+			} else {
+				this.list.splice(i, 0, event)
+				this.childAt(i, event)
+			}
+		}
+		const success = indices.some(i => {
+			if (!this.list[i]) {
+				createGroupAt(i)
+				return true
+			} else if (event.canGroup() && this.list[i] && this.list[i].canGroup() && this.list[i].data.sender === event.data.sender) {
+				// if (printed++ < 100) console.log("tryadd success, using existing group")
+				this.list[i].addEvent(event)
+				return true
+			}
+		})
+		// if (!success) console.log("tryadd failure", indices, this.list.map(l => l.data.sender), event.data) // I believe all the bugs are now fixed. Lol.
+		if (!success) createGroupAt(indices[0])
+	}
+
+	removeGroup(group) {
+		const index = this.list.indexOf(group)
+		this.list.splice(index, 1)
+		group.remove() // should get everything else
+	}
+
+	render() {
+		this.clearChildren()
+		this.child(this.loadMore)
+		this.list.forEach(group => this.child(group))
+		this.anchor = new Anchor()
+		this.child(this.anchor)
+	}
+}
+
+class Timeline extends Subscribable {
+	constructor(room) {
+		super()
+		Object.assign(this.events, {
+			beforeChange: [],
+			afterChange: [],
+			beforeScrollbackLoad: [],
+			afterScrollbackLoad: [],
+		})
+		Object.assign(this.eventDeps, {
+			beforeChange: [],
+			afterChange: [],
+			beforeScrollbackLoad: [],
+			afterScrollbackLoad: [],
+		})
+		this.room = room
+		this.id = this.room.id
+		this.list = []
+		this.map = new Map()
+		this.reactiveTimeline = new ReactiveTimeline(this.id, [])
+		this.latest = 0
+		this.latestEventID = null
+		this.pending = new Set()
+		this.pendingEdits = []
+		this.typing = new SubscribeValue().set([])
+		this.userReads = new SubscribeMap(SubscribeValue)
+		this.readMarker = new ReadMarker(this)
+		this.from = null
+	}
+
+	updateStateEvents(events) {
+		for (const eventData of events) {
+			let id = eventData.event_id
+			if (eventData.type === "m.room.member") {
+				// update members
+				if (eventData.membership !== "leave") {
+					const member = this.room.members.get(eventData.state_key)
+					// only use the latest state
+					if (!member.exists() || eventData.origin_server_ts > member.data.origin_server_ts) {
+						member.set(eventData)
+					}
+				}
+			}
+		}
+	}
+
+	updateEvents(events) {
+		this.broadcast("beforeChange")
+		// handle state events
+		this.updateStateEvents(events)
+		for (const eventData of events) {
+			// set variables
+			let id = eventData.event_id
+			if (eventData.origin_server_ts > this.latest) {
+				this.latest = eventData.origin_server_ts
+				this.latestEventID = id
+			}
+			// handle local echoes
+			if (eventData.sender === lsm.get("mx_user_id") && eventData.content && this.pending.has(eventData.content["chat.carbon.message.pending_id"])) {
+				const pendingID = eventData.content["chat.carbon.message.pending_id"]
+				if (id !== pendingID) {
+					const target = this.map.get(pendingID)
+					this.map.set(id, target)
+					this.map.delete(pendingID)
+					// update fully read marker - assume we have fully read up to messages we send
+					markFullyRead(this.id, id)
+				}
+			}
+			// handle timeline events
+			if (this.map.has(id)) {
+				// update existing event
+				this.map.get(id).update(eventData)
+			} else {
+				// skip redacted events
+				if (eventData.unsigned && eventData.unsigned.redacted_by) {
+					continue
+				}
+				// handle redactions
+				if (eventData.type === "m.room.redaction") {
+					if (this.map.has(eventData.redacts)) this.map.get(eventData.redacts).removeEvent()
+					continue
+				}
+				// handle edits
+				if (eventData.type === "m.room.message" && eventData.content["m.relates_to"] && eventData.content["m.relates_to"].rel_type === "m.replace") {
+					this.pendingEdits.push(eventData)
+					continue
+				}
+				// add new event
+				const event = renderEvent(eventData)
+				this.map.set(id, event)
+				this.reactiveTimeline.addEvent(event)
+				// update read receipt for sender on their own event
+				this.moveReadReceipt(eventData.sender, id)
+			}
+		}
+		// apply edits
+		this.pendingEdits = this.pendingEdits.filter(eventData => {
+			const replaces = eventData.content["m.relates_to"].event_id
+			if (this.map.has(replaces)) {
+				const event = this.map.get(replaces)
+				event.data.content = eventData.content["m.new_content"]
+				event.setEdited(eventData.origin_server_ts)
+				event.update(event.data)
+				return false // handled; remove from list
+			} else {
+				return true // we don't have the event it edits yet; keep in list
+			}
+		})
+		this.broadcast("afterChange")
+	}
+
+	updateEphemeral(events) {
+		for (const eventData of events) {
+			if (eventData.type === "m.typing") {
+				this.typing.set(eventData.content.user_ids)
+			}
+			if (eventData.type === "m.receipt") {
+				for (const eventID of Object.keys(eventData.content)) {
+					for (const user of Object.keys(eventData.content[eventID]["m.read"])) {
+						this.moveReadReceipt(user, eventID)
+					}
+				}
+				// console.log("Updated read receipts:", this.userReads)
+			}
+		}
+	}
+
+	moveReadReceipt(user, eventID) {
+		// check for a previous event to move from
+		const prev = this.userReads.get(user)
+		if (prev.exists()) {
+			const prevID = prev.value()
+			if (this.map.has(prevID) && this.map.has(eventID)) {
+				// ensure new message came later
+				if (this.map.get(eventID).data.origin_server_ts < this.map.get(prevID).data.origin_server_ts) return
+				this.map.get(prevID).readBy.delete(user)
+			}
+		}
+		// set on new message
+		this.userReads.set(user, eventID)
+		if (this.map.has(eventID)) this.map.get(eventID).readBy.add(user)
+	}
+
+	updateUnreadCount(count) {
+		this.room.number.update({unreads: count})
+	}
+
+	updateNotificationCount(count) {
+		this.room.number.update({notifications: count})
+	}
+
+	removeEvent(id) {
+		if (!this.map.has(id)) throw new Error(`Tried to delete event ID ${id} which does not exist`)
+		this.map.get(id).removeEvent()
+		this.map.delete(id)
+	}
+
+	getTimeline() {
+		return this.reactiveTimeline
+	}
+
+	async loadScrollback() {
+		debug = true
+		if (!this.from) return // no more scrollback for this timeline
+		const url = new URL(`${lsm.get("domain")}/_matrix/client/r0/rooms/${this.id}/messages`)
+		url.searchParams.set("access_token", lsm.get("access_token"))
+		url.searchParams.set("from", this.from)
+		url.searchParams.set("dir", "b")
+		url.searchParams.set("limit", "20")
+		const filter = {
+			lazy_load_members: true
+		}
+		url.searchParams.set("filter", JSON.stringify(filter))
+
+		const root = await fetch(url.toString()).then(res => res.json())
+
+		this.broadcast("beforeScrollbackLoad")
+
+		this.from = root.end
+		// console.log(this.updateEvents, root.chunk)
+		if (root.state) this.updateStateEvents(root.state)
+		if (root.chunk.length) {
+			// there are events to display
+			this.updateEvents(root.chunk)
+		}
+		if (!root.chunk.length || !root.end) {
+			// we reached the top of the scrollback
+			this.reactiveTimeline.loadMore.remove()
+		}
+		this.broadcast("afterScrollbackLoad")
+	}
+
+	send(type, content) {
+		const tx = getTxnId()
+		const id = `pending$${tx}`
+		this.pending.add(id)
+		content["chat.carbon.message.pending_id"] = id
+		const fakeEvent = {
+			type,
+			origin_server_ts: Date.now(),
+			event_id: id,
+			sender: lsm.get("mx_user_id"),
+			content,
+			pending: true
+		}
+		this.updateEvents([fakeEvent])
+		return fetch(`${lsm.get("domain")}/_matrix/client/r0/rooms/${this.id}/send/m.room.message/${tx}?access_token=${lsm.get("access_token")}`, {
+			method: "PUT",
+			body: JSON.stringify(content),
+			headers: {
+				"Content-Type": "application/json"
+			}
+		})
+	}
+}
+
+module.exports = {Timeline}
diff --git a/src/js/typing.js b/src/js/typing.js
new file mode 100644
index 0000000..b6208ef
--- /dev/null
+++ b/src/js/typing.js
@@ -0,0 +1,69 @@
+const {ElemJS, ejs, q} = require("./basic")
+const {store} = require("./store/store")
+const lsm = require("./lsm")
+
+/**
+ * Maximum number of typing users to display all names for.
+ * More will be shown as "X users are typing".
+ */
+const maxUsers = 4
+
+function getMemberName(mxid) {
+	return store.activeRoom.value().getMemberName(mxid)
+}
+
+class Typing extends ElemJS {
+	constructor() {
+		super(q("#c-typing"))
+
+		this.typingUnsubscribe = null
+
+		this.message = ejs("span")
+		this.child(this.message)
+
+		store.activeRoom.subscribe("changeSelf", this.changeRoom.bind(this))
+	}
+
+	changeRoom() {
+		if (this.typingUnsubscribe) {
+			this.typingUnsubscribe()
+			this.typingUnsubscribe = null
+		}
+		if (!store.activeRoom.exists()) return
+		const room = store.activeRoom.value()
+		this.typingUnsubscribe = room.timeline.typing.subscribe("changeSelf", this.render.bind(this))
+		this.render()
+	}
+
+	render() {
+		if (!store.activeRoom.exists()) return
+		const room = store.activeRoom.value()
+		let users = [...room.timeline.typing.value()]
+		// don't show own typing status
+		users = users.filter(u => u !== lsm.get("mx_user_id"))
+		if (users.length === 0) {
+			// nobody is typing
+			this.removeClass("c-typing--typing")
+		} else {
+			let message = ""
+			if (users.length === 1) {
+				message = `${getMemberName(users[0])} is typing...`
+			} else if (users.length <= maxUsers) {
+				// feel free to rewrite this loop if you know a better way
+				for (let i = 0; i < users.length; i++) {
+					if (i < users.length-1) {
+						message += `${getMemberName(users[i])}, `
+					} else {
+						message += `and ${getMemberName(users[i])} are typing...`
+					}
+				}
+			} else {
+				message = `${users.length} people are typing...`
+			}
+			this.class("c-typing--typing")
+			this.message.text(message)
+		}
+	}
+}
+
+new Typing()
diff --git a/src/login.pug b/src/login.pug
index 105b6bd..470ee82 100644
--- a/src/login.pug
+++ b/src/login.pug
@@ -1,21 +1,32 @@
 doctype html
 html
-	head
-		meta(charset="utf-8")
-		link(rel="stylesheet" type="text/css" href=getStatic("/sass/main.sass"))
-		title Carbon
-	body
-		main.main
-			form
-				div
-					label(for="login") Username
-					input(type="text" name="login" autocomplete="username" placeholder="example:matrix.org" required)#login
-				div
-					label(for="password") Password
-					input(type="text" name="password" autocomplete="current-password" required)#password
-				div
+  head
+    meta(charset="utf-8")
+    title Carbon
+    meta(name="viewport" content="width=device-width, initial-scale=1")
+    link(rel="stylesheet" type="text/css" href=getStatic("/sass/login.sass"))
+    script(type="module" src=getStatic("/js/login.js"))
+
+  body
+    main.main
+      .center-login-container
+        h1 Welcome to Carbon!
+        form.login-form(method="post" onsubmit="return false")#form
+          .data-input
+            .form-input-container
+              label(for="username") Username
+              input(type="text" name="username" autocomplete="username" placeholder="@username:server.tld" pattern="^@?[a-z0-9._=/-]+(?::[a-zA-Z0-9.:\\[\\]-]+)?$" required)#username
+
+            .form-input-container
+              label(for="password") Password
+              input(name="password" autocomplete="current-password" type="password" required)#password
+
+            .form-input-container
+              label(for="homeserver") Homeserver
+              input(type="text" name="homeserver" value="matrix.org" placeholder="matrix.org" required)#homeserver
+
+          #feedback
+
+          .form-input-container
+            input(type="submit" value="Log in")#submit
 
-					label(for="homeserver") Homeserver
-					input(type="text" name="homeserver" value="matrix.org" required)#homeserver
-				div
-					input(type="submit" value="Login")
diff --git a/src/sass/base.sass b/src/sass/base.sass
index 0232c2e..dfb9f7a 100644
--- a/src/sass/base.sass
+++ b/src/sass/base.sass
@@ -21,3 +21,39 @@ body
 .main
   height: 100vh
   display: flex
+
+button
+  appearance: none
+  border: none
+  background: none
+  color: inherit
+  font-family: inherit
+  font-size: inherit
+  font-style: inherit
+  font-weight: inherit
+  padding: 0
+  margin: 0
+  line-height: inherit
+  cursor: inherit
+
+// focus resets
+
+:focus
+  outline: none
+
+:-moz-focusring
+  outline: none
+
+::-moz-focus-inner
+  border: 0
+
+select:-moz-focusring
+  color: transparent
+  text-shadow: 0 0 0 #ddd
+
+body.show-focus
+  a, select, button, input, video, div, span
+    outline-color: #fff
+
+    &:focus
+      outline: 2px dotted
diff --git a/src/sass/colors.sass b/src/sass/colors.sass
index 278fc02..a603edd 100644
--- a/src/sass/colors.sass
+++ b/src/sass/colors.sass
@@ -5,3 +5,5 @@ $mild: #393c42
 $milder: #42454a
 $divider: #4b4e54
 $muted: #999
+$link: #57bffd
+$notify-highlight: #ffac4b
diff --git a/src/sass/components/chat-banner.sass b/src/sass/components/chat-banner.sass
new file mode 100644
index 0000000..f17acce
--- /dev/null
+++ b/src/sass/components/chat-banner.sass
@@ -0,0 +1,50 @@
+@use "../colors" as c
+
+.c-chat-banner
+  position: sticky
+  z-index: 1
+  top: 0
+  left: 0
+  right: 0
+  margin-right: 12px
+  outline-color: #000
+  opacity: 0
+  transform: translateY(-40px)
+  transition: transform 0.2s ease, opacity 0.2s ease-out
+
+  &--active
+    opacity: 1
+    transform: translateY(0px)
+
+  &__inner
+    display: grid
+    grid-template-columns: 1fr auto
+    background: c.$notify-highlight
+    color: #000
+    margin: 0px 12px
+    padding: 0px 12px
+    border-radius: 0px 0px 10px 10px
+    line-height: 1
+    box-shadow: 0px 5px 5px -2px rgba(0, 0, 0, 0.1)
+    cursor: pointer
+
+    &:hover
+      box-shadow: 0px 5px 5px -2px rgba(0, 0, 0, 0.6)
+
+  &__part
+    padding: 6px 0px 8px
+
+    &:hover
+      text-decoration: underline
+
+  &__part-inner
+    display: block
+    width: 100% // yes, really.
+    text-align: left
+
+  &__last
+    margin-left: 8px
+
+  &__last &__part-inner
+    border-left: 1px solid #222
+    padding-left: 8px
diff --git a/src/sass/components/chat-input.sass b/src/sass/components/chat-input.sass
index 892fee6..bb82dde 100644
--- a/src/sass/components/chat-input.sass
+++ b/src/sass/components/chat-input.sass
@@ -6,11 +6,14 @@
   -webkit-appearance: $value
 
 .c-chat-input
+  position: relative
   width: 100%
   border-top: 2px solid c.$divider
   background-color: c.$dark
 
   &__textarea
+    position: relative
+    z-index: 1
     width: calc(100% - 40px)
     height: 16px + (16px * 1.45)
     box-sizing: border-box
diff --git a/src/sass/components/chat.sass b/src/sass/components/chat.sass
index 5ca48e0..c8bb391 100644
--- a/src/sass/components/chat.sass
+++ b/src/sass/components/chat.sass
@@ -2,11 +2,12 @@
 
 .c-chat
   display: grid
-  grid-template-rows: 1fr 82px // fixed so that input box height adjustment doesn't mess up scroll
+  grid-template-rows: 0 1fr 82px // fixed so that input box height adjustment doesn't mess up scroll
   align-items: end
   flex: 1
 
   &__messages
+    position: relative
     height: 100%
     overflow-y: scroll
     scrollbar-color: c.$darkest c.$darker
diff --git a/src/sass/components/groups.sass b/src/sass/components/groups.sass
index e91f4cc..6eca6ec 100644
--- a/src/sass/components/groups.sass
+++ b/src/sass/components/groups.sass
@@ -36,11 +36,13 @@ $out-width: $base-width + rooms.$list-width
     box-sizing: border-box
 
 .c-group
+  position: relative
   display: flex
   align-items: center
   padding: $icon-padding / 2 $icon-padding
   cursor: pointer
   border-radius: 8px
+  background-color: c.$darkest
 
   &:hover
     background-color: c.$darker
@@ -62,6 +64,29 @@ $out-width: $base-width + rooms.$list-width
     overflow: hidden
     text-overflow: ellipsis
 
+  &__number
+    position: absolute
+    right: 240px
+    bottom: 0px
+    background: #ddd
+    color: #000
+    font-size: 14px
+    line-height: 1
+    padding: 3px 4px
+    border-radius: 7px
+    border: 3px solid c.$darkest
+    opacity: 0
+    transform: translate(6px, 6px)
+    transition: transform 0.15s ease-out, opacity 0.15s ease-out
+    pointer-events: none
+
+    @at-root .c-group:hover &
+      border-color: c.$darker
+
+    &--active
+      opacity: 1
+      transform: translate(0px, 0px)
+
 .c-group-marker
   position: absolute
   top: 5px
diff --git a/src/sass/components/highlighted-code.sass b/src/sass/components/highlighted-code.sass
new file mode 100644
index 0000000..1ec7290
--- /dev/null
+++ b/src/sass/components/highlighted-code.sass
@@ -0,0 +1 @@
+@use "../../../node_modules/highlight.js/scss/obsidian"
diff --git a/src/sass/components/messages.sass b/src/sass/components/messages.sass
index 7f56834..060dec4 100644
--- a/src/sass/components/messages.sass
+++ b/src/sass/components/messages.sass
@@ -1,6 +1,6 @@
 @use "../colors" as c
 
-.c-event-groups *
+.c-event-groups > *
   overflow-anchor: none
 
 .c-message-group, .c-message-event
@@ -9,7 +9,8 @@
   border-top: 1px solid c.$divider
 
 .c-message-group
-  display: flex
+  display: grid
+  grid-template-columns: auto 1fr
 
   &__avatar
     flex-shrink: 0
@@ -23,7 +24,7 @@
     border-radius: 50%
 
     &--no-icon
-      background-color: #48d
+      background-color: #bbb
 
   &__intro
     display: flex
@@ -46,9 +47,19 @@
 
 .c-message
   margin-top: 4px
+  overflow-wrap: anywhere
   opacity: 1
   transition: opacity 0.2s ease-out
 
+  &--plain
+    white-space: pre-wrap
+
+  &--media
+    // fix whitespace
+    font-size: 0
+    margin-top: 8px
+    display: flex
+
   &--pending
     opacity: 0.5
 
@@ -66,18 +77,70 @@
     &:hover
       background-color: c.$darker
 
+  &__image
+    width: auto
+    height: auto
+    max-width: 400px
+    max-height: 300px
+
+  // message formatting rules
+
+  code, pre
+    border-radius: 4px
+    font-size: 0.9em
+
+  pre
+    background-color: c.$darkest
+    padding: 8px
+    border: 1px solid c.$divider
+    white-space: pre-wrap
+
+  code
+    background-color: c.$darker
+    padding: 2px 4px
+
+  a
+    color: c.$link
+
+  blockquote
+    margin-left: 8px
+    border-left: 4px solid c.$muted
+    padding: 2px 0px 2px 12px
+
+  p, pre, blockquote
+    margin: 16px 0px
+
+    &:first-child
+      margin-top: 0px
+
+    &:last-child
+      margin-bottom: 0px
+
 .c-message-event
-  padding-top: 10px
+  // closer spacing than normal messages
+  padding-top: 2px
   padding-left: 6px
+  margin-bottom: -4px
+  line-height: 1.2
 
   &__inner
-    display: flex
-    align-items: center
+    text-indent: -36px
+    margin-left: 36px
+
+  img
+    // let me know if there's a smarter way to line this shit up
+    position: relative
+    top: -5px
+    transform: translateY(50%)
 
   &__icon
     margin-right: 8px
-    position: relative
-    top: 1px
+
+  &__avatar
+    width: 16px
+    height: 16px
+    border-radius: 50%
+    margin: 0px 6px
 
 .c-message-notice
   padding: 12px
@@ -87,3 +150,37 @@
     padding: 12px
     background-color: c.$milder
     border-radius: 8px
+
+.c-media
+  &__wrapper
+    overflow: hidden
+    position: relative
+
+  &--spoiler
+    cursor: pointer
+
+    img
+      filter: blur(40px)
+
+  &--shown img
+    filter: none
+
+  &__spoiler
+    position: absolute
+    top: 0
+    bottom: 0
+    left: 0
+    right: 0
+    display: flex
+    align-items: center
+    justify-content: center
+    font-size: 18px
+    font-weight: 500
+    color: #fff
+    text-transform: uppercase
+    background: rgba(0, 0, 0, 0.3)
+    cursor: pointer
+    pointer-events: none
+
+  &--shown &__spoiler
+    display: none
diff --git a/src/sass/components/read-marker.sass b/src/sass/components/read-marker.sass
new file mode 100644
index 0000000..7e1c572
--- /dev/null
+++ b/src/sass/components/read-marker.sass
@@ -0,0 +1,42 @@
+@use "../colors" as c
+
+.c-read-marker
+  display: none
+  position: relative
+
+  &--attached
+    display: block
+
+  &__inner
+    position: absolute
+    left: -64px
+    right: 0px
+    height: 2px
+    top: 0px
+    background-color: c.$notify-highlight
+
+    @at-root .c-message:last-child &
+      top: 11px
+
+    @at-root .c-message-event &
+      top: 7px
+
+  &__text
+    position: absolute
+    right: -14px
+    top: -9px
+    display: flex
+    align-items: center
+    background-color: c.$notify-highlight
+    color: #000
+    font-size: 12px
+    font-weight: 600
+    line-height: 1
+    padding: 4px
+    border-radius: 5px
+    text-transform: uppercase
+
+  &__loading
+    background-color: #000
+    width: 10px
+    height: 10px
diff --git a/src/sass/components/rooms.sass b/src/sass/components/rooms.sass
index 462e13c..670d9ba 100644
--- a/src/sass/components/rooms.sass
+++ b/src/sass/components/rooms.sass
@@ -43,3 +43,23 @@ $icon-padding: 8px
     white-space: nowrap
     overflow: hidden
     text-overflow: ellipsis
+    flex: 1
+
+  &__number
+    flex-shrink: 0
+    line-height: 1
+    padding: 4px 5px
+    border-radius: 5px
+    font-size: 14px
+    pointer-events: none
+
+    &--none
+      display: none
+
+    &--unreads
+      background-color: #ddd
+      color: #111
+
+    &--notifications
+      background-color: #ffac4b
+      color: #000
diff --git a/src/sass/components/spoilers.sass b/src/sass/components/spoilers.sass
new file mode 100644
index 0000000..e8a1784
--- /dev/null
+++ b/src/sass/components/spoilers.sass
@@ -0,0 +1,8 @@
+.mx-spoiler
+  color: #331911
+  background-color:  #331911
+  outline-color: #fff !important
+  cursor: pointer
+
+  &--shown
+    color: inherit
diff --git a/src/sass/components/typing.sass b/src/sass/components/typing.sass
new file mode 100644
index 0000000..dad8d90
--- /dev/null
+++ b/src/sass/components/typing.sass
@@ -0,0 +1,21 @@
+@use "../colors" as c
+
+.c-typing
+  height: 39px
+  background: c.$divider
+  position: absolute
+  right: 0
+  left: 0
+  top: 0
+  z-index: 0
+  margin: 20px
+  border-radius: 8px
+  padding: 0px 12px
+  font-size: 14px
+  line-height: 19px
+  transform: translateY(0px)
+  transition: transform 0.15s ease
+  color: #fff
+
+  &--typing
+    transform: translateY(-21px)
diff --git a/src/sass/loading.sass b/src/sass/loading.sass
new file mode 100644
index 0000000..9705bbe
--- /dev/null
+++ b/src/sass/loading.sass
@@ -0,0 +1,13 @@
+@keyframes spin
+  0%
+    transform: rotate(0deg)
+  100%
+    transform: rotate(180deg)
+
+.loading-icon
+  display: inline-block
+  background-color: #ccc
+  width: 12px
+  height: 12px
+  margin-right: 6px
+  animation: spin 0.7s infinite
diff --git a/src/sass/login.sass b/src/sass/login.sass
new file mode 100644
index 0000000..74d08ac
--- /dev/null
+++ b/src/sass/login.sass
@@ -0,0 +1,73 @@
+@use "./base"
+@use "./loading.sass"
+@use "./colors.sass" as c
+
+
+.main
+  justify-content: center
+  align-items: center
+
+.center-login-container
+  display: flex
+  flex-flow: column
+  justify-content: center
+  align-items: center
+  width: min(100vw, 450px)
+  padding: max(1rem,3vw) 2rem
+  margin: 8px
+  box-shadow: 0px 2px 10px c.$darkest
+  background-color: c.$darker
+  border-radius: 5px
+
+.login-form
+  align-items: center
+  flex: 1 1 auto
+  width: 100%
+  display: flex
+  justify-content: space-around
+  flex-flow: column
+
+.data-input
+  width: 100%
+
+.form-input-container
+  width: 100%
+  display: flex
+  flex-direction: column
+  margin: 1em 0
+
+.form-feedback
+  width: 100%
+  margin: -0.5em 0 -0.8em
+
+.form-error
+  color: red
+
+
+input, button
+  font-family: inherit
+  font-size: 17px
+  background-color: c.$mild
+  color: #eee
+  width: 100%
+  border-radius: 5px
+  box-sizing: border-box
+  transition: background-color 0.15s ease-out, border-color 0.15s ease-out
+  padding: 4px 9px
+  border: 0px
+
+input[type="text"],input[type="password"]
+  border: 3px solid transparent
+  margin: 0.4em 0px
+
+  &:hover, &:focus
+    border-color: c.$milder
+
+button, input[type="submit"]
+  padding: 7px
+
+  &:hover
+    background-color: c.$milder
+
+label
+  font-size: 18px
diff --git a/src/sass/main.sass b/src/sass/main.sass
index d342bbb..c050148 100644
--- a/src/sass/main.sass
+++ b/src/sass/main.sass
@@ -1,7 +1,13 @@
 @use "./base"
+@use "./loading"
 @use "./components/groups"
 @use "./components/rooms"
 @use "./components/messages"
 @use "./components/chat"
 @use "./components/chat-input"
+@use "./components/typing"
 @use "./components/anchor"
+@use "./components/highlighted-code"
+@use "./components/read-marker"
+@use "./components/chat-banner"
+@use "./components/spoilers"