Merge branch 'dev' into federation

This commit is contained in:
Dessalines 2020-04-14 16:07:20 -04:00
commit 37f94206f1
55 changed files with 1370 additions and 413 deletions

2
CODE_OF_CONDUCT.md vendored
View file

@ -30,6 +30,6 @@ In the Lemmy community we strive to go the extra step to look out for each other
And if someone takes issue with something you said or did, resist the urge to be defensive. Just stop doing what it was they complained about and apologize. Even if you feel you were misinterpreted or unfairly accused, chances are good there was something you couldve communicated better — remember that its your responsibility to make others comfortable. Everyone wants to get along and we are all here first and foremost because we want to talk about cool technology. You will find that people will be eager to assume good intent and forgive as long as you earn their trust.
The enforcement policies listed above apply to all official Lemmy venues; including git repositories under [github.com/dessalines/lemmy](https://github.com/dessalines/lemmy) and [yerbamate.dev/dessalines/lemmy](https://yerbamate.dev/dessalines/lemmy), the [Matrix channel](https://matrix.to/#/!BZVTUuEiNmRcbFeLeI:matrix.org?via=matrix.org&via=privacytools.io&via=permaweb.io); and all instances under lemmy.ml. For other projects adopting the Rust Code of Conduct, please contact the maintainers of those projects for enforcement. If you wish to use this code of conduct for your own project, consider explicitly mentioning your moderation policy or making a copy with your own moderation policy so as to avoid confusion.
The enforcement policies listed above apply to all official Lemmy venues; including git repositories under [github.com/LemmyNet/lemmy](https://github.com/LemmyNet/lemmy) and [yerbamate.dev/dessalines/lemmy](https://yerbamate.dev/dessalines/lemmy), the [Matrix channel](https://matrix.to/#/!BZVTUuEiNmRcbFeLeI:matrix.org?via=matrix.org&via=privacytools.io&via=permaweb.io); and all instances under lemmy.ml. For other projects adopting the Rust Code of Conduct, please contact the maintainers of those projects for enforcement. If you wish to use this code of conduct for your own project, consider explicitly mentioning your moderation policy or making a copy with your own moderation policy so as to avoid confusion.
Adapted from the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct), which is based on the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling) as well as the [Contributor Covenant v1.3.0](https://www.contributor-covenant.org/version/1/3/0/).

23
README.md vendored
View file

@ -1,12 +1,12 @@
<div align="center">
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
[![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/lemmy)
[![GitHub issues](https://img.shields.io/github/issues-raw/dessalines/lemmy.svg)](https://github.com/dessalines/lemmy/issues)
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/LemmyNet/lemmy.svg)
[![Build Status](https://travis-ci.org/LemmyNet/lemmy.svg?branch=master)](https://travis-ci.org/LemmyNet/lemmy)
[![GitHub issues](https://img.shields.io/github/issues-raw/LemmyNet/lemmy.svg)](https://github.com/LemmyNet/lemmy/issues)
[![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/)
[![Translation status](http://weblate.yerbamate.dev/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.dev/engage/lemmy/)
[![License](https://img.shields.io/github/license/dessalines/lemmy.svg)](LICENSE)
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
[![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE)
![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social)
</div>
<p align="center">
@ -22,11 +22,11 @@
·
<a href="https://dev.lemmy.ml/docs/index.html">Documentation</a>
·
<a href="https://github.com/dessalines/lemmy/issues">Report Bug</a>
<a href="https://github.com/LemmyNet/lemmy/issues">Report Bug</a>
·
<a href="https://github.com/dessalines/lemmy/issues">Request Feature</a>
<a href="https://github.com/LemmyNet/lemmy/issues">Request Feature</a>
·
<a href="https://github.com/dessalines/lemmy/blob/master/RELEASES.md">Releases</a>
<a href="https://github.com/LemmyNet/lemmy/blob/master/RELEASES.md">Releases</a>
</p>
</p>
@ -36,7 +36,7 @@ Front Page|Post
---|---
![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png)
[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
[Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
@ -108,8 +108,9 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
- [Support on Liberapay.](https://liberapay.com/Lemmy)
- [Support on Liberapay](https://liberapay.com/Lemmy).
- [Support on Patreon](https://www.patreon.com/dessalines).
- [Support on OpenCollective](https://opencollective.com/lemmy).
- [List of Sponsors](https://dev.lemmy.ml/sponsors).
### Crypto
@ -132,7 +133,7 @@ If you want to help with translating, take a look at [Weblate](https://weblate.y
- [Mastodon](https://mastodon.social/@LemmyDev) - [![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev)
- [Matrix](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org) - [![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
- [GitHub](https://github.com/dessalines/lemmy)
- [GitHub](https://github.com/LemmyNet/lemmy)
- [Gitea](https://yerbamate.dev/dessalines/lemmy)
- [GitLab](https://gitlab.com/dessalines/lemmy)

4
RELEASES.md vendored
View file

@ -1,6 +1,6 @@
# Lemmy v0.6.0 Release (2020-01-16)
`v0.6.0` is here, and we've closed [41 issues!](https://github.com/dessalines/lemmy/milestone/15?closed=1)
`v0.6.0` is here, and we've closed [41 issues!](https://github.com/LemmyNet/lemmy/milestone/15?closed=1)
This is the biggest release by far:
@ -10,7 +10,7 @@ This is the biggest release by far:
- Can set a custom language.
- Lemmy-wide settings to disable downvotes, and close registration.
- A better documentation system, hosted in lemmy itself.
- [Huge DB performance gains](https://github.com/dessalines/lemmy/issues/411) (everthing down to < `30ms`) by using materialized views.
- [Huge DB performance gains](https://github.com/LemmyNet/lemmy/issues/411) (everthing down to < `30ms`) by using materialized views.
- Fixed major issue with similar post URL and title searching.
- Upgraded to Actix `2.0`
- Faster comment / post voting.

2
ansible/VERSION vendored
View file

@ -1 +1 @@
v0.6.44
v0.6.49

View file

@ -72,5 +72,5 @@ git push origin $new_tag
git push
# Pushing to any ansible deploys
cd ../../ansible || exit
ansible-playbook lemmy.yml --become
cd ../../../lemmy-ansible || exit
ansible-playbook -i prod playbooks/site.yml --vault-password-file vault_pass

View file

@ -1,12 +0,0 @@
#!/bin/sh
# Building from the dev branch for dev servers
git checkout dev
# Rebuilding dev docker
docker-compose build
docker tag dev_lemmy:latest dessalines/lemmy:dev
docker push dessalines/lemmy:dev
# SSH and pull it
ssh $LEMMY_USER@$LEMMY_HOST "cd ~/git/lemmy/docker/dev && docker pull dessalines/lemmy:dev && docker-compose up -d"

View file

@ -21,7 +21,7 @@ services:
environment:
- RUST_LOG=debug
volumes:
- ../lemmy.hjson:/config/config.hjson:ro
- ../lemmy.hjson:/config/config.hjson
depends_on:
- postgres
- pictshare

11
docker/dev/test_deploy.sh vendored Executable file
View file

@ -0,0 +1,11 @@
#!/bin/sh
# Rebuilding dev docker
docker-compose build
docker tag dev_lemmy:latest dessalines/lemmy:test
docker push dessalines/lemmy:test
# Run the playbook
pushd ../../../lemmy-ansible
ansible-playbook -i test playbooks/site.yml --vault-password-file vault_pass
popd

View file

@ -12,14 +12,14 @@ services:
restart: always
lemmy:
image: dessalines/lemmy:v0.6.44
image: dessalines/lemmy:v0.6.49
ports:
- "127.0.0.1:8536:8536"
restart: always
environment:
- RUST_LOG=error
volumes:
- ./lemmy.hjson:/config/config.hjson:ro
- ./lemmy.hjson:/config/config.hjson
depends_on:
- postgres
- pictshare

1
docs/src/SUMMARY.md vendored
View file

@ -10,6 +10,7 @@
- [Install with Ansible](administration_install_ansible.md)
- [Install with Kubernetes](administration_install_kubernetes.md)
- [Configuration](administration_configuration.md)
- [Backup and Restore](administration_backup_and_restore.md)
- [Contributing](contributing.md)
- [Docker Development](contributing_docker_development.md)
- [Local Development](contributing_local_development.md)

2
docs/src/about.md vendored
View file

@ -4,7 +4,7 @@ Front Page|Post
---|---
![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png)
[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
[Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.

View file

@ -51,3 +51,4 @@
- [Activitypub implementers guide](https://socialhub.activitypub.rocks/t/draft-guide-for-new-activitypub-implementers/479)
- [Data storage questions](https://socialhub.activitypub.rocks/t/data-storage-questions/579/3)
- [Activitypub as it has been understood](https://flak.tedunangst.com/post/ActivityPub-as-it-has-been-understood)
- [Asonix http signatures in rust](https://git.asonix.dog/Aardwolf/http-signature-normalization)

View file

@ -0,0 +1,44 @@
# Backup and Restore Guide
## Docker and Ansible
When using docker or ansible, there should be a `volumes` folder, which contains both the database, and all the pictures. Copy this folder to the new instance to restore your data.
### Incremental Database backup
To incrementally backup the DB to an `.sql` file, you can run:
```bash
docker exec -t FOLDERNAME_postgres_1 pg_dumpall -c -U lemmy > lemmy_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql
```
### A Sample backup script
```bash
#!/bin/sh
# DB Backup
ssh MY_USER@MY_IP "docker exec -t FOLDERNAME_postgres_1 pg_dumpall -c -U lemmy" > ~/BACKUP_LOCATION/INSTANCE_NAME_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql
# Volumes folder Backup
rsync -avP -zz --rsync-path="sudo rsync" MY_USER@MY_IP:/LEMMY_LOCATION/volumes ~/BACKUP_LOCATION/FOLDERNAME
```
### Restoring the DB
If you need to restore from a `pg_dumpall` file, you need to first clear out your existing database
```bash
# Drop the existing DB
docker exec -i FOLDERNAME_postgres_1 psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
# Restore from the .sql backup
cat db_dump.sql | docker exec -i FOLDERNAME_postgres_1 psql -U lemmy # restores the db
# This also might be necessary when doing a db import with a different password.
docker exec -i FOLDERNAME_postgres_1 psql -U lemmy -c "alter user lemmy with password 'bleh'"
```
## More resources
- https://stackoverflow.com/questions/24718706/backup-restore-a-dockerized-postgresql-database

View file

@ -7,7 +7,7 @@ First, you need to [install Ansible on your local computer](https://docs.ansible
Then run the following commands on your local computer:
```bash
git clone https://github.com/dessalines/lemmy.git
git clone https://github.com/LemmyNet/lemmy.git
cd lemmy/ansible/
cp inventory.example inventory
nano inventory # enter your server, domain, contact email

View file

@ -4,13 +4,14 @@ Information about contributing to Lemmy, whether it is translating, testing, des
## Issue tracking / Repositories
- [GitHub (for issues)](https://github.com/dessalines/lemmy)
- [GitHub (for issues)](https://github.com/LemmyNet/lemmy)
- [Gitea](https://yerbamate.dev/dessalines/lemmy)
- [GitLab](https://gitlab.com/dessalines/lemmy)
## Translating
Go [here](https://github.com/dessalines/lemmy#translations) for translation instructions.
Check out [Lemmy's Weblate](https://weblate.yerbamate.dev/projects/lemmy/) for translations.
## Architecture

View file

@ -3,7 +3,7 @@
## Running
```bash
git clone https://github.com/dessalines/lemmy
git clone https://github.com/LemmyNet/lemmy
cd lemmy/docker/dev
./docker_update.sh # This builds and runs it, updating for your changes
```

View file

@ -22,7 +22,7 @@ export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
#### Running
```bash
git clone https://github.com/dessalines/lemmy
git clone https://github.com/LemmyNet/lemmy
cd lemmy
./install.sh
# For live coding, where both the front and back end, automagically reload on any save, do:

View file

@ -92,85 +92,93 @@
- [Request](#request-17)
- [Response](#response-17)
- [HTTP](#http-18)
* [Community](#community)
+ [Get Community](#get-community)
+ [Get Site Config](#get-site-config)
- [Request](#request-18)
- [Response](#response-18)
- [HTTP](#http-19)
+ [Create Community](#create-community)
+ [Save Site Config](#save-site-config)
- [Request](#request-19)
- [Response](#response-19)
- [HTTP](#http-20)
+ [List Communities](#list-communities)
* [Community](#community)
+ [Get Community](#get-community)
- [Request](#request-20)
- [Response](#response-20)
- [HTTP](#http-21)
+ [Ban from Community](#ban-from-community)
+ [Create Community](#create-community)
- [Request](#request-21)
- [Response](#response-21)
- [HTTP](#http-22)
+ [Add Mod to Community](#add-mod-to-community)
+ [List Communities](#list-communities)
- [Request](#request-22)
- [Response](#response-22)
- [HTTP](#http-23)
+ [Edit Community](#edit-community)
+ [Ban from Community](#ban-from-community)
- [Request](#request-23)
- [Response](#response-23)
- [HTTP](#http-24)
+ [Follow Community](#follow-community)
+ [Add Mod to Community](#add-mod-to-community)
- [Request](#request-24)
- [Response](#response-24)
- [HTTP](#http-25)
+ [Get Followed Communities](#get-followed-communities)
+ [Edit Community](#edit-community)
- [Request](#request-25)
- [Response](#response-25)
- [HTTP](#http-26)
+ [Transfer Community](#transfer-community)
+ [Follow Community](#follow-community)
- [Request](#request-26)
- [Response](#response-26)
- [HTTP](#http-27)
* [Post](#post)
+ [Create Post](#create-post)
+ [Get Followed Communities](#get-followed-communities)
- [Request](#request-27)
- [Response](#response-27)
- [HTTP](#http-28)
+ [Get Post](#get-post)
+ [Transfer Community](#transfer-community)
- [Request](#request-28)
- [Response](#response-28)
- [HTTP](#http-29)
+ [Get Posts](#get-posts)
* [Post](#post)
+ [Create Post](#create-post)
- [Request](#request-29)
- [Response](#response-29)
- [HTTP](#http-30)
+ [Create Post Like](#create-post-like)
+ [Get Post](#get-post)
- [Request](#request-30)
- [Response](#response-30)
- [HTTP](#http-31)
+ [Edit Post](#edit-post)
+ [Get Posts](#get-posts)
- [Request](#request-31)
- [Response](#response-31)
- [HTTP](#http-32)
+ [Save Post](#save-post)
+ [Create Post Like](#create-post-like)
- [Request](#request-32)
- [Response](#response-32)
- [HTTP](#http-33)
* [Comment](#comment)
+ [Create Comment](#create-comment)
+ [Edit Post](#edit-post)
- [Request](#request-33)
- [Response](#response-33)
- [HTTP](#http-34)
+ [Edit Comment](#edit-comment)
+ [Save Post](#save-post)
- [Request](#request-34)
- [Response](#response-34)
- [HTTP](#http-35)
+ [Save Comment](#save-comment)
* [Comment](#comment)
+ [Create Comment](#create-comment)
- [Request](#request-35)
- [Response](#response-35)
- [HTTP](#http-36)
+ [Create Comment Like](#create-comment-like)
+ [Edit Comment](#edit-comment)
- [Request](#request-36)
- [Response](#response-36)
- [HTTP](#http-37)
+ [Save Comment](#save-comment)
- [Request](#request-37)
- [Response](#response-37)
- [HTTP](#http-38)
+ [Create Comment Like](#create-comment-like)
- [Request](#request-38)
- [Response](#response-38)
- [HTTP](#http-39)
* [RSS / Atom feeds](#rss--atom-feeds)
+ [All](#all)
+ [Community](#community-1)
@ -779,6 +787,53 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
`POST /site/transfer`
#### Get Site Config
##### Request
```rust
{
op: "GetSiteConfig",
data: {
auth: String
}
}
```
##### Response
```rust
{
op: "GetSiteConfig",
data: {
config_hjson: String,
}
}
```
##### HTTP
`GET /site/config`
#### Save Site Config
##### Request
```rust
{
op: "SaveSiteConfig",
data: {
config_hjson: String,
auth: String
}
}
```
##### Response
```rust
{
op: "SaveSiteConfig",
data: {
config_hjson: String,
}
}
```
##### HTTP
`PUT /site/config`
### Community
#### Get Community
##### Request

View file

@ -97,6 +97,22 @@ pub struct TransferSite {
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct GetSiteConfig {
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct GetSiteConfigResponse {
config_hjson: String,
}
#[derive(Serialize, Deserialize)]
pub struct SaveSiteConfig {
config_hjson: String,
auth: String,
}
impl Perform<ListCategoriesResponse> for Oper<ListCategories> {
fn perform(&self, conn: &PgConnection) -> Result<ListCategoriesResponse, Error> {
let _data: &ListCategories = &self.data;
@ -514,3 +530,57 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
})
}
}
impl Perform<GetSiteConfigResponse> for Oper<GetSiteConfig> {
fn perform(&self, conn: &PgConnection) -> Result<GetSiteConfigResponse, Error> {
let data: &GetSiteConfig = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Only let admins read this
let admins = UserView::admins(&conn)?;
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
if !admin_ids.contains(&user_id) {
return Err(APIError::err("not_an_admin").into());
}
let config_hjson = Settings::read_config_file()?;
Ok(GetSiteConfigResponse { config_hjson })
}
}
impl Perform<GetSiteConfigResponse> for Oper<SaveSiteConfig> {
fn perform(&self, conn: &PgConnection) -> Result<GetSiteConfigResponse, Error> {
let data: &SaveSiteConfig = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Only let admins read this
let admins = UserView::admins(&conn)?;
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
if !admin_ids.contains(&user_id) {
return Err(APIError::err("not_an_admin").into());
}
// Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
let config_hjson = match Settings::save_config_file(&data.config_hjson) {
Ok(config_hjson) => config_hjson,
Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
};
Ok(GetSiteConfigResponse { config_hjson })
}
}

View file

@ -115,7 +115,7 @@ pub fn send_email(
to_username: &str,
html: &str,
) -> Result<(), String> {
let email_config = Settings::get().email.as_ref().ok_or("no_email_setup")?;
let email_config = Settings::get().email.ok_or("no_email_setup")?;
let email = Email::builder()
.to((to_email, to_username))
@ -130,7 +130,7 @@ pub fn send_email(
} else {
SmtpClient::new(&email_config.smtp_server, ClientSecurity::None).unwrap()
}
.hello_name(ClientId::Domain(Settings::get().hostname.to_owned()))
.hello_name(ClientId::Domain(Settings::get().hostname))
.smtp_utf8(true)
.authentication_mechanism(Mechanism::Plain)
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited);

View file

@ -57,6 +57,7 @@ async fn main() -> Result<(), Error> {
// Create Http server with websocket support
Ok(
HttpServer::new(move || {
let settings = Settings::get();
App::new()
.wrap(middleware::Logger::default())
.data(pool.clone())

View file

@ -52,6 +52,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route("/api/v1/site", web::post().to(route_post::<CreateSite, SiteResponse>))
.route("/api/v1/site", web::put().to(route_post::<EditSite, SiteResponse>))
.route("/api/v1/site/transfer", web::post().to(route_post::<TransferSite, GetSiteResponse>))
.route("/api/v1/site/config", web::get().to(route_get::<GetSiteConfig, GetSiteConfigResponse>))
.route("/api/v1/site/config", web::put().to(route_post::<SaveSiteConfig, GetSiteConfigResponse>))
.route("/api/v1/admin/add", web::post().to(route_post::<AddAdmin, AddAdminResponse>))
.route("/api/v1/user/ban", web::post().to(route_post::<BanUser, BanUserResponse>))
// User account actions

View file

@ -33,6 +33,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route("/modlog/community/{community_id}", web::get().to(index))
.route("/modlog", web::get().to(index))
.route("/setup", web::get().to(index))
.route("/admin", web::get().to(index))
.route(
"/search/q/{q}/type/{type}/sort/{sort}/page/{page}",
web::get().to(index),
@ -44,6 +45,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
async fn index() -> Result<NamedFile, actix_web::error::Error> {
Ok(NamedFile::open(
Settings::get().front_end_dir.to_owned() + "/index.html",
Settings::get().front_end_dir + "/index.html",
)?)
}

View file

@ -1,12 +1,15 @@
use config::{Config, ConfigError, Environment, File};
use failure::Error;
use serde::Deserialize;
use std::env;
use std::fs;
use std::net::IpAddr;
use std::sync::RwLock;
static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson";
static CONFIG_FILE: &str = "config/config.hjson";
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct Settings {
pub setup: Option<Setup>,
pub database: Database,
@ -20,7 +23,7 @@ pub struct Settings {
pub federation: Federation,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct Setup {
pub admin_username: String,
pub admin_password: String,
@ -28,7 +31,7 @@ pub struct Setup {
pub site_name: String,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct RateLimitConfig {
pub message: i32,
pub message_per_second: i32,
@ -38,7 +41,7 @@ pub struct RateLimitConfig {
pub register_per_second: i32,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct EmailConfig {
pub smtp_server: String,
pub smtp_login: Option<String>,
@ -47,7 +50,7 @@ pub struct EmailConfig {
pub use_tls: bool,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct Database {
pub user: String,
pub password: String,
@ -65,12 +68,10 @@ pub struct Federation {
}
lazy_static! {
static ref SETTINGS: Settings = {
match Settings::init() {
static ref SETTINGS: RwLock<Settings> = RwLock::new(match Settings::init() {
Ok(c) => c,
Err(e) => panic!("{}", e),
}
};
});
}
impl Settings {
@ -96,8 +97,8 @@ impl Settings {
}
/// Returns the config as a struct.
pub fn get() -> &'static Self {
&SETTINGS
pub fn get() -> Self {
SETTINGS.read().unwrap().to_owned()
}
/// Returns the postgres connection url. If LEMMY_DATABASE_URL is set, that is used,
@ -119,4 +120,22 @@ impl Settings {
pub fn api_endpoint(&self) -> String {
format!("{}/api/v1", self.hostname)
}
pub fn read_config_file() -> Result<String, Error> {
Ok(fs::read_to_string(CONFIG_FILE)?)
}
pub fn save_config_file(data: &str) -> Result<String, Error> {
fs::write(CONFIG_FILE, data)?;
// Reload the new settings
// From https://stackoverflow.com/questions/29654927/how-do-i-assign-a-string-to-a-mutable-static-variable/47181804#47181804
let mut new_settings = SETTINGS.write().unwrap();
*new_settings = match Settings::init() {
Ok(c) => c,
Err(e) => panic!("{}", e),
};
Self::read_config_file()
}
}

View file

@ -1 +1 @@
pub const VERSION: &str = "v0.6.44";
pub const VERSION: &str = "v0.6.49";

View file

@ -46,4 +46,6 @@ pub enum UserOperation {
GetPrivateMessages,
UserJoin,
GetComments,
GetSiteConfig,
SaveSiteConfig,
}

View file

@ -708,6 +708,16 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
res.online = chat.sessions.len();
to_json_string(&user_operation, &res)
}
UserOperation::GetSiteConfig => {
let get_site_config: GetSiteConfig = serde_json::from_str(data)?;
let res = Oper::new(get_site_config).perform(&conn)?;
to_json_string(&user_operation, &res)
}
UserOperation::SaveSiteConfig => {
let save_site_config: SaveSiteConfig = serde_json::from_str(data)?;
let res = Oper::new(save_site_config).perform(&conn)?;
to_json_string(&user_operation, &res)
}
UserOperation::Search => {
do_user_operation::<Search, SearchResponse>(user_operation, data, &conn)
}

View file

@ -156,7 +156,7 @@ hr {
}
.emoji {
height: 1.2em !important;
max-height: 1.2em !important;
}
.text-wrap-truncate {

33
ui/package.json vendored
View file

@ -14,19 +14,20 @@
},
"keywords": [],
"dependencies": {
"@joeattardi/emoji-button": "^2.12.1",
"@types/autosize": "^3.0.6",
"@types/js-cookie": "^2.2.5",
"@types/js-cookie": "^2.2.6",
"@types/jwt-decode": "^2.2.1",
"@types/markdown-it": "^0.0.9",
"@types/markdown-it-container": "^2.0.2",
"@types/node": "^13.9.2",
"@types/node": "^13.11.1",
"autosize": "^4.0.2",
"bootswatch": "^4.3.1",
"classcat": "^1.1.3",
"classcat": "^4.0.2",
"dotenv": "^8.2.0",
"emoji-short-name": "^1.0.0",
"husky": "^4.2.3",
"i18next": "^19.3.3",
"husky": "^4.2.5",
"i18next": "^19.4.1",
"inferno": "^7.4.2",
"inferno-i18next": "nimbusec-oss/inferno-i18next",
"inferno-router": "^7.4.2",
@ -37,26 +38,26 @@
"markdown-it-emoji": "^1.4.0",
"mobius1-selectr": "^2.4.13",
"moment": "^2.24.0",
"prettier": "^1.18.2",
"prettier": "^2.0.4",
"reconnecting-websocket": "^4.4.0",
"rxjs": "^6.4.0",
"terser": "^4.6.7",
"tippy.js": "^6.1.0",
"rxjs": "^6.5.5",
"terser": "^4.6.11",
"tippy.js": "^6.1.1",
"toastify-js": "^1.7.0",
"tributejs": "^5.1.2",
"tributejs": "^5.1.3",
"twemoji": "^12.1.2",
"ws": "^7.2.3"
},
"devDependencies": {
"eslint": "^6.5.1",
"eslint-plugin-inferno": "^7.14.3",
"eslint-plugin-jane": "^7.2.0",
"eslint-plugin-jane": "^7.2.1",
"fuse-box": "^3.1.3",
"lint-staged": "^10.0.8",
"sortpack": "^2.1.2",
"ts-node": "^8.7.0",
"ts-transform-classcat": "^0.0.2",
"ts-transform-inferno": "^4.0.2",
"lint-staged": "^10.1.3",
"sortpack": "^2.1.4",
"ts-node": "^8.8.2",
"ts-transform-classcat": "^1.0.0",
"ts-transform-inferno": "^4.0.3",
"typescript": "^3.8.3"
},
"engines": {

241
ui/src/components/admin-settings.tsx vendored Normal file
View file

@ -0,0 +1,241 @@
import { Component, linkEvent } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
UserOperation,
SiteResponse,
GetSiteResponse,
SiteConfigForm,
GetSiteConfigResponse,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService } from '../services';
import { wsJsonToRes, capitalizeFirstLetter, toast, randomStr } from '../utils';
import autosize from 'autosize';
import { SiteForm } from './site-form';
import { UserListing } from './user-listing';
import { i18n } from '../i18next';
interface AdminSettingsState {
siteRes: GetSiteResponse;
siteConfigRes: GetSiteConfigResponse;
siteConfigForm: SiteConfigForm;
loading: boolean;
siteConfigLoading: boolean;
}
export class AdminSettings extends Component<any, AdminSettingsState> {
private siteConfigTextAreaId = `site-config-${randomStr()}`;
private subscription: Subscription;
private emptyState: AdminSettingsState = {
siteRes: {
site: {
id: null,
name: null,
creator_id: null,
creator_name: null,
published: null,
number_of_users: null,
number_of_posts: null,
number_of_comments: null,
number_of_communities: null,
enable_downvotes: null,
open_registration: null,
enable_nsfw: null,
},
admins: [],
banned: [],
online: null,
},
siteConfigForm: {
config_hjson: null,
auth: null,
},
siteConfigRes: {
config_hjson: null,
},
loading: true,
siteConfigLoading: null,
};
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
WebSocketService.Instance.getSite();
WebSocketService.Instance.getSiteConfig();
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
return (
<div class="container">
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
</h5>
) : (
<div class="row">
<div class="col-12 col-md-6">
<SiteForm site={this.state.siteRes.site} />
{this.admins()}
{this.bannedUsers()}
</div>
<div class="col-12 col-md-6">{this.adminSettings()}</div>
</div>
)}
</div>
);
}
admins() {
return (
<>
<h5>{capitalizeFirstLetter(i18n.t('admins'))}</h5>
<ul class="list-unstyled">
{this.state.siteRes.admins.map(admin => (
<li class="list-inline-item">
<UserListing
user={{
name: admin.name,
avatar: admin.avatar,
}}
/>
</li>
))}
</ul>
</>
);
}
bannedUsers() {
return (
<>
<h5>{i18n.t('banned_users')}</h5>
<ul class="list-unstyled">
{this.state.siteRes.banned.map(banned => (
<li class="list-inline-item">
<UserListing
user={{
name: banned.name,
avatar: banned.avatar,
}}
/>
</li>
))}
</ul>
</>
);
}
adminSettings() {
return (
<div>
<h5>{i18n.t('admin_settings')}</h5>
<form onSubmit={linkEvent(this, this.handleSiteConfigSubmit)}>
<div class="form-group row">
<label
class="col-12 col-form-label"
htmlFor={this.siteConfigTextAreaId}
>
{i18n.t('site_config')}
</label>
<div class="col-12">
<textarea
id={this.siteConfigTextAreaId}
value={this.state.siteConfigForm.config_hjson}
onInput={linkEvent(this, this.handleSiteConfigHjsonChange)}
class="form-control text-monospace"
rows={3}
/>
</div>
</div>
<div class="form-group row">
<div class="col-12">
<button type="submit" class="btn btn-secondary mr-2">
{this.state.siteConfigLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
capitalizeFirstLetter(i18n.t('save'))
)}
</button>
</div>
</div>
</form>
</div>
);
}
handleSiteConfigSubmit(i: AdminSettings, event: any) {
event.preventDefault();
i.state.siteConfigLoading = true;
WebSocketService.Instance.saveSiteConfig(i.state.siteConfigForm);
i.setState(i.state);
}
handleSiteConfigHjsonChange(i: AdminSettings, event: any) {
i.state.siteConfigForm.config_hjson = event.target.value;
i.setState(i.state);
}
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
this.context.router.history.push('/');
this.state.loading = false;
this.setState(this.state);
return;
} else if (msg.reconnect) {
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
// This means it hasn't been set up yet
if (!data.site) {
this.context.router.history.push('/setup');
}
this.state.siteRes = data;
this.setState(this.state);
document.title = `${i18n.t('admin_settings')} - ${
this.state.siteRes.site.name
}`;
} else if (res.op == UserOperation.EditSite) {
let data = res.data as SiteResponse;
this.state.siteRes.site = data.site;
this.setState(this.state);
toast(i18n.t('site_saved'));
} else if (res.op == UserOperation.GetSiteConfig) {
let data = res.data as GetSiteConfigResponse;
this.state.siteConfigRes = data;
this.state.loading = false;
this.state.siteConfigForm.config_hjson = this.state.siteConfigRes.config_hjson;
this.setState(this.state);
var textarea: any = document.getElementById(this.siteConfigTextAreaId);
autosize(textarea);
} else if (res.op == UserOperation.SaveSiteConfig) {
let data = res.data as GetSiteConfigResponse;
this.state.siteConfigRes = data;
this.state.siteConfigForm.config_hjson = this.state.siteConfigRes.config_hjson;
this.state.siteConfigLoading = false;
toast(i18n.t('site_saved'));
this.setState(this.state);
}
}
}

View file

@ -17,10 +17,12 @@ import {
toast,
setupTribute,
wsJsonToRes,
emojiPicker,
} from '../utils';
import { WebSocketService, UserService } from '../services';
import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js';
import emojiShortName from 'emoji-short-name';
import { i18n } from '../i18next';
interface CommentFormProps {
@ -69,6 +71,8 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
super(props, context);
this.tribute = setupTribute();
this.setupEmojiPicker();
this.state = this.emptyState;
if (this.props.node) {
@ -158,8 +162,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
</button>
{this.state.commentForm.content && (
<button
className={`btn btn-sm mr-2 btn-secondary ${this.state
.previewMode && 'active'}`}
className={`btn btn-sm mr-2 btn-secondary ${
this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
@ -209,6 +214,15 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
<use xlinkHref="#icon-spinner"></use>
</svg>
)}
<span
onClick={linkEvent(this, this.handleEmojiPickerClick)}
class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold"
data-tippy-content={i18n.t('emoji_picker')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-smile"></use>
</svg>
</span>
</div>
</div>
</form>
@ -216,6 +230,20 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
);
}
setupEmojiPicker() {
emojiPicker.on('emoji', twemojiHtmlStr => {
if (this.state.commentForm.content == null) {
this.state.commentForm.content = '';
}
var el = document.createElement('div');
el.innerHTML = twemojiHtmlStr;
let nativeUnicode = (el.childNodes[0] as HTMLElement).getAttribute('alt');
let shortName = `:${emojiShortName[nativeUnicode]}:`;
this.state.commentForm.content += shortName;
this.setState(this.state);
});
}
handleFinished() {
this.state.previewMode = false;
this.state.loading = false;
@ -242,6 +270,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
i.setState(i.state);
}
handleEmojiPickerClick(_i: CommentForm, event: any) {
emojiPicker.togglePicker(event.target);
}
handleCommentContentChange(i: CommentForm, event: any) {
i.state.commentForm.content = event.target.value;
i.setState(i.state);

View file

@ -24,8 +24,6 @@ import {
getUnixTime,
canMod,
isMod,
pictshareAvatarThumbnail,
showAvatars,
setupTippy,
colorList,
} from '../utils';
@ -33,6 +31,7 @@ import moment from 'moment';
import { MomentTime } from './moment-time';
import { CommentForm } from './comment-form';
import { CommentNodes } from './comment-nodes';
import { UserListing } from './user-listing';
import { i18n } from '../i18next';
interface CommentNodeState {
@ -143,25 +142,21 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
>
<div
class={`${!this.props.noIndent &&
class={`${
!this.props.noIndent &&
this.props.node.comment.parent_id &&
'ml-2'}`}
'ml-2'
}`}
>
<div class="d-flex flex-wrap align-items-center mb-1 mt-1 text-muted small">
<Link
className="mr-2 text-body font-weight-bold"
to={`/u/${node.comment.creator_name}`}
>
{node.comment.creator_avatar && showAvatars() && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(node.comment.creator_avatar)}
class="rounded-circle mr-1"
<span class="mr-2">
<UserListing
user={{
name: node.comment.creator_name,
avatar: node.comment.creator_avatar,
}}
/>
)}
<span>{node.comment.creator_name}</span>
</Link>
</span>
{this.isMod && (
<div className="badge badge-light d-none d-sm-inline mr-2">
{i18n.t('mod')}
@ -191,7 +186,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</>
)}
<div
className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mr-2"
className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"
onClick={linkEvent(this, this.handleCommentCollapse)}
>
{this.state.collapsed ? (
@ -256,8 +251,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
this.loadingIcon
) : (
<svg
class={`icon icon-inline ${node.comment.read &&
'text-success'}`}
class={`icon icon-inline ${
node.comment.read && 'text-success'
}`}
>
<use xlinkHref="#icon-check"></use>
</svg>
@ -309,8 +305,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
this.loadingIcon
) : (
<svg
class={`icon icon-inline ${node.comment.saved &&
'text-warning'}`}
class={`icon icon-inline ${
node.comment.saved && 'text-warning'
}`}
>
<use xlinkHref="#icon-star"></use>
</svg>
@ -357,8 +354,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
data-tippy-content={i18n.t('view_source')}
>
<svg
class={`icon icon-inline ${this.state
.viewSource && 'text-success'}`}
class={`icon icon-inline ${
this.state.viewSource && 'text-success'
}`}
>
<use xlinkHref="#icon-file-text"></use>
</svg>
@ -387,8 +385,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
>
<svg
class={`icon icon-inline ${node.comment
.deleted && 'text-danger'}`}
class={`icon icon-inline ${
node.comment.deleted && 'text-danger'
}`}
>
<use xlinkHref="#icon-trash"></use>
</svg>

View file

@ -33,13 +33,12 @@ import { SortSelect } from './sort-select';
import { ListingTypeSelect } from './listing-type-select';
import { DataTypeSelect } from './data-type-select';
import { SiteForm } from './site-form';
import { UserListing } from './user-listing';
import {
wsJsonToRes,
repoUrl,
mdToHtml,
fetchLimit,
pictshareAvatarThumbnail,
showAvatars,
toast,
getListingTypeFromProps,
getPageFromProps,
@ -316,20 +315,12 @@ export class Main extends Component<any, MainState> {
<li class="list-inline-item">{i18n.t('admins')}:</li>
{this.state.siteRes.admins.map(admin => (
<li class="list-inline-item">
<Link
class="text-body font-weight-bold"
to={`/u/${admin.name}`}
>
{admin.avatar && showAvatars() && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(admin.avatar)}
class="rounded-circle mr-1"
<UserListing
user={{
name: admin.name,
avatar: admin.avatar,
}}
/>
)}
<span>{admin.name}</span>
</Link>
</li>
))}
</ul>
@ -619,6 +610,7 @@ export class Main extends Component<any, MainState> {
this.state.siteRes.site = data.site;
this.state.showEditSite = false;
this.setState(this.state);
toast(i18n.t('site_saved'));
} else if (res.op == UserOperation.GetPosts) {
let data = res.data as GetPostsResponse;
this.state.posts = data.posts;

View file

@ -16,6 +16,7 @@ import {
Comment,
CommentResponse,
PrivateMessage,
UserView,
PrivateMessageResponse,
WebSocketJsonResponse,
} from '../interfaces';
@ -40,6 +41,7 @@ interface NavbarState {
messages: Array<PrivateMessage>;
unreadCount: number;
siteName: string;
admins: Array<UserView>;
}
export class Navbar extends Component<any, NavbarState> {
@ -53,6 +55,7 @@ export class Navbar extends Component<any, NavbarState> {
messages: [],
expanded: false,
siteName: undefined,
admins: [],
};
constructor(props: any, context: any) {
@ -179,6 +182,19 @@ export class Navbar extends Component<any, NavbarState> {
</li>
</ul>
<ul class="navbar-nav ml-auto">
{this.canAdmin && (
<li className="nav-item mt-1">
<Link
class="nav-link"
to={`/admin`}
title={i18n.t('admin_settings')}
>
<svg class="icon">
<use xlinkHref="#icon-settings"></use>
</svg>
</Link>
</li>
)}
{this.state.isLoggedIn ? (
<>
<li className="nav-item mt-1">
@ -298,7 +314,10 @@ export class Navbar extends Component<any, NavbarState> {
if (data.site && !this.state.siteName) {
this.state.siteName = data.site.name;
this.state.admins = data.admins;
WebSocketService.Instance.site = data.site;
WebSocketService.Instance.admins = data.admins;
this.setState(this.state);
}
}
@ -353,9 +372,16 @@ export class Navbar extends Component<any, NavbarState> {
);
}
get canAdmin(): boolean {
return (
UserService.Instance.user &&
this.state.admins.map(a => a.id).includes(UserService.Instance.user.id)
);
}
requestNotificationPermission() {
if (UserService.Instance.user) {
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function () {
if (!Notification) {
toast(i18n.t('notifications_error'), 'danger');
return;

View file

@ -34,9 +34,11 @@ import {
randomStr,
setupTribute,
setupTippy,
emojiPicker,
} from '../utils';
import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js';
import emojiShortName from 'emoji-short-name';
import Selectr from 'mobius1-selectr';
import { i18n } from '../i18next';
@ -92,6 +94,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
this.tribute = setupTribute();
this.setupEmojiPicker();
this.state = this.emptyState;
if (this.props.post) {
@ -190,8 +194,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<form>
<label
htmlFor="file-upload"
className={`${UserService.Instance.user &&
'pointer'} d-inline-block float-right text-muted h6 font-weight-bold`}
className={`${
UserService.Instance.user && 'pointer'
} d-inline-block float-right text-muted font-weight-bold`}
data-tippy-content={i18n.t('upload_image')}
>
<svg class="icon icon-inline">
@ -284,8 +289,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
)}
{this.state.postForm.body && (
<button
className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
.previewMode && 'active'}`}
className={`mt-1 mr-2 btn btn-sm btn-secondary ${
this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
@ -294,13 +300,22 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<a
href={markdownHelpUrl}
target="_blank"
class="d-inline-block float-right text-muted h6 font-weight-bold"
class="d-inline-block float-right text-muted font-weight-bold"
title={i18n.t('formatting_help')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-help-circle"></use>
</svg>
</a>
<span
onClick={linkEvent(this, this.handleEmojiPickerClick)}
class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold"
data-tippy-content={i18n.t('emoji_picker')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-smile"></use>
</svg>
</span>
</div>
</div>
{!this.props.post && (
@ -369,6 +384,20 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
);
}
setupEmojiPicker() {
emojiPicker.on('emoji', twemojiHtmlStr => {
if (this.state.postForm.body == null) {
this.state.postForm.body = '';
}
var el = document.createElement('div');
el.innerHTML = twemojiHtmlStr;
let nativeUnicode = (el.childNodes[0] as HTMLElement).getAttribute('alt');
let shortName = `:${emojiShortName[nativeUnicode]}:`;
this.state.postForm.body += shortName;
this.setState(this.state);
});
}
handlePostSubmit(i: PostForm, event: any) {
event.preventDefault();
if (i.props.post) {
@ -512,6 +541,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
});
}
handleEmojiPickerClick(_i: PostForm, event: any) {
emojiPicker.togglePicker(event.target);
}
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
if (msg.error) {

View file

@ -19,18 +19,19 @@ import {
import { MomentTime } from './moment-time';
import { PostForm } from './post-form';
import { IFramelyCard } from './iframely-card';
import { UserListing } from './user-listing';
import {
md,
mdToHtml,
canMod,
isMod,
isImage,
isVideo,
getUnixTime,
pictshareAvatarThumbnail,
showAvatars,
pictshareImage,
setupTippy,
hostname,
previewLines,
} from '../utils';
import { i18n } from '../i18next';
@ -415,20 +416,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<ul class="list-inline mb-0 text-muted small">
<li className="list-inline-item">
<span>{i18n.t('by')} </span>
<Link
className="text-body font-weight-bold"
to={`/u/${post.creator_name}`}
>
{post.creator_avatar && showAvatars() && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(post.creator_avatar)}
class="rounded-circle mr-1"
<UserListing
user={{
name: post.creator_name,
avatar: post.creator_avatar,
}}
/>
)}
<span>{post.creator_name}</span>
</Link>
{this.isMod && (
<span className="mx-1 badge badge-light">
{i18n.t('mod')}
@ -465,6 +458,24 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<MomentTime data={post} />
</span>
</li>
{post.body && (
<>
<li className="list-inline-item"></li>
<li className="list-inline-item">
{/* Using a link with tippy doesn't work on touch devices unfortunately */}
<Link
className="text-muted"
data-tippy-content={md.render(previewLines(post.body))}
data-tippy-allowHtml={true}
to={`/post/${post.id}`}
>
<svg class="mr-1 icon icon-inline">
<use xlinkHref="#icon-book-open"></use>
</svg>
</Link>
</li>
</>
)}
<li className="list-inline-item"></li>
{this.state.upvotes !== this.state.score && (
<>

View file

@ -213,8 +213,9 @@ export class Post extends Component<any, PostState> {
return (
<div class="btn-group btn-group-toggle mb-2">
<label
className={`btn btn-sm btn-secondary pointer ${this.state
.commentSort === CommentSortType.Hot && 'active'}`}
className={`btn btn-sm btn-secondary pointer ${
this.state.commentSort === CommentSortType.Hot && 'active'
}`}
>
{i18n.t('hot')}
<input
@ -225,8 +226,9 @@ export class Post extends Component<any, PostState> {
/>
</label>
<label
className={`btn btn-sm btn-secondary pointer ${this.state
.commentSort === CommentSortType.Top && 'active'}`}
className={`btn btn-sm btn-secondary pointer ${
this.state.commentSort === CommentSortType.Top && 'active'
}`}
>
{i18n.t('top')}
<input
@ -237,8 +239,9 @@ export class Post extends Component<any, PostState> {
/>
</label>
<label
className={`btn btn-sm btn-secondary pointer ${this.state
.commentSort === CommentSortType.New && 'active'}`}
className={`btn btn-sm btn-secondary pointer ${
this.state.commentSort === CommentSortType.New && 'active'
}`}
>
{i18n.t('new')}
<input
@ -249,8 +252,9 @@ export class Post extends Component<any, PostState> {
/>
</label>
<label
className={`btn btn-sm btn-secondary pointer ${this.state
.commentSort === CommentSortType.Old && 'active'}`}
className={`btn btn-sm btn-secondary pointer ${
this.state.commentSort === CommentSortType.Old && 'active'
}`}
>
{i18n.t('old')}
<input
@ -460,7 +464,7 @@ export class Post extends Component<any, PostState> {
} else if (res.op == UserOperation.Search) {
let data = res.data as SearchResponse;
this.state.crossPosts = data.posts.filter(
p => p.id != this.state.post.id
p => p.id != Number(this.props.match.params.id)
);
this.setState(this.state);
} else if (res.op == UserOperation.TransferSite) {

View file

@ -21,14 +21,13 @@ import {
capitalizeFirstLetter,
markdownHelpUrl,
mdToHtml,
showAvatars,
pictshareAvatarThumbnail,
wsJsonToRes,
toast,
randomStr,
setupTribute,
setupTippy,
} from '../utils';
import { UserListing } from './user-listing';
import Tribute from 'tributejs/src/Tribute.js';
import autosize from 'autosize';
import { i18n } from '../i18next';
@ -132,22 +131,12 @@ export class PrivateMessageForm extends Component<
{this.state.recipient && (
<div class="col-sm-10 form-control-plaintext">
<Link
className="text-body font-weight-bold"
to={`/u/${this.state.recipient.name}`}
>
{this.state.recipient.avatar && showAvatars() && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(
this.state.recipient.avatar
)}
class="rounded-circle mr-1"
<UserListing
user={{
name: this.state.recipient.name,
avatar: this.state.recipient.avatar,
}}
/>
)}
<span>{this.state.recipient.name}</span>
</Link>
</div>
)}
</div>
@ -233,8 +222,9 @@ export class PrivateMessageForm extends Component<
</button>
{this.state.privateMessageForm.content && (
<button
className={`btn btn-secondary mr-2 ${this.state.previewMode &&
'active'}`}
className={`btn btn-secondary mr-2 ${
this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}

View file

@ -58,6 +58,7 @@ export class PrivateMessage extends Component<
<div class="border-top border-light">
<div>
<ul class="list-inline mb-0 text-muted small">
{/* TODO refactor this */}
<li className="list-inline-item">
{this.mine ? i18n.t('to') : i18n.t('from')}
</li>
@ -143,8 +144,9 @@ export class PrivateMessage extends Component<
}
>
<svg
class={`icon icon-inline ${message.read &&
'text-success'}`}
class={`icon icon-inline ${
message.read && 'text-success'
}`}
>
<use xlinkHref="#icon-check"></use>
</svg>
@ -187,8 +189,9 @@ export class PrivateMessage extends Component<
}
>
<svg
class={`icon icon-inline ${message.deleted &&
'text-danger'}`}
class={`icon icon-inline ${
message.deleted && 'text-danger'
}`}
>
<use xlinkHref="#icon-trash"></use>
</svg>
@ -203,8 +206,9 @@ export class PrivateMessage extends Component<
data-tippy-content={i18n.t('view_source')}
>
<svg
class={`icon icon-inline ${this.state.viewSource &&
'text-success'}`}
class={`icon icon-inline ${
this.state.viewSource && 'text-success'
}`}
>
<use xlinkHref="#icon-file-text"></use>
</svg>

View file

@ -30,6 +30,7 @@ import {
commentsToFlatNodes,
} from '../utils';
import { PostListing } from './post-listing';
import { UserListing } from './user-listing';
import { SortSelect } from './sort-select';
import { CommentNodes } from './comment-nodes';
import { i18n } from '../i18next';
@ -266,22 +267,12 @@ export class Search extends Component<any, SearchState> {
{i.type_ == 'users' && (
<div>
<span>
<Link
className="text-info"
to={`/u/${(i.data as UserView).name}`}
>
{(i.data as UserView).avatar && showAvatars() && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(
(i.data as UserView).avatar
)}
class="rounded-circle mr-1"
<UserListing
user={{
name: (i.data as UserView).name,
avatar: (i.data as UserView).avatar,
}}
/>
)}
<span>{`/u/${(i.data as UserView).name}`}</span>
</Link>
</span>
<span>{` - ${
(i.data as UserView).comment_score

View file

@ -15,6 +15,7 @@ import {
showAvatars,
} from '../utils';
import { CommunityForm } from './community-form';
import { UserListing } from './user-listing';
import { i18n } from '../i18next';
interface SidebarProps {
@ -110,8 +111,9 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
}
>
<svg
class={`icon icon-inline ${community.deleted &&
'text-danger'}`}
class={`icon icon-inline ${
community.deleted && 'text-danger'
}`}
>
<use xlinkHref="#icon-trash"></use>
</svg>
@ -204,27 +206,19 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<li class="list-inline-item">{i18n.t('mods')}: </li>
{this.props.moderators.map(mod => (
<li class="list-inline-item">
<Link
class="text-body font-weight-bold"
to={`/u/${mod.user_name}`}
>
{mod.avatar && showAvatars() && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(mod.avatar)}
class="rounded-circle mr-1"
<UserListing
user={{
name: mod.user_name,
avatar: mod.avatar,
}}
/>
)}
<span>{mod.user_name}</span>
</Link>
</li>
))}
</ul>
<Link
class={`btn btn-sm btn-secondary btn-block mb-3 ${(community.deleted ||
community.removed) &&
'no-click'}`}
class={`btn btn-sm btn-secondary btn-block mb-3 ${
(community.deleted || community.removed) && 'no-click'
}`}
to={`/create_post?community=${community.name}`}
>
{i18n.t('create_a_post')}

View file

@ -58,12 +58,19 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
});
}
// Necessary to stop the loading
componentWillReceiveProps() {
this.state.loading = false;
this.setState(this.state);
}
render() {
return (
<>
<Prompt
when={
!this.state.loading &&
!this.props.site &&
(this.state.siteForm.name || this.state.siteForm.description)
}
message={i18n.t('block_leaving')}

View file

@ -2,11 +2,12 @@ import { Component } from 'inferno';
import { WebSocketService } from '../services';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
import { repoUrl } from '../utils';
let general = [
'alexx henry',
'Nathan J. Goode',
'Andre Vallestero',
'riccardo',
'NotTooHighToHack',
];
let highlighted = ['Alex Benishek'];
@ -44,7 +45,7 @@ export class Sponsors extends Component<any, any> {
<h5>{i18n.t('donate_to_lemmy')}</h5>
<p>
<T i18nKey="sponsor_message">
#<a href="https://github.com/dessalines/lemmy">#</a>
#<a href={repoUrl}>#</a>
</T>
</p>
<a class="btn btn-secondary" href="https://liberapay.com/Lemmy/">
@ -56,6 +57,12 @@ export class Sponsors extends Component<any, any> {
>
{i18n.t('support_on_patreon')}
</a>
<a
class="btn btn-secondary ml-2"
href="https://opencollective.com/lemmy"
>
{i18n.t('support_on_open_collective')}
</a>
</div>
);
}

File diff suppressed because one or more lines are too long

36
ui/src/components/user-listing.tsx vendored Normal file
View file

@ -0,0 +1,36 @@
import { Component } from 'inferno';
import { Link } from 'inferno-router';
import { UserView } from '../interfaces';
import { pictshareAvatarThumbnail, showAvatars } from '../utils';
interface UserOther {
name: string;
avatar?: string;
}
interface UserListingProps {
user: UserView | UserOther;
}
export class UserListing extends Component<UserListingProps, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
let user = this.props.user;
return (
<Link className="text-body font-weight-bold" to={`/u/${user.name}`}>
{user.avatar && showAvatars() && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(user.avatar)}
class="rounded-circle mr-2"
/>
)}
<span>{user.name}</span>
</Link>
);
}
}

18
ui/src/index.tsx vendored
View file

@ -15,27 +15,25 @@ import { Communities } from './components/communities';
import { User } from './components/user';
import { Modlog } from './components/modlog';
import { Setup } from './components/setup';
import { AdminSettings } from './components/admin-settings';
import { Inbox } from './components/inbox';
import { Search } from './components/search';
import { Sponsors } from './components/sponsors';
import { Symbols } from './components/symbols';
import { i18n } from './i18next';
import { WebSocketService, UserService } from './services';
const container = document.getElementById('app');
class Index extends Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
WebSocketService.Instance;
UserService.Instance;
}
render() {
return (
<Provider i18next={i18n}>
<BrowserRouter>
<div>
<Navbar />
<div class="mt-4 p-0 fl-1">
<Switch>
@ -51,9 +49,15 @@ class Index extends Component<any, any> {
path={`/create_private_message`}
component={CreatePrivateMessage}
/>
<Route path={`/communities/page/:page`} component={Communities} />
<Route
path={`/communities/page/:page`}
component={Communities}
/>
<Route path={`/communities`} component={Communities} />
<Route path={`/post/:id/comment/:comment_id`} component={Post} />
<Route
path={`/post/:id/comment/:comment_id`}
component={Post}
/>
<Route path={`/post/:id`} component={Post} />
<Route
path={`/c/:name/data_type/:data_type/sort/:sort/page/:page`}
@ -74,6 +78,7 @@ class Index extends Component<any, any> {
/>
<Route path={`/modlog`} component={Modlog} />
<Route path={`/setup`} component={Setup} />
<Route path={`/admin`} component={AdminSettings} />
<Route
path={`/search/q/:q/type/:type/sort/:sort/page/:page`}
component={Search}
@ -88,6 +93,7 @@ class Index extends Component<any, any> {
<Symbols />
</div>
<Footer />
</div>
</BrowserRouter>
</Provider>
);

21
ui/src/interfaces.ts vendored
View file

@ -43,6 +43,8 @@ export enum UserOperation {
GetPrivateMessages,
UserJoin,
GetComments,
GetSiteConfig,
SaveSiteConfig,
}
export enum CommentSortType {
@ -724,6 +726,19 @@ export interface SiteForm {
auth?: string;
}
export interface GetSiteConfig {
auth?: string;
}
export interface GetSiteConfigResponse {
config_hjson: string;
}
export interface SiteConfigForm {
config_hjson: string;
auth?: string;
}
export interface GetSiteResponse {
site: Site;
admins: Array<UserView>;
@ -871,7 +886,8 @@ export type MessageType =
| PasswordChangeForm
| PrivateMessageForm
| EditPrivateMessageForm
| GetPrivateMessagesForm;
| GetPrivateMessagesForm
| SiteConfigForm;
type ResponseType =
| SiteResponse
@ -893,7 +909,8 @@ type ResponseType =
| BanUserResponse
| AddAdminResponse
| PrivateMessageResponse
| PrivateMessagesResponse;
| PrivateMessagesResponse
| GetSiteConfigResponse;
export interface WebSocketResponse {
op: UserOperation;

View file

@ -40,6 +40,8 @@ import {
GetPrivateMessagesForm,
GetCommentsForm,
UserJoinForm,
GetSiteConfig,
SiteConfigForm,
MessageType,
WebSocketJsonResponse,
} from '../interfaces';
@ -268,6 +270,12 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.GetSite, {}));
}
public getSiteConfig() {
let siteConfig: GetSiteConfig = {};
this.setAuth(siteConfig);
this.ws.send(this.wsSendWrapper(UserOperation.GetSiteConfig, siteConfig));
}
public search(form: SearchForm) {
this.setAuth(form, false);
this.ws.send(this.wsSendWrapper(UserOperation.Search, form));
@ -314,6 +322,11 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form));
}
public saveSiteConfig(form: SiteConfigForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.SaveSiteConfig, form));
}
private wsSendWrapper(op: UserOperation, data: MessageType) {
let send = { op: UserOperation[op], data: data };
console.log(send);

24
ui/src/utils.ts vendored
View file

@ -43,8 +43,9 @@ import twemoji from 'twemoji';
import emojiShortName from 'emoji-short-name';
import Toastify from 'toastify-js';
import tippy from 'tippy.js';
import EmojiButton from '@joeattardi/emoji-button';
export const repoUrl = 'https://github.com/dessalines/lemmy';
export const repoUrl = 'https://github.com/LemmyNet/lemmy';
export const helpGuideUrl = '/docs/about_guide.html';
export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
@ -88,6 +89,14 @@ export const themes = [
'i386',
];
export const emojiPicker = new EmojiButton({
// Use the emojiShortName from native
style: 'twemoji',
theme: 'dark',
position: 'auto-start',
// TODO i18n
});
export function randomStr() {
return Math.random()
.toString(36)
@ -473,8 +482,9 @@ export function setupTribute(): Tribute {
{
trigger: ':',
menuItemTemplate: (item: any) => {
let emoji = `:${item.original.key}:`;
return `${item.original.val} ${emoji}`;
let shortName = `:${item.original.key}:`;
let twemojiIcon = twemoji.parse(item.original.val);
return `${twemojiIcon} ${shortName}`;
},
selectTemplate: (item: any) => {
return `:${item.original.key}:`;
@ -824,6 +834,14 @@ function randomHsl() {
return `hsla(${Math.random() * 360}, 100%, 50%, 1)`;
}
export function previewLines(text: string, lines: number = 3): string {
// Use lines * 2 because markdown requires 2 lines
return text
.split('\n')
.slice(0, lines * 2)
.join('\n');
}
export function hostname(url: string): string {
return new URL(url).hostname;
}

2
ui/src/version.ts vendored
View file

@ -1 +1 @@
export const version: string = 'v0.6.44';
export const version: string = 'v0.6.49';

View file

@ -53,6 +53,8 @@
"mods": "mods",
"moderates": "Moderates",
"settings": "Settings",
"admin_settings": "Admin Settings",
"site_config": "Site Configuration",
"remove_as_mod": "remove as mod",
"appoint_as_mod": "appoint as mod",
"modlog": "Modlog",
@ -78,6 +80,7 @@
"unban": "unban",
"unban_from_site": "unban from site",
"banned": "banned",
"banned_users": "Banned Users",
"save": "save",
"unsave": "unsave",
"create": "create",
@ -187,6 +190,7 @@
"Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:",
"support_on_patreon": "Support on Patreon",
"support_on_liberapay": "Support on Liberapay",
"support_on_open_collective": "Support on OpenCollective",
"donate_to_lemmy": "Donate to Lemmy",
"donate": "Donate",
"general_sponsors":
@ -210,6 +214,7 @@
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
"not_logged_in": "Not logged in.",
"logged_in": "Logged in.",
"site_saved": "Site Saved.",
"community_ban": "You have been banned from this community.",
"site_ban": "You have been banned from the site",
"couldnt_create_comment": "Couldn't create comment.",
@ -251,5 +256,6 @@
"couldnt_update_private_message": "Couldn't update private message.",
"time": "Time",
"action": "Action",
"emoji_picker": "Emoji Picker",
"block_leaving": "Are you sure you want to leave?"
}

View file

@ -4,13 +4,15 @@
"no_posts": "Sin publicaciones.",
"create_a_post": "Crear una publicación",
"create_post": "Crear Publicación",
"number_of_posts": "{{count}} Publicaciones",
"number_of_posts": "{{count}} Publicación",
"number_of_posts_plural": "{{count}} Publicaciónes",
"posts": "Publicaciones",
"related_posts": "Estas publicaciones podrían estar relacionadas",
"cross_posts": "Este link también ha sido publicado en:",
"cross_post": "cross-post",
"comments": "Comentarios",
"number_of_comments": "{{count}} Comentarios",
"number_of_comments": "{{count}} Comentario",
"number_of_comments_plural": "{{count}} Comentarios",
"remove_comment": "Eliminar Comentarios",
"communities": "Comunidades",
"users": "Usuarios",
@ -20,7 +22,8 @@
"subscribed_to_communities": "Suscrito a <1>comunidades</1>",
"trending_communities": "<1>Comunidades</1> en tendencia",
"list_of_communities": "Lista de comunidades",
"number_of_communities": "{{count}} Comunidades",
"number_of_communities": "{{count}} Comunidad",
"number_of_communities_plural": "{{count}} Comunidades",
"community_reqs": "minúsculas, guión bajo, y sin espacios.",
"create_private_message": "Crear Mensaje Privado",
"send_secure_message": "Enviar Mensaje Seguro",
@ -63,8 +66,7 @@
"delete": "eliminar",
"deleted": "eliminado",
"delete_account": "Eliminar Cuenta",
"delete_account_confirm":
"Aviso: esta acción eliminará permanentemente tu información. Introduce tu contraseña para continuar",
"delete_account_confirm": "Aviso: esta acción eliminará permanentemente tu información. Introduce tu contraseña para continuar",
"restore": "restaurar",
"ban": "expulsar",
"ban_from_site": "expulsar del sitio",
@ -77,10 +79,14 @@
"creator": "creador",
"username": "Nombre de Usuario",
"email_or_username": "Correo o Usuario",
"number_of_users": "{{count}} Usuarios",
"number_of_subscribers": "{{count}} Suscriptores",
"number_of_points": "{{count}} Puntos",
"number_online": "{{count}} Usuarios En Línea",
"number_of_users": "{{count}} Usuario",
"number_of_users_plural": "{{count}} Usuarios",
"number_of_subscribers": "{{count}} Suscriptor",
"number_of_subscribers_plural": "{{count}} Suscriptores",
"number_of_points": "{{count}} Punto",
"number_of_points_plural": "{{count}} Puntos",
"number_online": "{{count}} Usuario En Línea",
"number_online_plural": "{{count}} Usuarios En Línea",
"name": "Nombre",
"title": "Titulo",
"category": "Categoría",
@ -120,8 +126,7 @@
"login_sign_up": "Iniciar sesión / Crear cuenta",
"login": "Iniciar sesión",
"sign_up": "Crear cuenta",
"notifications_error":
"Notificaciones de escritorio no disponibles en tu navegador. Prueba Firefox o Chrome.",
"notifications_error": "Notificaciones de escritorio no disponibles en tu navegador. Prueba Firefox o Chrome.",
"unread_messages": "Mensajes no leídos",
"messages": "Mensajes",
"password": "Contraseña",
@ -134,8 +139,7 @@
"no_email_setup": "Este servidor no ha activado correctamente el correo.",
"email": "Correo electrónico",
"matrix_user_id": "Usuario Matricial",
"private_message_disclaimer":
"Aviso: Los mensajes privados en Lemmy no son seguros. Por favor cree una cuenta en <1>Riot.im</1> para mensajeria segura.",
"private_message_disclaimer": "Aviso: Los mensajes privados en Lemmy no son seguros. Por favor cree una cuenta en <1>Riot.im</1> para mensajeria segura.",
"send_notifications_to_email": "Enviar notificaciones al correo",
"optional": "Opcional",
"expires": "Expira",
@ -165,14 +169,12 @@
"theme": "Tema",
"sponsors": "Patrocinadores",
"sponsors_of_lemmy": "Patrocinadores de Lemmy",
"sponsor_message":
"Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:",
"sponsor_message": "Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:",
"support_on_patreon": "Apoyo en Patreon",
"support_on_liberapay": "Apoyo en Liberapay",
"donate_to_lemmy": "Donar a Lemmy",
"donate": "Donar",
"general_sponsors":
"Los Patrocinadores Generales son aquellos que señaron entre $10 y $39 a Lemmy.",
"general_sponsors": "Los Patrocinadores Generales son aquellos que señaron entre $10 y $39 a Lemmy.",
"crypto": "Crypto",
"bitcoin": "Bitcoin",
"ethereum": "Ethereum",
@ -188,8 +190,7 @@
"yes": "sí",
"no": "no",
"powered_by": "Impulsado por",
"landing_0":
"Lemmy es un <1>agregador de links</1> / alternativa a reddit, con la intención de funcionar en el <2>fediverso</2>.<3></3>Es alojable por uno mismo (sin necesidad de grandes compañías), tiene actualización en vivo de cadenas de comentarios, y es pequeño (<4>~80kB</4>). Federar con el sistema de redes ActivityPub forma parte de los objetivos del proyecto. <5></5>Esta es una <6>version beta muy prematura</6>, y actualmente muchas de las características están rotas o faltan. <7></7>Sugiere nuevas características o reporta errores <8>aquí</8>.<9></9>Hecho con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
"landing_0": "Lemmy es un <1>agregador de links</1> / alternativa a reddit, con la intención de funcionar en el <2>fediverso</2>.<3></3>Es alojable por uno mismo (sin necesidad de grandes compañías), tiene actualización en vivo de cadenas de comentarios, y es pequeño (<4>~80kB</4>). Federar con el sistema de redes ActivityPub forma parte de los objetivos del proyecto. <5></5>Esta es una <6>version beta muy prematura</6>, y actualmente muchas de las características están rotas o faltan. <7></7>Sugiere nuevas características o reporta errores <8>aquí</8>.<9></9>Hecho con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
"not_logged_in": "No has iniciado sesión.",
"logged_in": "Has iniciado sesión.",
"community_ban": "Has sido expulsado de esta comunidad.",
@ -204,12 +205,9 @@
"couldnt_find_community": "No se pudo encontrar la comunidad.",
"couldnt_update_community": "No se pudo actualizar la comunidad.",
"community_already_exists": "Esta comunidad ya existe.",
"community_moderator_already_exists":
"Este moderador de la comunidad ya existe.",
"community_follower_already_exists":
"Este seguidor de la comunidad ya existe.",
"community_user_already_banned":
"Este usuario de la comunidad ya fue expulsado.",
"community_moderator_already_exists": "Este moderador de la comunidad ya existe.",
"community_follower_already_exists": "Este seguidor de la comunidad ya existe.",
"community_user_already_banned": "Este usuario de la comunidad ya fue expulsado.",
"couldnt_create_post": "No se pudo crear la publicación.",
"couldnt_like_post": "No se pudo gustar la publicación.",
"couldnt_find_post": "No se pudo encontrar la publicación.",
@ -220,21 +218,31 @@
"not_an_admin": "No es un administrador.",
"site_already_exists": "El sitio ya existe.",
"couldnt_update_site": "No se pudo actualizar el sitio.",
"couldnt_find_that_username_or_email":
"No se pudo encontrar ese nombre de usuario o correo electrónico.",
"couldnt_find_that_username_or_email": "No se pudo encontrar ese nombre de usuario o correo electrónico.",
"password_incorrect": "Contraseña incorrecta.",
"passwords_dont_match": "Las contraseñas no coinciden.",
"admin_already_created": "Lo sentimos, ya hay un adminisitrador.",
"user_already_exists": "El usuario ya existe.",
"email_already_exists": "El correo ya está en uso.",
"couldnt_update_user": "No se pudo actualizar el usuario.",
"system_err_login":
"Error del sistema. Intente cerrar sesión e ingresar de nuevo.",
"system_err_login": "Error del sistema. Intente cerrar sesión e ingresar de nuevo.",
"couldnt_create_private_message": "No se pudo crear el mensaje privado.",
"no_private_message_edit_allowed":
"Sin permisos para editar el mensaje privado.",
"no_private_message_edit_allowed": "Sin permisos para editar el mensaje privado.",
"couldnt_update_private_message": "No se pudo actualizar el mensaje privado.",
"old": "Antiguo",
"time": "Tiempo",
"action": "Acción"
"action": "Acción",
"more": "más",
"cross_posted_to": "publicado también en:",
"sorting_help": "ayuda del orden",
"upvote": "Voto Positivo",
"number_of_upvotes": "{{count}} Voto Positivo",
"number_of_upvotes_plural": "{{count}} Votos Positivos",
"downvote": "Voto Negativo",
"number_of_downvotes": "{{count}} Voto Negativo",
"number_of_downvotes_plural": "{{count}} Votos Negativos",
"couldnt_get_comments": "No se pudo obtener los comentarios.",
"post_title_too_long": "El título de la publicación es muy largo.",
"block_leaving": "¿Está seguro de que desea salir?",
"show_context": "Mostrar contexto"
}

View file

@ -227,5 +227,6 @@
"no_private_message_edit_allowed": "Pas autorisé à modifier un message privé.",
"couldnt_update_private_message": "Impossible de modifier un message privé.",
"time": "Temps",
"action": "Action"
"action": "Action",
"more": "plus"
}

View file

@ -1,2 +1,225 @@
{
"post": "პოსტი",
"remove_post": "პოსტის წაშლა",
"no_posts": "0 პოსტები",
"create_a_post": "პოსტის შექმნა",
"create_post": "პოსტის შექმნა",
"number_of_posts": "თარგმნა",
"number_of_posts_plural": "თარგმნა",
"posts": "პოსტები",
"cross_posts": "ეს ლინკი უკვე დადებულია აქ:",
"comments": "კომენტარები",
"number_of_comments": "კომენტარი",
"number_of_comments_plural": "კომანტარები",
"remove_comment": "კომენტარის წაშლა",
"communities": "თემები",
"users": "მომხმარებელი",
"create_a_community": "ახალი თემის შექმნა",
"create_community": "თემის შექმნა",
"remove_community": "თემის წაშლა",
"community_reqs": "პატარა ასო, ქვედა ტირე, და გამოტოვების გარეშე.",
"create_private_message": "კერძო მესეჯის შექმნა",
"send_secure_message": "ინკრიპტული მესეჯის გაგზავნა",
"send_message": "მესეჯის გაგზავნა",
"message": "მესეჯი",
"edit": "რადექტირება",
"reply": "პასუხის გაცემა",
"more": "მეტი",
"cancel": "გაუქება",
"upload_image": "სურათის ატვირთვა",
"avatar": "ავატარი",
"upload_avatar": "ავატარის ატვირთვა",
"show_context": "კონტექსტის ნახვა",
"sorting_help": "სორტირების დახმარება",
"view_source": "view source",
"unlock": "გაღება",
"lock": "ჩაკეტვა",
"sticky": "sticky",
"link": "ლინკი",
"archive_link": "ლინკის არქივება",
"mod": "მოდერატორი",
"mods": "მოდერატორები",
"moderates": "მოდერატორს",
"settings": "პარამეტრები",
"appoint_as_mod": "დანიშნე როგორც მოდერატორი",
"modlog": "მოდ-ლოგი",
"admin": "ადმინი",
"admins": "ადმინები",
"appoint_as_admin": "დანიშნე როგორც ადმინი",
"remove": "მოხსნა",
"removed": "მოხსნილია",
"locked": "ჩაკეტილი",
"stickied": "დაწეპებული",
"reason": "მიზეზი",
"mark_as_read": "მონიშნე როგორც წაკითხული",
"mark_as_unread": "მონიშნე როგორც წაუკითხავი",
"delete": "წაშლა",
"deleted": "წაშლილია",
"delete_account": "ჩემი ანგარიშის წაშლა",
"restore": "რასტორაცია",
"ban": "გაშავება",
"ban_from_site": "გაშავება საიტიდან",
"unban": "გაშავების გაუქმნება",
"unban_from_site": "სატიდან გაშავების გაუქმნება",
"banned": "გაშავებულია",
"save": "დამახსოვრება",
"unsave": "დამახსოვრების გაუქმნება",
"create": "შექმნა",
"preview": "წინასწარ ნახვა",
"show_avatars": "ავატარები გამოჩენა",
"formatting_help": "formatting help",
"unsticky": "unsticky",
"remove_as_mod": "მოხსენი როგორც მოდერატორი",
"remove_as_admin": "მოხსენი როგორც ადმინი",
"delete_account_confirm": "გაფთხილება: ეს შენს ყველაფერს წაშლის. პაროლი ჩაწერეთ რომ დაადასტუროთ.",
"creator": "შემქნელი",
"username": "მომხმარებლის სახელი",
"email_or_username": "ელ-პოსტა ან მომხმარებლის სახელი",
"number_of_users": "მომხმარებელი",
"number_of_users_plural": "მომხმარებლები",
"number_of_subscribers": "გამომწერი",
"number_of_subscribers_plural": "გამომწერები",
"number_of_points": "ქულა",
"number_of_points_plural": "ქულა",
"number_online": "მომხმარებელი საიტზე",
"number_online_plural": "მომხმარებელი საიტზე",
"name": "სახელი",
"title": "სათაური",
"category": "კატეგორია",
"subscribers": "გამომწერი",
"both": "ორივე",
"saved": "შანახული",
"unsubscribe": "გამოწერის გაუქმნება",
"subscribe": "გამოწერა",
"subscribed": "გამოწერილია",
"prev": "წუნა",
"next": "შემდეგი",
"sidebar": "Sidebar",
"sort_type": "სორტირების ტიპი",
"inbox": "Inbox",
"inbox_for": "<1>{{user}}</1>-s Inbox",
"mark_all_as_read": "მონიშვნა ყველასი როგორც წაკითხული",
"type": "ტიპი",
"unread": "წაუკითხავია",
"mentions": "ხსენებები",
"reply_sent": "პასუხი გაგზავნილია",
"message_sent": "მესეჯი",
"search": "ძებმა",
"overview": "გადახედვა",
"view": "ნახვა",
"logout": "გასვლა",
"login_sign_up": "შესვლა ან რეგისტრაცია",
"login": "შესვლა",
"sign_up": "რეგისტრაცია",
"unread_messages": "წაუკითხავი მესეჯები",
"messages": "მესეჯები",
"password": "პაროლი",
"verify_password": "პაროლის დადასტურება",
"old_password": "ძველი პაროლი",
"forgot_password": "აღგდენა",
"reset_password_mail_sent": "ელ-პოსტა შეამოწმეთ",
"password_change": "პაროლის შეცვლა",
"new_password": "ახალი პაროლი",
"email": "ელ-პოსტა",
"matrix_user_id": "მატრიცული მომხმარებელი",
"private_message_disclaimer": ".",
"send_notifications_to_email": "შეტყობინების გაგზავნა ელ-პოსტაზე",
"optional": "არასავალდებულო",
"expires": "ვადა გასდის",
"language": "ენა",
"browser_default": "Browser Default",
"enable_downvotes": "არმოწონების ჩართვა",
"upvote": "მოწონება",
"downvote": "არ მოწონება",
"open_registration": "რეგისტრაციის გახსნა",
"registration_closed": "რეგისტრაცია დახურულია",
"enable_nsfw": "Enable NSFW",
"url": "მისამართი",
"body": "ტექსტი",
"copy_suggested_title": "დაკოპირება რეკომინდებულის სათაური: {{title}}",
"community": "თემა",
"expand_here": "Expand here",
"subscribe_to_communities": "Subscribe to some <1>communities</1>.",
"chat": "ჩეტი",
"recent_comments": "ბოლო კომენტარები",
"no_results": "0 შედეგი",
"setup": "Setup",
"lemmy_instance_setup": "Lemmy Instance Setup",
"setup_admin": "Set Up Site Administrator",
"your_site": "შენი გვერდი",
"modified": "რედაკტირებული",
"nsfw": "NSFW",
"notifications_error": "გთხოვთ იხმაღეთ Chome ან Firefox შეტყობინებისთვის",
"no_email_setup": "This server hasn't correctly set up email.",
"downvotes_disabled": "არმოწონები გამორთულია",
"number_of_upvotes": "მოწონება",
"number_of_upvotes_plural": "მოწონება",
"number_of_downvotes": "არ მოწონება",
"number_of_downvotes_plural": "არ მოწონება",
"hot": "ცხელი",
"new": "ახალი",
"old": "ძველი",
"top_day": "ტოპ დღეს",
"week": "კვირა",
"month": "თვე",
"year": "წელი",
"all": "ყველა",
"top": "ტოპ",
"api": "API",
"show_nsfw": "Show NSFW content",
"related_posts": "ეს პოსტები შეიძლება ერთმანეც ეხება",
"cross_post": "გადაკვეთა-პოსტი",
"general_sponsors": "General Sponsors are those that pledged $10 to $39 to Lemmy.",
"cross_posted_to": "გადაკვეთა-პოსტი გაკეთდა: ",
"subscribed_to_communities": "მიყვები <1>communities</1>",
"trending_communities": "ტრენდული <1>communities</1>",
"list_of_communities": "ყველა თემა",
"number_of_communities": "თემა",
"number_of_communities_plural": "თემები",
"landing": "Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
"docs": "დოკუმენტაცია",
"couldnt_like_comment": "კომენტარის მოწონება ვერ მოხერხდა.",
"couldnt_update_comment": "კომენტარის განახლება ვერ მოხერხდა.",
"replies": "პასუხები",
"theme": "საიტის თემა",
"sponsors": "სპონსორები",
"sponsors_of_lemmy": "Sponsors",
"sponsor_message": "Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:",
"support_on_patreon": "Support on Patreon",
"support_on_liberapay": "Support on Liberapay",
"donate_to_lemmy": "Donate to Lemmy",
"no_comment_edit_allowed": "კომენტარის რედაკტირება არ შეიძლება.",
"donate": "Donate",
"crypto": "Crypto",
"bitcoin": "Bitcoin",
"ethereum": "Ethereum",
"code": "კოდი",
"joined": "დაემატა",
"by": "by",
"to": "to",
"from": "from",
"transfer_community": "transfer community",
"transfer_site": "transfer site",
"are_you_sure": "დარწმუნებული ხარ?",
"yes": "კი",
"no": "არა",
"powered_by": "Powered by",
"not_logged_in": "შასული არ ხართ",
"logged_in": "შაული ხართ.",
"community_ban": "შენ ამ თემისგან გაშავებული ხარ.",
"site_ban": "საიტიდან გაშავებული ხარ.",
"couldnt_create_comment": "კომენტარის შექმნა ვერ მოხერხდა.",
"couldnt_find_community": "ტემა არ მოიძებნა.",
"couldnt_save_comment": "კომენტარის შენახვა ვერ მოხერხდა.",
"couldnt_get_comments": "კომენტარების ნახვა ვერ მოხერხდა.",
"no_post_edit_allowed": "პოსტის რედაკტირება არ შეიძლება.",
"no_community_edit_allowed": "თემის რედაკტირება არ შეიძლება.",
"couldnt_update_community": "თემა ვერ განახლდა.",
"community_already_exists": "ეს თემა უკვე არსებობს.",
"community_follower_already_exists": "თემის ფოლოვორი უკვე არსებობს.",
"community_user_already_banned": "თემის მომხმარებელი უკვე შავ სიაშია.",
"couldnt_like_post": "პოსტის მოწონება ვერ მოხერხდა.",
"community_moderator_already_exists": "ამ თემის მოდერატორი უკვე არსებობს.",
"couldnt_create_post": "პოსტი ვერ შეიქმნა.",
"post_title_too_long": "პოსტის სათაური ძალიან გრძელია."
}

251
ui/yarn.lock vendored
View file

@ -126,10 +126,55 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@popperjs/core@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.1.1.tgz#12c572ab88ef7345b43f21883fca26631c223085"
integrity sha512-sLqWxCzC5/QHLhziXSCAksBxHfOnQlhPRVgPK0egEw+ktWvG75T2k+aYWVjVh9+WKeT3tlG3ZNbZQvZLmfuOIw==
"@fortawesome/fontawesome-common-types@^0.2.28":
version "0.2.28"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz#1091bdfe63b3f139441e9cba27aa022bff97d8b2"
integrity sha512-gtis2/5yLdfI6n0ia0jH7NJs5i/Z/8M/ZbQL6jXQhCthEOe5Cr5NcQPhgTvFxNOtURE03/ZqUcEskdn2M+QaBg==
"@fortawesome/fontawesome-svg-core@^1.2.22":
version "1.2.28"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.28.tgz#e5b8c8814ef375f01f5d7c132d3c3a2f83a3abf9"
integrity sha512-4LeaNHWvrneoU0i8b5RTOJHKx7E+y7jYejplR7uSVB34+mp3Veg7cbKk7NBCLiI4TyoWS1wh9ZdoyLJR8wSAdg==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.28"
"@fortawesome/free-regular-svg-icons@^5.10.2":
version "5.13.0"
resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.13.0.tgz#925a13d8bdda0678f71551828cac80ab47b8150c"
integrity sha512-70FAyiS5j+ANYD4dh9NGowTorNDnyvQHHpCM7FpnF7GxtDjBUCKdrFqCPzesEIpNDFNd+La3vex+jDk4nnUfpA==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.28"
"@fortawesome/free-solid-svg-icons@^5.10.2":
version "5.13.0"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.13.0.tgz#44d9118668ad96b4fd5c9434a43efc5903525739"
integrity sha512-IHUgDJdomv6YtG4p3zl1B5wWf9ffinHIvebqQOmV3U+3SLw4fC+LUCCgwfETkbTtjy5/Qws2VoVf6z/ETQpFpg==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.28"
"@joeattardi/emoji-button@^2.12.1":
version "2.12.1"
resolved "https://registry.yarnpkg.com/@joeattardi/emoji-button/-/emoji-button-2.12.1.tgz#190df7c00721e04742ed6f8852db828798a4cf98"
integrity sha512-rUuCXIcv4mRFK2IUKarYJN6J667wtH234smb1aQILzRf3/ycOoa6yUwnnvjxZeXMsPhuTnz15ndMOP2DhO5nNw==
dependencies:
"@fortawesome/fontawesome-svg-core" "^1.2.22"
"@fortawesome/free-regular-svg-icons" "^5.10.2"
"@fortawesome/free-solid-svg-icons" "^5.10.2"
"@popperjs/core" "^2.0.0"
focus-trap "^5.1.0"
tiny-emitter "^2.1.0"
tslib "^1.10.0"
twemoji "^12.1.5"
"@popperjs/core@^2.0.0":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.2.3.tgz#0ae22b5650ab0b8fe508047245b66e71fc59e983"
integrity sha512-68EQPzEZRrpFavFX40V2+80eqzQIhgza2AGTXW+i8laxSA4It+Y13rmZInrAYoIujp8YO7YJPbvgOesDZcIulQ==
"@popperjs/core@^2.2.0":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.3.2.tgz#1e56eb99bccddbda6a3e29aa4f3660f5b23edc43"
integrity sha512-18Tz3QghwsuHUC4gTNoxcEw1ClsrJ+lRypYpm+aucQonYNnmskQYvDZZKLHMPvQ7OwthWJl715UEX+Tg2fJkJw==
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.0"
@ -162,10 +207,10 @@
dependencies:
"@types/sizzle" "*"
"@types/js-cookie@^2.2.5":
version "2.2.5"
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.5.tgz#38dfaacae8623b37cc0b0d27398e574e3fc28b1e"
integrity sha512-cpmwBRcHJmmZx0OGU7aPVwGWGbs4iKwVYchk9iuMtxNCA2zorwdaTz4GkLgs2WGxiRZRFKnV1k6tRUHX7tBMxg==
"@types/js-cookie@^2.2.6":
version "2.2.6"
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f"
integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==
"@types/json-schema@^7.0.3":
version "7.0.4"
@ -196,10 +241,10 @@
dependencies:
"@types/linkify-it" "*"
"@types/node@^13.9.2":
version "13.9.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.2.tgz#ace1880c03594cc3e80206d96847157d8e7fa349"
integrity sha512-bnoqK579sAYrQbp73wwglccjJ4sfRdKU7WNEZ5FW4K2U6Kc0/eZ5kvXG0JKsEKFB50zrFmfFt52/cvBbZa7eXg==
"@types/node@^13.11.1":
version "13.11.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7"
integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
@ -749,6 +794,14 @@ chalk@^3.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72"
integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chardet@^0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
@ -790,10 +843,10 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classcat@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/classcat/-/classcat-1.1.3.tgz#ec748eecd962ec195a5d8f73f01d67c3d9040912"
integrity sha512-nuf6HJ5RlEgUUPqN/giIy1wsfA0LJwCHpo/aMGMwEIAxYypbLW/ZdPH4SNrF+OwdrkL3wxJmAs4GPyoE3ZkQ4w==
classcat@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/classcat/-/classcat-4.0.2.tgz#bd5d51b656e01e9cdd21c1aae3d29ed035a52126"
integrity sha512-RlMPOPp8VDu3CJOUVorPumhz/CI+t9ft6f0uexxxCguk28/M+Kf27eQXjNWeDTisEQWei/30oDfITOQqr1TNpQ==
clean-css@^4.1.9:
version "4.2.3"
@ -890,10 +943,10 @@ commander@^4.0.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83"
integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw==
compare-versions@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393"
integrity sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg==
compare-versions@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
component-emitter@^1.2.1:
version "1.3.0"
@ -1283,10 +1336,10 @@ eslint-plugin-inferno@^7.14.3:
object.values "^1.1.0"
resolve "^1.12.0"
eslint-plugin-jane@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jane/-/eslint-plugin-jane-7.2.0.tgz#a2454a6700c644e6c86821ca294adf303e75eddc"
integrity sha512-/BPZrfxWX9T45gJSf4/2GHfBYgsBYTW7StAQfxL8PxWABZIQKWPWy/5ZokX7UaJlgKHAoC42rJHCQLK5hmfJNA==
eslint-plugin-jane@^7.2.1:
version "7.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jane/-/eslint-plugin-jane-7.2.1.tgz#5ffba9ce75e0a5e5dbe3918fc0c5332d2cd89c13"
integrity sha512-hUmhEkHTDq6lQ4oLWZV5cLut9L67fcTiy0USbTsEOx658i9Jdikedt8NJhtamRqO5OUHBGSPU0JkOqBtVNUD+A==
dependencies:
"@typescript-eslint/eslint-plugin" "2.24.0"
"@typescript-eslint/parser" "2.24.0"
@ -1300,7 +1353,7 @@ eslint-plugin-jane@^7.2.0:
eslint-plugin-prettier "3.1.2"
eslint-plugin-promise "4.2.1"
eslint-plugin-react "7.19.0"
eslint-plugin-react-hooks "2.5.0"
eslint-plugin-react-hooks "2.5.1"
eslint-plugin-unicorn "17.2.0"
eslint-plugin-jest@23.8.2:
@ -1349,10 +1402,10 @@ eslint-plugin-promise@4.2.1:
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==
eslint-plugin-react-hooks@2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.0.tgz#c50ab7ca5945ce6d1cf8248d9e185c80b54171b6"
integrity sha512-bzvdX47Jx847bgAYf0FPX3u1oxU+mKU8tqrpj4UX9A96SbAmj/HVEefEy6rJUog5u8QIlOPTKZcBpGn5kkKfAQ==
eslint-plugin-react-hooks@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.1.tgz#4ef5930592588ce171abeb26f400c7fbcbc23cd0"
integrity sha512-Y2c4b55R+6ZzwtTppKwSmK/Kar8AdLiC2f9NADCuxbcTgPPg41Gyqa6b9GppgXSvCtkRw43ZE86CT5sejKC6/g==
eslint-plugin-react@7.19.0:
version "7.19.0"
@ -1829,6 +1882,14 @@ fliplog@^0.3.13:
dependencies:
chain-able "^1.0.1"
focus-trap@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-5.1.0.tgz#64a0bfabd95c382103397dbc96bfef3a3cf8e5ad"
integrity sha512-CkB/nrO55069QAUjWFBpX6oc+9V90Qhgpe6fBWApzruMq5gnlh90Oo7iSSDK7pKiV5ugG6OY2AXM5mxcmL3lwQ==
dependencies:
tabbable "^4.0.0"
xtend "^4.0.1"
for-in@^1.0.1, for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@ -2182,14 +2243,14 @@ human-signals@^1.1.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
husky@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.3.tgz#3b18d2ee5febe99e27f2983500202daffbc3151e"
integrity sha512-VxTsSTRwYveKXN4SaH1/FefRJYCtx+wx04sSVcOpD7N2zjoHxa+cEJ07Qg5NmV3HAK+IRKOyNVpi2YBIVccIfQ==
husky@^4.2.5:
version "4.2.5"
resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.5.tgz#2b4f7622673a71579f901d9885ed448394b5fa36"
integrity sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==
dependencies:
chalk "^3.0.0"
chalk "^4.0.0"
ci-info "^2.0.0"
compare-versions "^3.5.1"
compare-versions "^3.6.0"
cosmiconfig "^6.0.0"
find-versions "^3.2.0"
opencollective-postinstall "^2.0.2"
@ -2198,10 +2259,10 @@ husky@^4.2.3:
slash "^3.0.0"
which-pm-runs "^1.0.0"
i18next@^19.3.3:
version "19.3.3"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.3.3.tgz#04bd79b315e5fe2c87ab8f411e5d55eda0a17bd8"
integrity sha512-CnuPqep5/JsltkGvQqzYN4d79eCe0TreCBRF3a8qHHi8x4SON1qqZ/pvR2X7BfNkNqpA5HXIqw0E731H+VsgSg==
i18next@^19.4.1:
version "19.4.1"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.4.1.tgz#4929d15d3d01e4712350a368d005cefa50ff5455"
integrity sha512-dC3ue15jkLebN2je4xEjfjVYd/fSAo+UVK9f+JxvceCJRowkI+S0lGohgKejqU+FYLfvw9IAPylIIEWwR8Djrg==
dependencies:
"@babel/runtime" "^7.3.1"
@ -2814,10 +2875,10 @@ linkify-it@^2.0.0:
dependencies:
uc.micro "^1.0.1"
lint-staged@^10.0.8:
version "10.0.8"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.0.8.tgz#0f7849cdc336061f25f5d4fcbcfa385701ff4739"
integrity sha512-Oa9eS4DJqvQMVdywXfEor6F4vP+21fPHF8LUXgBbVWUSWBddjqsvO6Bv1LwMChmgQZZqwUvgJSHlu8HFHAPZmA==
lint-staged@^10.1.3:
version "10.1.3"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.1.3.tgz#da27713d3ac519da305381b4de87d5f866b1d2f1"
integrity sha512-o2OkLxgVns5RwSC5QF7waeAjJA5nz5gnUfqL311LkZcFipKV7TztrSlhNUK5nQX9H0E5NELAdduMQ+M/JPT7RQ==
dependencies:
chalk "^3.0.0"
commander "^4.0.1"
@ -3612,10 +3673,10 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@^1.18.2:
version "1.19.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
prettier@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.4.tgz#2d1bae173e355996ee355ec9830a7a1ee05457ef"
integrity sha512-SVJIQ51spzFDvh4fIbCLvciiDMCrRhlN3mbZvv/+ycjvmF5E73bKdGfU8QDLNmjYJf+lsGnDBC4UUnvTe5OO0w==
pretty-time@^0.2.0:
version "0.2.0"
@ -4001,13 +4062,20 @@ rx-lite@*, rx-lite@^4.0.8:
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=
rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.3:
rxjs@^6.3.3, rxjs@^6.5.3:
version "6.5.4"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c"
integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==
dependencies:
tslib "^1.9.0"
rxjs@^6.5.5:
version "6.5.5"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec"
integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==
dependencies:
tslib "^1.9.0"
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@ -4197,10 +4265,10 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
sortpack@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/sortpack/-/sortpack-2.1.2.tgz#25bf86f2923c81f43a00a2166ff4d271fafeed11"
integrity sha512-43fSND1vmAdyfgC38aOkVxZBV331f4blF8acjwQmx7Gba4nuL2ene/Cq5eixNmDhKA/qQHnvSeAl+jEWb31rfg==
sortpack@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/sortpack/-/sortpack-2.1.4.tgz#a2e251c5868455135cc41d3c98a53756a6de5282"
integrity sha512-RGD0l9kGmuPelXMT8WMMiSv1MkUkaqElB39nMkboIaqVkYns1aaNx263B2EE5QzF1YVUOrBlXnQpd7RX68SSow==
source-map-resolve@^0.5.0:
version "0.5.3"
@ -4473,6 +4541,11 @@ symbol-observable@^1.1.0:
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
tabbable@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261"
integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==
table@^5.2.3:
version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
@ -4483,10 +4556,10 @@ table@^5.2.3:
slice-ansi "^2.1.0"
string-width "^3.0.0"
terser@^4.6.7:
version "4.6.7"
resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.7.tgz#478d7f9394ec1907f0e488c5f6a6a9a2bad55e72"
integrity sha512-fmr7M1f7DBly5cX2+rFDvmGBAaaZyPrHYK4mMdHEDAdNTqXSZgSOfqsfGq2HqPGT/1V0foZZuCZFx8CHKgAk3g==
terser@^4.6.11:
version "4.6.11"
resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.11.tgz#12ff99fdd62a26de2a82f508515407eb6ccd8a9f"
integrity sha512-76Ynm7OXUG5xhOpblhytE7X58oeNSmC8xnNhjWVo8CksHit0U0kO4hfNbPrrYwowLWFgM2n9L176VNx2QaHmtA==
dependencies:
commander "^2.20.0"
source-map "~0.6.1"
@ -4502,6 +4575,11 @@ through@^2.3.6:
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
tiny-emitter@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
tiny-invariant@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
@ -4512,12 +4590,12 @@ tiny-warning@^1.0.0:
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tippy.js@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.1.0.tgz#9c58b94f92f3044d5e861b9d83da3c2a6d3d4323"
integrity sha512-cRFydlVZlvo4soQSUfVNbH2K77zDUhDAzaAjxseyn81gGIa+j72y98yDL2yB0n8gas/E+Zlr1iOyR5ckslUFqA==
tippy.js@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.1.1.tgz#9ed09aa4f9c47fb06a0e280e03055f898f5ddfff"
integrity sha512-Sk+FPihack9XFbPOc2jRbn6iRLA9my2a8qhaGY6wwD3EeW57/xY5PAPkZOutKVYDWLyNZ/laCkJqg7QJG/gqQw==
dependencies:
"@popperjs/core" "^2.1.1"
"@popperjs/core" "^2.2.0"
tmp@^0.0.33:
version "0.0.33"
@ -4581,15 +4659,15 @@ tough-cookie@~2.4.3:
psl "^1.1.24"
punycode "^1.4.1"
tributejs@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-5.1.2.tgz#d8492d974d3098d6016248d689fb063cda6e77f7"
integrity sha512-R9ff/q6w4T5f3Y9+RL+qinog3X1eAj1UnR/yfZaGJ8D3wuJs4/vicrGYul9+fgS9EJ/iYgwARekTb92xwark0g==
tributejs@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-5.1.3.tgz#980600fc72865be5868893078b4bfde721129eae"
integrity sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==
ts-node@^8.7.0:
version "8.7.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.7.0.tgz#266186947596bef9f3a034687595b30e31b20976"
integrity sha512-s659CsHrsxaRVDEleuOkGvbsA0rWHtszUNEt1r0CgAFN5ZZTQtDzpsluS7W5pOGJIa1xZE8R/zK4dEs+ldFezg==
ts-node@^8.8.2:
version "8.8.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.8.2.tgz#0b39e690bee39ea5111513a9d2bcdc0bc121755f"
integrity sha512-duVj6BpSpUpD/oM4MfhO98ozgkp3Gt9qIp3jGxwU2DFvl/3IRaEAvbLa8G60uS7C77457e/m5TMowjedeRxI1Q==
dependencies:
arg "^4.1.0"
diff "^4.0.1"
@ -4597,17 +4675,20 @@ ts-node@^8.7.0:
source-map-support "^0.5.6"
yn "3.1.1"
ts-transform-classcat@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/ts-transform-classcat/-/ts-transform-classcat-0.0.2.tgz#2386c9418f3a7c1f03261ff51225b70d0a7664fb"
integrity sha512-7laOOhgVxWVqvhK10mIEfedJx2nnNOS8J4P/6a/ehXtHFvsBVRRS9/FcTifgzJweOScZsF5BRD5VOGeNidMSqQ==
dependencies:
typescript "^2.6.2"
ts-transform-classcat@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ts-transform-classcat/-/ts-transform-classcat-1.0.0.tgz#6ae1be1b32f1f3c6b1c4232daf8a28e3ced0b62f"
integrity sha512-LWXEYvBwHDOqBBtoDWSUmbPMsw8FI9vD4XZm98RgziN9UCIj5MRtpmXuP5YYoimCTlPU+D4TFR3IqS+5xSzWsQ==
ts-transform-inferno@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/ts-transform-inferno/-/ts-transform-inferno-4.0.2.tgz#06b9be45edf874ba7a6ebfb6107ba782509c6afe"
integrity sha512-CZb4+w/2l2zikPZ/c51fi3n+qnR2HCEfAS73oGQB80aqRLffkZqm25kYYTMmqUW2+oVfs4M5AZa0z14cvxlQ5w==
ts-transform-inferno@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/ts-transform-inferno/-/ts-transform-inferno-4.0.3.tgz#2cc0eb125abdaff24b8298106a618ab7c6319edc"
integrity sha512-Pcg0PVQwJ7Fpv4+3R9obFNsrNKQyLbmUqsjeG7T7r4/4UTgIl0MSwurexjtuGpCp2iv2X/i9ffKPAfAOyYJ9og==
tslib@^1.10.0:
version "1.11.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
version "1.10.0"
@ -4638,7 +4719,7 @@ twemoji-parser@12.1.3:
resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-12.1.3.tgz#916c0153e77bd5f1011e7a99cbeacf52e43c9371"
integrity sha512-ND4LZXF4X92/PFrzSgGkq6KPPg8swy/U0yRw1k/+izWRVmq1HYi3khPwV3XIB6FRudgVICAaBhJfW8e8G3HC7Q==
twemoji@^12.1.2:
twemoji@^12.1.2, twemoji@^12.1.5:
version "12.1.5"
resolved "https://registry.yarnpkg.com/twemoji/-/twemoji-12.1.5.tgz#a961fb65a1afcb1f729ad7e59391f9fe969820b9"
integrity sha512-B0PBVy5xomwb1M/WZxf/IqPZfnoIYy1skXnlHjMwLwTNfZ9ljh8VgWQktAPcJXu8080WoEh6YwQGPVhDVqvrVQ==
@ -4673,11 +4754,6 @@ type-is@~1.6.17, type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
typescript@^2.6.2:
version "2.9.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==
typescript@^3.8.3:
version "3.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
@ -4890,6 +4966,11 @@ xregexp@^4.3.0:
dependencies:
"@babel/runtime-corejs3" "^7.8.3"
xtend@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
yaml@^1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.7.2.tgz#f26aabf738590ab61efaca502358e48dc9f348b2"