Merge branch 'dev' into federation

This commit is contained in:
Dessalines 2020-04-14 16:07:20 -04:00
commit 1336b4ed60
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. 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/). 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"> <div align="center">
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg) ![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/LemmyNet/lemmy.svg)
[![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/lemmy) [![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/dessalines/lemmy.svg)](https://github.com/dessalines/lemmy/issues) [![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/) [![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/) [![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) [![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE)
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social) ![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social)
</div> </div>
<p align="center"> <p align="center">
@ -22,11 +22,11 @@
· ·
<a href="https://dev.lemmy.ml/docs/index.html">Documentation</a> <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>
</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) ![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. 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. 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 Patreon](https://www.patreon.com/dessalines).
- [Support on OpenCollective](https://opencollective.com/lemmy).
- [List of Sponsors](https://dev.lemmy.ml/sponsors). - [List of Sponsors](https://dev.lemmy.ml/sponsors).
### Crypto ### 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) - [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) - [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) - [Gitea](https://yerbamate.dev/dessalines/lemmy)
- [GitLab](https://gitlab.com/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) # 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: This is the biggest release by far:
@ -10,7 +10,7 @@ This is the biggest release by far:
- Can set a custom language. - Can set a custom language.
- Lemmy-wide settings to disable downvotes, and close registration. - Lemmy-wide settings to disable downvotes, and close registration.
- A better documentation system, hosted in lemmy itself. - 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. - Fixed major issue with similar post URL and title searching.
- Upgraded to Actix `2.0` - Upgraded to Actix `2.0`
- Faster comment / post voting. - 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 git push
# Pushing to any ansible deploys # Pushing to any ansible deploys
cd ../../ansible || exit cd ../../../lemmy-ansible || exit
ansible-playbook lemmy.yml --become 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: environment:
- RUST_LOG=debug - RUST_LOG=debug
volumes: volumes:
- ../lemmy.hjson:/config/config.hjson:ro - ../lemmy.hjson:/config/config.hjson
depends_on: depends_on:
- postgres - postgres
- pictshare - 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 restart: always
lemmy: lemmy:
image: dessalines/lemmy:v0.6.44 image: dessalines/lemmy:v0.6.49
ports: ports:
- "127.0.0.1:8536:8536" - "127.0.0.1:8536:8536"
restart: always restart: always
environment: environment:
- RUST_LOG=error - RUST_LOG=error
volumes: volumes:
- ./lemmy.hjson:/config/config.hjson:ro - ./lemmy.hjson:/config/config.hjson
depends_on: depends_on:
- postgres - postgres
- pictshare - pictshare

1
docs/src/SUMMARY.md vendored
View file

@ -10,6 +10,7 @@
- [Install with Ansible](administration_install_ansible.md) - [Install with Ansible](administration_install_ansible.md)
- [Install with Kubernetes](administration_install_kubernetes.md) - [Install with Kubernetes](administration_install_kubernetes.md)
- [Configuration](administration_configuration.md) - [Configuration](administration_configuration.md)
- [Backup and Restore](administration_backup_and_restore.md)
- [Contributing](contributing.md) - [Contributing](contributing.md)
- [Docker Development](contributing_docker_development.md) - [Docker Development](contributing_docker_development.md)
- [Local Development](contributing_local_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) ![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. 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) - [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) - [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) - [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: Then run the following commands on your local computer:
```bash ```bash
git clone https://github.com/dessalines/lemmy.git git clone https://github.com/LemmyNet/lemmy.git
cd lemmy/ansible/ cd lemmy/ansible/
cp inventory.example inventory cp inventory.example inventory
nano inventory # enter your server, domain, contact email nano inventory # enter your server, domain, contact email
@ -19,4 +19,4 @@ To update to a new version, just run the following in your local Lemmy repo:
git pull origin master git pull origin master
cd ansible cd ansible
ansible-playbook lemmy.yml --become ansible-playbook lemmy.yml --become
``` ```

View file

@ -4,13 +4,14 @@ Information about contributing to Lemmy, whether it is translating, testing, des
## Issue tracking / Repositories ## 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) - [Gitea](https://yerbamate.dev/dessalines/lemmy)
- [GitLab](https://gitlab.com/dessalines/lemmy) - [GitLab](https://gitlab.com/dessalines/lemmy)
## Translating ## 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 ## Architecture

View file

@ -3,7 +3,7 @@
## Running ## Running
```bash ```bash
git clone https://github.com/dessalines/lemmy git clone https://github.com/LemmyNet/lemmy
cd lemmy/docker/dev cd lemmy/docker/dev
./docker_update.sh # This builds and runs it, updating for your changes ./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 #### Running
```bash ```bash
git clone https://github.com/dessalines/lemmy git clone https://github.com/LemmyNet/lemmy
cd lemmy cd lemmy
./install.sh ./install.sh
# For live coding, where both the front and back end, automagically reload on any save, do: # 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) - [Request](#request-17)
- [Response](#response-17) - [Response](#response-17)
- [HTTP](#http-18) - [HTTP](#http-18)
* [Community](#community) + [Get Site Config](#get-site-config)
+ [Get Community](#get-community)
- [Request](#request-18) - [Request](#request-18)
- [Response](#response-18) - [Response](#response-18)
- [HTTP](#http-19) - [HTTP](#http-19)
+ [Create Community](#create-community) + [Save Site Config](#save-site-config)
- [Request](#request-19) - [Request](#request-19)
- [Response](#response-19) - [Response](#response-19)
- [HTTP](#http-20) - [HTTP](#http-20)
+ [List Communities](#list-communities) * [Community](#community)
+ [Get Community](#get-community)
- [Request](#request-20) - [Request](#request-20)
- [Response](#response-20) - [Response](#response-20)
- [HTTP](#http-21) - [HTTP](#http-21)
+ [Ban from Community](#ban-from-community) + [Create Community](#create-community)
- [Request](#request-21) - [Request](#request-21)
- [Response](#response-21) - [Response](#response-21)
- [HTTP](#http-22) - [HTTP](#http-22)
+ [Add Mod to Community](#add-mod-to-community) + [List Communities](#list-communities)
- [Request](#request-22) - [Request](#request-22)
- [Response](#response-22) - [Response](#response-22)
- [HTTP](#http-23) - [HTTP](#http-23)
+ [Edit Community](#edit-community) + [Ban from Community](#ban-from-community)
- [Request](#request-23) - [Request](#request-23)
- [Response](#response-23) - [Response](#response-23)
- [HTTP](#http-24) - [HTTP](#http-24)
+ [Follow Community](#follow-community) + [Add Mod to Community](#add-mod-to-community)
- [Request](#request-24) - [Request](#request-24)
- [Response](#response-24) - [Response](#response-24)
- [HTTP](#http-25) - [HTTP](#http-25)
+ [Get Followed Communities](#get-followed-communities) + [Edit Community](#edit-community)
- [Request](#request-25) - [Request](#request-25)
- [Response](#response-25) - [Response](#response-25)
- [HTTP](#http-26) - [HTTP](#http-26)
+ [Transfer Community](#transfer-community) + [Follow Community](#follow-community)
- [Request](#request-26) - [Request](#request-26)
- [Response](#response-26) - [Response](#response-26)
- [HTTP](#http-27) - [HTTP](#http-27)
* [Post](#post) + [Get Followed Communities](#get-followed-communities)
+ [Create Post](#create-post)
- [Request](#request-27) - [Request](#request-27)
- [Response](#response-27) - [Response](#response-27)
- [HTTP](#http-28) - [HTTP](#http-28)
+ [Get Post](#get-post) + [Transfer Community](#transfer-community)
- [Request](#request-28) - [Request](#request-28)
- [Response](#response-28) - [Response](#response-28)
- [HTTP](#http-29) - [HTTP](#http-29)
+ [Get Posts](#get-posts) * [Post](#post)
+ [Create Post](#create-post)
- [Request](#request-29) - [Request](#request-29)
- [Response](#response-29) - [Response](#response-29)
- [HTTP](#http-30) - [HTTP](#http-30)
+ [Create Post Like](#create-post-like) + [Get Post](#get-post)
- [Request](#request-30) - [Request](#request-30)
- [Response](#response-30) - [Response](#response-30)
- [HTTP](#http-31) - [HTTP](#http-31)
+ [Edit Post](#edit-post) + [Get Posts](#get-posts)
- [Request](#request-31) - [Request](#request-31)
- [Response](#response-31) - [Response](#response-31)
- [HTTP](#http-32) - [HTTP](#http-32)
+ [Save Post](#save-post) + [Create Post Like](#create-post-like)
- [Request](#request-32) - [Request](#request-32)
- [Response](#response-32) - [Response](#response-32)
- [HTTP](#http-33) - [HTTP](#http-33)
* [Comment](#comment) + [Edit Post](#edit-post)
+ [Create Comment](#create-comment)
- [Request](#request-33) - [Request](#request-33)
- [Response](#response-33) - [Response](#response-33)
- [HTTP](#http-34) - [HTTP](#http-34)
+ [Edit Comment](#edit-comment) + [Save Post](#save-post)
- [Request](#request-34) - [Request](#request-34)
- [Response](#response-34) - [Response](#response-34)
- [HTTP](#http-35) - [HTTP](#http-35)
+ [Save Comment](#save-comment) * [Comment](#comment)
+ [Create Comment](#create-comment)
- [Request](#request-35) - [Request](#request-35)
- [Response](#response-35) - [Response](#response-35)
- [HTTP](#http-36) - [HTTP](#http-36)
+ [Create Comment Like](#create-comment-like) + [Edit Comment](#edit-comment)
- [Request](#request-36) - [Request](#request-36)
- [Response](#response-36) - [Response](#response-36)
- [HTTP](#http-37) - [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) * [RSS / Atom feeds](#rss--atom-feeds)
+ [All](#all) + [All](#all)
+ [Community](#community-1) + [Community](#community-1)
@ -779,6 +787,53 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
`POST /site/transfer` `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 ### Community
#### Get Community #### Get Community
##### Request ##### Request

View file

@ -97,6 +97,22 @@ pub struct TransferSite {
auth: String, 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> { impl Perform<ListCategoriesResponse> for Oper<ListCategories> {
fn perform(&self, conn: &PgConnection) -> Result<ListCategoriesResponse, Error> { fn perform(&self, conn: &PgConnection) -> Result<ListCategoriesResponse, Error> {
let _data: &ListCategories = &self.data; 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, to_username: &str,
html: &str, html: &str,
) -> Result<(), String> { ) -> 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() let email = Email::builder()
.to((to_email, to_username)) .to((to_email, to_username))
@ -130,7 +130,7 @@ pub fn send_email(
} else { } else {
SmtpClient::new(&email_config.smtp_server, ClientSecurity::None).unwrap() 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) .smtp_utf8(true)
.authentication_mechanism(Mechanism::Plain) .authentication_mechanism(Mechanism::Plain)
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited); .connection_reuse(ConnectionReuseParameters::ReuseUnlimited);

View file

@ -57,6 +57,7 @@ async fn main() -> Result<(), Error> {
// Create Http server with websocket support // Create Http server with websocket support
Ok( Ok(
HttpServer::new(move || { HttpServer::new(move || {
let settings = Settings::get();
App::new() App::new()
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.data(pool.clone()) .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::post().to(route_post::<CreateSite, SiteResponse>))
.route("/api/v1/site", web::put().to(route_post::<EditSite, 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/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/admin/add", web::post().to(route_post::<AddAdmin, AddAdminResponse>))
.route("/api/v1/user/ban", web::post().to(route_post::<BanUser, BanUserResponse>)) .route("/api/v1/user/ban", web::post().to(route_post::<BanUser, BanUserResponse>))
// User account actions // 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/community/{community_id}", web::get().to(index))
.route("/modlog", web::get().to(index)) .route("/modlog", web::get().to(index))
.route("/setup", web::get().to(index)) .route("/setup", web::get().to(index))
.route("/admin", web::get().to(index))
.route( .route(
"/search/q/{q}/type/{type}/sort/{sort}/page/{page}", "/search/q/{q}/type/{type}/sort/{sort}/page/{page}",
web::get().to(index), web::get().to(index),
@ -44,6 +45,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
async fn index() -> Result<NamedFile, actix_web::error::Error> { async fn index() -> Result<NamedFile, actix_web::error::Error> {
Ok(NamedFile::open( 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 config::{Config, ConfigError, Environment, File};
use failure::Error;
use serde::Deserialize; use serde::Deserialize;
use std::env; use std::env;
use std::fs;
use std::net::IpAddr; use std::net::IpAddr;
use std::sync::RwLock;
static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson"; static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson";
static CONFIG_FILE: &str = "config/config.hjson"; static CONFIG_FILE: &str = "config/config.hjson";
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Clone)]
pub struct Settings { pub struct Settings {
pub setup: Option<Setup>, pub setup: Option<Setup>,
pub database: Database, pub database: Database,
@ -20,7 +23,7 @@ pub struct Settings {
pub federation: Federation, pub federation: Federation,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Clone)]
pub struct Setup { pub struct Setup {
pub admin_username: String, pub admin_username: String,
pub admin_password: String, pub admin_password: String,
@ -28,7 +31,7 @@ pub struct Setup {
pub site_name: String, pub site_name: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Clone)]
pub struct RateLimitConfig { pub struct RateLimitConfig {
pub message: i32, pub message: i32,
pub message_per_second: i32, pub message_per_second: i32,
@ -38,7 +41,7 @@ pub struct RateLimitConfig {
pub register_per_second: i32, pub register_per_second: i32,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Clone)]
pub struct EmailConfig { pub struct EmailConfig {
pub smtp_server: String, pub smtp_server: String,
pub smtp_login: Option<String>, pub smtp_login: Option<String>,
@ -47,7 +50,7 @@ pub struct EmailConfig {
pub use_tls: bool, pub use_tls: bool,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Clone)]
pub struct Database { pub struct Database {
pub user: String, pub user: String,
pub password: String, pub password: String,
@ -65,12 +68,10 @@ pub struct Federation {
} }
lazy_static! { lazy_static! {
static ref SETTINGS: Settings = { static ref SETTINGS: RwLock<Settings> = RwLock::new(match Settings::init() {
match Settings::init() { Ok(c) => c,
Ok(c) => c, Err(e) => panic!("{}", e),
Err(e) => panic!("{}", e), });
}
};
} }
impl Settings { impl Settings {
@ -96,8 +97,8 @@ impl Settings {
} }
/// Returns the config as a struct. /// Returns the config as a struct.
pub fn get() -> &'static Self { pub fn get() -> Self {
&SETTINGS SETTINGS.read().unwrap().to_owned()
} }
/// Returns the postgres connection url. If LEMMY_DATABASE_URL is set, that is used, /// 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 { pub fn api_endpoint(&self) -> String {
format!("{}/api/v1", self.hostname) 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, GetPrivateMessages,
UserJoin, UserJoin,
GetComments, 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(); res.online = chat.sessions.len();
to_json_string(&user_operation, &res) 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 => { UserOperation::Search => {
do_user_operation::<Search, SearchResponse>(user_operation, data, &conn) do_user_operation::<Search, SearchResponse>(user_operation, data, &conn)
} }

View file

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

33
ui/package.json vendored
View file

@ -14,19 +14,20 @@
}, },
"keywords": [], "keywords": [],
"dependencies": { "dependencies": {
"@joeattardi/emoji-button": "^2.12.1",
"@types/autosize": "^3.0.6", "@types/autosize": "^3.0.6",
"@types/js-cookie": "^2.2.5", "@types/js-cookie": "^2.2.6",
"@types/jwt-decode": "^2.2.1", "@types/jwt-decode": "^2.2.1",
"@types/markdown-it": "^0.0.9", "@types/markdown-it": "^0.0.9",
"@types/markdown-it-container": "^2.0.2", "@types/markdown-it-container": "^2.0.2",
"@types/node": "^13.9.2", "@types/node": "^13.11.1",
"autosize": "^4.0.2", "autosize": "^4.0.2",
"bootswatch": "^4.3.1", "bootswatch": "^4.3.1",
"classcat": "^1.1.3", "classcat": "^4.0.2",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"emoji-short-name": "^1.0.0", "emoji-short-name": "^1.0.0",
"husky": "^4.2.3", "husky": "^4.2.5",
"i18next": "^19.3.3", "i18next": "^19.4.1",
"inferno": "^7.4.2", "inferno": "^7.4.2",
"inferno-i18next": "nimbusec-oss/inferno-i18next", "inferno-i18next": "nimbusec-oss/inferno-i18next",
"inferno-router": "^7.4.2", "inferno-router": "^7.4.2",
@ -37,26 +38,26 @@
"markdown-it-emoji": "^1.4.0", "markdown-it-emoji": "^1.4.0",
"mobius1-selectr": "^2.4.13", "mobius1-selectr": "^2.4.13",
"moment": "^2.24.0", "moment": "^2.24.0",
"prettier": "^1.18.2", "prettier": "^2.0.4",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"rxjs": "^6.4.0", "rxjs": "^6.5.5",
"terser": "^4.6.7", "terser": "^4.6.11",
"tippy.js": "^6.1.0", "tippy.js": "^6.1.1",
"toastify-js": "^1.7.0", "toastify-js": "^1.7.0",
"tributejs": "^5.1.2", "tributejs": "^5.1.3",
"twemoji": "^12.1.2", "twemoji": "^12.1.2",
"ws": "^7.2.3" "ws": "^7.2.3"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^6.5.1", "eslint": "^6.5.1",
"eslint-plugin-inferno": "^7.14.3", "eslint-plugin-inferno": "^7.14.3",
"eslint-plugin-jane": "^7.2.0", "eslint-plugin-jane": "^7.2.1",
"fuse-box": "^3.1.3", "fuse-box": "^3.1.3",
"lint-staged": "^10.0.8", "lint-staged": "^10.1.3",
"sortpack": "^2.1.2", "sortpack": "^2.1.4",
"ts-node": "^8.7.0", "ts-node": "^8.8.2",
"ts-transform-classcat": "^0.0.2", "ts-transform-classcat": "^1.0.0",
"ts-transform-inferno": "^4.0.2", "ts-transform-inferno": "^4.0.3",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },
"engines": { "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, toast,
setupTribute, setupTribute,
wsJsonToRes, wsJsonToRes,
emojiPicker,
} from '../utils'; } from '../utils';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import autosize from 'autosize'; import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js'; import Tribute from 'tributejs/src/Tribute.js';
import emojiShortName from 'emoji-short-name';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
interface CommentFormProps { interface CommentFormProps {
@ -69,6 +71,8 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
super(props, context); super(props, context);
this.tribute = setupTribute(); this.tribute = setupTribute();
this.setupEmojiPicker();
this.state = this.emptyState; this.state = this.emptyState;
if (this.props.node) { if (this.props.node) {
@ -158,8 +162,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
</button> </button>
{this.state.commentForm.content && ( {this.state.commentForm.content && (
<button <button
className={`btn btn-sm mr-2 btn-secondary ${this.state className={`btn btn-sm mr-2 btn-secondary ${
.previewMode && 'active'}`} this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)} onClick={linkEvent(this, this.handlePreviewToggle)}
> >
{i18n.t('preview')} {i18n.t('preview')}
@ -209,6 +214,15 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
<use xlinkHref="#icon-spinner"></use> <use xlinkHref="#icon-spinner"></use>
</svg> </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>
</div> </div>
</form> </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() { handleFinished() {
this.state.previewMode = false; this.state.previewMode = false;
this.state.loading = false; this.state.loading = false;
@ -242,6 +270,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
i.setState(i.state); i.setState(i.state);
} }
handleEmojiPickerClick(_i: CommentForm, event: any) {
emojiPicker.togglePicker(event.target);
}
handleCommentContentChange(i: CommentForm, event: any) { handleCommentContentChange(i: CommentForm, event: any) {
i.state.commentForm.content = event.target.value; i.state.commentForm.content = event.target.value;
i.setState(i.state); i.setState(i.state);

View file

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

View file

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

View file

@ -16,6 +16,7 @@ import {
Comment, Comment,
CommentResponse, CommentResponse,
PrivateMessage, PrivateMessage,
UserView,
PrivateMessageResponse, PrivateMessageResponse,
WebSocketJsonResponse, WebSocketJsonResponse,
} from '../interfaces'; } from '../interfaces';
@ -40,6 +41,7 @@ interface NavbarState {
messages: Array<PrivateMessage>; messages: Array<PrivateMessage>;
unreadCount: number; unreadCount: number;
siteName: string; siteName: string;
admins: Array<UserView>;
} }
export class Navbar extends Component<any, NavbarState> { export class Navbar extends Component<any, NavbarState> {
@ -53,6 +55,7 @@ export class Navbar extends Component<any, NavbarState> {
messages: [], messages: [],
expanded: false, expanded: false,
siteName: undefined, siteName: undefined,
admins: [],
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -179,6 +182,19 @@ export class Navbar extends Component<any, NavbarState> {
</li> </li>
</ul> </ul>
<ul class="navbar-nav ml-auto"> <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 ? ( {this.state.isLoggedIn ? (
<> <>
<li className="nav-item mt-1"> <li className="nav-item mt-1">
@ -298,7 +314,10 @@ export class Navbar extends Component<any, NavbarState> {
if (data.site && !this.state.siteName) { if (data.site && !this.state.siteName) {
this.state.siteName = data.site.name; this.state.siteName = data.site.name;
this.state.admins = data.admins;
WebSocketService.Instance.site = data.site; WebSocketService.Instance.site = data.site;
WebSocketService.Instance.admins = data.admins;
this.setState(this.state); 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() { requestNotificationPermission() {
if (UserService.Instance.user) { if (UserService.Instance.user) {
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
if (!Notification) { if (!Notification) {
toast(i18n.t('notifications_error'), 'danger'); toast(i18n.t('notifications_error'), 'danger');
return; return;

View file

@ -34,9 +34,11 @@ import {
randomStr, randomStr,
setupTribute, setupTribute,
setupTippy, setupTippy,
emojiPicker,
} from '../utils'; } from '../utils';
import autosize from 'autosize'; import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js'; import Tribute from 'tributejs/src/Tribute.js';
import emojiShortName from 'emoji-short-name';
import Selectr from 'mobius1-selectr'; import Selectr from 'mobius1-selectr';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
@ -92,6 +94,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this); this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
this.tribute = setupTribute(); this.tribute = setupTribute();
this.setupEmojiPicker();
this.state = this.emptyState; this.state = this.emptyState;
if (this.props.post) { if (this.props.post) {
@ -190,8 +194,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<form> <form>
<label <label
htmlFor="file-upload" htmlFor="file-upload"
className={`${UserService.Instance.user && className={`${
'pointer'} d-inline-block float-right text-muted h6 font-weight-bold`} UserService.Instance.user && 'pointer'
} d-inline-block float-right text-muted font-weight-bold`}
data-tippy-content={i18n.t('upload_image')} data-tippy-content={i18n.t('upload_image')}
> >
<svg class="icon icon-inline"> <svg class="icon icon-inline">
@ -284,8 +289,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
)} )}
{this.state.postForm.body && ( {this.state.postForm.body && (
<button <button
className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state className={`mt-1 mr-2 btn btn-sm btn-secondary ${
.previewMode && 'active'}`} this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)} onClick={linkEvent(this, this.handlePreviewToggle)}
> >
{i18n.t('preview')} {i18n.t('preview')}
@ -294,13 +300,22 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<a <a
href={markdownHelpUrl} href={markdownHelpUrl}
target="_blank" 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')} title={i18n.t('formatting_help')}
> >
<svg class="icon icon-inline"> <svg class="icon icon-inline">
<use xlinkHref="#icon-help-circle"></use> <use xlinkHref="#icon-help-circle"></use>
</svg> </svg>
</a> </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>
</div> </div>
{!this.props.post && ( {!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) { handlePostSubmit(i: PostForm, event: any) {
event.preventDefault(); event.preventDefault();
if (i.props.post) { 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) { parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg); let res = wsJsonToRes(msg);
if (msg.error) { if (msg.error) {

View file

@ -19,18 +19,19 @@ import {
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
import { PostForm } from './post-form'; import { PostForm } from './post-form';
import { IFramelyCard } from './iframely-card'; import { IFramelyCard } from './iframely-card';
import { UserListing } from './user-listing';
import { import {
md,
mdToHtml, mdToHtml,
canMod, canMod,
isMod, isMod,
isImage, isImage,
isVideo, isVideo,
getUnixTime, getUnixTime,
pictshareAvatarThumbnail,
showAvatars,
pictshareImage, pictshareImage,
setupTippy, setupTippy,
hostname, hostname,
previewLines,
} from '../utils'; } from '../utils';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
@ -415,20 +416,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<ul class="list-inline mb-0 text-muted small"> <ul class="list-inline mb-0 text-muted small">
<li className="list-inline-item"> <li className="list-inline-item">
<span>{i18n.t('by')} </span> <span>{i18n.t('by')} </span>
<Link <UserListing
className="text-body font-weight-bold" user={{
to={`/u/${post.creator_name}`} name: post.creator_name,
> avatar: post.creator_avatar,
{post.creator_avatar && showAvatars() && ( }}
<img />
height="32"
width="32"
src={pictshareAvatarThumbnail(post.creator_avatar)}
class="rounded-circle mr-1"
/>
)}
<span>{post.creator_name}</span>
</Link>
{this.isMod && ( {this.isMod && (
<span className="mx-1 badge badge-light"> <span className="mx-1 badge badge-light">
{i18n.t('mod')} {i18n.t('mod')}
@ -465,6 +458,24 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<MomentTime data={post} /> <MomentTime data={post} />
</span> </span>
</li> </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> <li className="list-inline-item"></li>
{this.state.upvotes !== this.state.score && ( {this.state.upvotes !== this.state.score && (
<> <>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

116
ui/src/index.tsx vendored
View file

@ -15,79 +15,85 @@ import { Communities } from './components/communities';
import { User } from './components/user'; import { User } from './components/user';
import { Modlog } from './components/modlog'; import { Modlog } from './components/modlog';
import { Setup } from './components/setup'; import { Setup } from './components/setup';
import { AdminSettings } from './components/admin-settings';
import { Inbox } from './components/inbox'; import { Inbox } from './components/inbox';
import { Search } from './components/search'; import { Search } from './components/search';
import { Sponsors } from './components/sponsors'; import { Sponsors } from './components/sponsors';
import { Symbols } from './components/symbols'; import { Symbols } from './components/symbols';
import { i18n } from './i18next'; import { i18n } from './i18next';
import { WebSocketService, UserService } from './services';
const container = document.getElementById('app'); const container = document.getElementById('app');
class Index extends Component<any, any> { class Index extends Component<any, any> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
WebSocketService.Instance;
UserService.Instance;
} }
render() { render() {
return ( return (
<Provider i18next={i18n}> <Provider i18next={i18n}>
<BrowserRouter> <BrowserRouter>
<Navbar /> <div>
<div class="mt-4 p-0 fl-1"> <Navbar />
<Switch> <div class="mt-4 p-0 fl-1">
<Route exact path={`/`} component={Main} /> <Switch>
<Route <Route exact path={`/`} component={Main} />
path={`/home/data_type/:data_type/listing_type/:listing_type/sort/:sort/page/:page`} <Route
component={Main} path={`/home/data_type/:data_type/listing_type/:listing_type/sort/:sort/page/:page`}
/> component={Main}
<Route path={`/login`} component={Login} /> />
<Route path={`/create_post`} component={CreatePost} /> <Route path={`/login`} component={Login} />
<Route path={`/create_community`} component={CreateCommunity} /> <Route path={`/create_post`} component={CreatePost} />
<Route <Route path={`/create_community`} component={CreateCommunity} />
path={`/create_private_message`} <Route
component={CreatePrivateMessage} path={`/create_private_message`}
/> component={CreatePrivateMessage}
<Route path={`/communities/page/:page`} component={Communities} /> />
<Route path={`/communities`} component={Communities} /> <Route
<Route path={`/post/:id/comment/:comment_id`} component={Post} /> path={`/communities/page/:page`}
<Route path={`/post/:id`} component={Post} /> component={Communities}
<Route />
path={`/c/:name/data_type/:data_type/sort/:sort/page/:page`} <Route path={`/communities`} component={Communities} />
component={Community} <Route
/> path={`/post/:id/comment/:comment_id`}
<Route path={`/community/:id`} component={Community} /> component={Post}
<Route path={`/c/:name`} component={Community} /> />
<Route <Route path={`/post/:id`} component={Post} />
path={`/u/:username/view/:view/sort/:sort/page/:page`} <Route
component={User} path={`/c/:name/data_type/:data_type/sort/:sort/page/:page`}
/> component={Community}
<Route path={`/user/:id`} component={User} /> />
<Route path={`/u/:username`} component={User} /> <Route path={`/community/:id`} component={Community} />
<Route path={`/inbox`} component={Inbox} /> <Route path={`/c/:name`} component={Community} />
<Route <Route
path={`/modlog/community/:community_id`} path={`/u/:username/view/:view/sort/:sort/page/:page`}
component={Modlog} component={User}
/> />
<Route path={`/modlog`} component={Modlog} /> <Route path={`/user/:id`} component={User} />
<Route path={`/setup`} component={Setup} /> <Route path={`/u/:username`} component={User} />
<Route <Route path={`/inbox`} component={Inbox} />
path={`/search/q/:q/type/:type/sort/:sort/page/:page`} <Route
component={Search} path={`/modlog/community/:community_id`}
/> component={Modlog}
<Route path={`/search`} component={Search} /> />
<Route path={`/sponsors`} component={Sponsors} /> <Route path={`/modlog`} component={Modlog} />
<Route <Route path={`/setup`} component={Setup} />
path={`/password_change/:token`} <Route path={`/admin`} component={AdminSettings} />
component={PasswordChange} <Route
/> path={`/search/q/:q/type/:type/sort/:sort/page/:page`}
</Switch> component={Search}
<Symbols /> />
<Route path={`/search`} component={Search} />
<Route path={`/sponsors`} component={Sponsors} />
<Route
path={`/password_change/:token`}
component={PasswordChange}
/>
</Switch>
<Symbols />
</div>
<Footer />
</div> </div>
<Footer />
</BrowserRouter> </BrowserRouter>
</Provider> </Provider>
); );

21
ui/src/interfaces.ts vendored
View file

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

View file

@ -40,6 +40,8 @@ import {
GetPrivateMessagesForm, GetPrivateMessagesForm,
GetCommentsForm, GetCommentsForm,
UserJoinForm, UserJoinForm,
GetSiteConfig,
SiteConfigForm,
MessageType, MessageType,
WebSocketJsonResponse, WebSocketJsonResponse,
} from '../interfaces'; } from '../interfaces';
@ -268,6 +270,12 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.GetSite, {})); 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) { public search(form: SearchForm) {
this.setAuth(form, false); this.setAuth(form, false);
this.ws.send(this.wsSendWrapper(UserOperation.Search, form)); this.ws.send(this.wsSendWrapper(UserOperation.Search, form));
@ -314,6 +322,11 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form)); 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) { private wsSendWrapper(op: UserOperation, data: MessageType) {
let send = { op: UserOperation[op], data: data }; let send = { op: UserOperation[op], data: data };
console.log(send); 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 emojiShortName from 'emoji-short-name';
import Toastify from 'toastify-js'; import Toastify from 'toastify-js';
import tippy from 'tippy.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 helpGuideUrl = '/docs/about_guide.html';
export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`; export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
export const sortingHelpUrl = `${helpGuideUrl}#sorting`; export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
@ -88,6 +89,14 @@ export const themes = [
'i386', 'i386',
]; ];
export const emojiPicker = new EmojiButton({
// Use the emojiShortName from native
style: 'twemoji',
theme: 'dark',
position: 'auto-start',
// TODO i18n
});
export function randomStr() { export function randomStr() {
return Math.random() return Math.random()
.toString(36) .toString(36)
@ -473,8 +482,9 @@ export function setupTribute(): Tribute {
{ {
trigger: ':', trigger: ':',
menuItemTemplate: (item: any) => { menuItemTemplate: (item: any) => {
let emoji = `:${item.original.key}:`; let shortName = `:${item.original.key}:`;
return `${item.original.val} ${emoji}`; let twemojiIcon = twemoji.parse(item.original.val);
return `${twemojiIcon} ${shortName}`;
}, },
selectTemplate: (item: any) => { selectTemplate: (item: any) => {
return `:${item.original.key}:`; return `:${item.original.key}:`;
@ -824,6 +834,14 @@ function randomHsl() {
return `hsla(${Math.random() * 360}, 100%, 50%, 1)`; 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 { export function hostname(url: string): string {
return new URL(url).hostname; 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", "mods": "mods",
"moderates": "Moderates", "moderates": "Moderates",
"settings": "Settings", "settings": "Settings",
"admin_settings": "Admin Settings",
"site_config": "Site Configuration",
"remove_as_mod": "remove as mod", "remove_as_mod": "remove as mod",
"appoint_as_mod": "appoint as mod", "appoint_as_mod": "appoint as mod",
"modlog": "Modlog", "modlog": "Modlog",
@ -78,6 +80,7 @@
"unban": "unban", "unban": "unban",
"unban_from_site": "unban from site", "unban_from_site": "unban from site",
"banned": "banned", "banned": "banned",
"banned_users": "Banned Users",
"save": "save", "save": "save",
"unsave": "unsave", "unsave": "unsave",
"create": "create", "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:", "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_patreon": "Support on Patreon",
"support_on_liberapay": "Support on Liberapay", "support_on_liberapay": "Support on Liberapay",
"support_on_open_collective": "Support on OpenCollective",
"donate_to_lemmy": "Donate to Lemmy", "donate_to_lemmy": "Donate to Lemmy",
"donate": "Donate", "donate": "Donate",
"general_sponsors": "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>.", "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.", "not_logged_in": "Not logged in.",
"logged_in": "Logged in.", "logged_in": "Logged in.",
"site_saved": "Site Saved.",
"community_ban": "You have been banned from this community.", "community_ban": "You have been banned from this community.",
"site_ban": "You have been banned from the site", "site_ban": "You have been banned from the site",
"couldnt_create_comment": "Couldn't create comment.", "couldnt_create_comment": "Couldn't create comment.",
@ -251,5 +256,6 @@
"couldnt_update_private_message": "Couldn't update private message.", "couldnt_update_private_message": "Couldn't update private message.",
"time": "Time", "time": "Time",
"action": "Action", "action": "Action",
"emoji_picker": "Emoji Picker",
"block_leaving": "Are you sure you want to leave?" "block_leaving": "Are you sure you want to leave?"
} }

View file

@ -4,13 +4,15 @@
"no_posts": "Sin publicaciones.", "no_posts": "Sin publicaciones.",
"create_a_post": "Crear una publicación", "create_a_post": "Crear una publicación",
"create_post": "Crear 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", "posts": "Publicaciones",
"related_posts": "Estas publicaciones podrían estar relacionadas", "related_posts": "Estas publicaciones podrían estar relacionadas",
"cross_posts": "Este link también ha sido publicado en:", "cross_posts": "Este link también ha sido publicado en:",
"cross_post": "cross-post", "cross_post": "cross-post",
"comments": "Comentarios", "comments": "Comentarios",
"number_of_comments": "{{count}} Comentarios", "number_of_comments": "{{count}} Comentario",
"number_of_comments_plural": "{{count}} Comentarios",
"remove_comment": "Eliminar Comentarios", "remove_comment": "Eliminar Comentarios",
"communities": "Comunidades", "communities": "Comunidades",
"users": "Usuarios", "users": "Usuarios",
@ -20,7 +22,8 @@
"subscribed_to_communities": "Suscrito a <1>comunidades</1>", "subscribed_to_communities": "Suscrito a <1>comunidades</1>",
"trending_communities": "<1>Comunidades</1> en tendencia", "trending_communities": "<1>Comunidades</1> en tendencia",
"list_of_communities": "Lista de comunidades", "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.", "community_reqs": "minúsculas, guión bajo, y sin espacios.",
"create_private_message": "Crear Mensaje Privado", "create_private_message": "Crear Mensaje Privado",
"send_secure_message": "Enviar Mensaje Seguro", "send_secure_message": "Enviar Mensaje Seguro",
@ -63,8 +66,7 @@
"delete": "eliminar", "delete": "eliminar",
"deleted": "eliminado", "deleted": "eliminado",
"delete_account": "Eliminar Cuenta", "delete_account": "Eliminar Cuenta",
"delete_account_confirm": "delete_account_confirm": "Aviso: esta acción eliminará permanentemente tu información. Introduce tu contraseña para continuar",
"Aviso: esta acción eliminará permanentemente tu información. Introduce tu contraseña para continuar",
"restore": "restaurar", "restore": "restaurar",
"ban": "expulsar", "ban": "expulsar",
"ban_from_site": "expulsar del sitio", "ban_from_site": "expulsar del sitio",
@ -77,10 +79,14 @@
"creator": "creador", "creator": "creador",
"username": "Nombre de Usuario", "username": "Nombre de Usuario",
"email_or_username": "Correo o Usuario", "email_or_username": "Correo o Usuario",
"number_of_users": "{{count}} Usuarios", "number_of_users": "{{count}} Usuario",
"number_of_subscribers": "{{count}} Suscriptores", "number_of_users_plural": "{{count}} Usuarios",
"number_of_points": "{{count}} Puntos", "number_of_subscribers": "{{count}} Suscriptor",
"number_online": "{{count}} Usuarios En Línea", "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", "name": "Nombre",
"title": "Titulo", "title": "Titulo",
"category": "Categoría", "category": "Categoría",
@ -120,8 +126,7 @@
"login_sign_up": "Iniciar sesión / Crear cuenta", "login_sign_up": "Iniciar sesión / Crear cuenta",
"login": "Iniciar sesión", "login": "Iniciar sesión",
"sign_up": "Crear cuenta", "sign_up": "Crear cuenta",
"notifications_error": "notifications_error": "Notificaciones de escritorio no disponibles en tu navegador. Prueba Firefox o Chrome.",
"Notificaciones de escritorio no disponibles en tu navegador. Prueba Firefox o Chrome.",
"unread_messages": "Mensajes no leídos", "unread_messages": "Mensajes no leídos",
"messages": "Mensajes", "messages": "Mensajes",
"password": "Contraseña", "password": "Contraseña",
@ -134,8 +139,7 @@
"no_email_setup": "Este servidor no ha activado correctamente el correo.", "no_email_setup": "Este servidor no ha activado correctamente el correo.",
"email": "Correo electrónico", "email": "Correo electrónico",
"matrix_user_id": "Usuario Matricial", "matrix_user_id": "Usuario Matricial",
"private_message_disclaimer": "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.",
"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", "send_notifications_to_email": "Enviar notificaciones al correo",
"optional": "Opcional", "optional": "Opcional",
"expires": "Expira", "expires": "Expira",
@ -165,14 +169,12 @@
"theme": "Tema", "theme": "Tema",
"sponsors": "Patrocinadores", "sponsors": "Patrocinadores",
"sponsors_of_lemmy": "Patrocinadores de Lemmy", "sponsors_of_lemmy": "Patrocinadores de Lemmy",
"sponsor_message": "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:",
"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_patreon": "Apoyo en Patreon",
"support_on_liberapay": "Apoyo en Liberapay", "support_on_liberapay": "Apoyo en Liberapay",
"donate_to_lemmy": "Donar a Lemmy", "donate_to_lemmy": "Donar a Lemmy",
"donate": "Donar", "donate": "Donar",
"general_sponsors": "general_sponsors": "Los Patrocinadores Generales son aquellos que señaron entre $10 y $39 a Lemmy.",
"Los Patrocinadores Generales son aquellos que señaron entre $10 y $39 a Lemmy.",
"crypto": "Crypto", "crypto": "Crypto",
"bitcoin": "Bitcoin", "bitcoin": "Bitcoin",
"ethereum": "Ethereum", "ethereum": "Ethereum",
@ -188,8 +190,7 @@
"yes": "sí", "yes": "sí",
"no": "no", "no": "no",
"powered_by": "Impulsado por", "powered_by": "Impulsado por",
"landing_0": "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>.",
"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.", "not_logged_in": "No has iniciado sesión.",
"logged_in": "Has iniciado sesión.", "logged_in": "Has iniciado sesión.",
"community_ban": "Has sido expulsado de esta comunidad.", "community_ban": "Has sido expulsado de esta comunidad.",
@ -204,12 +205,9 @@
"couldnt_find_community": "No se pudo encontrar la comunidad.", "couldnt_find_community": "No se pudo encontrar la comunidad.",
"couldnt_update_community": "No se pudo actualizar la comunidad.", "couldnt_update_community": "No se pudo actualizar la comunidad.",
"community_already_exists": "Esta comunidad ya existe.", "community_already_exists": "Esta comunidad ya existe.",
"community_moderator_already_exists": "community_moderator_already_exists": "Este moderador de la comunidad ya existe.",
"Este moderador de la comunidad ya existe.", "community_follower_already_exists": "Este seguidor de la comunidad ya existe.",
"community_follower_already_exists": "community_user_already_banned": "Este usuario de la comunidad ya fue expulsado.",
"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_create_post": "No se pudo crear la publicación.",
"couldnt_like_post": "No se pudo gustar la publicación.", "couldnt_like_post": "No se pudo gustar la publicación.",
"couldnt_find_post": "No se pudo encontrar la publicación.", "couldnt_find_post": "No se pudo encontrar la publicación.",
@ -220,21 +218,31 @@
"not_an_admin": "No es un administrador.", "not_an_admin": "No es un administrador.",
"site_already_exists": "El sitio ya existe.", "site_already_exists": "El sitio ya existe.",
"couldnt_update_site": "No se pudo actualizar el sitio.", "couldnt_update_site": "No se pudo actualizar el sitio.",
"couldnt_find_that_username_or_email": "couldnt_find_that_username_or_email": "No se pudo encontrar ese nombre de usuario o correo electrónico.",
"No se pudo encontrar ese nombre de usuario o correo electrónico.",
"password_incorrect": "Contraseña incorrecta.", "password_incorrect": "Contraseña incorrecta.",
"passwords_dont_match": "Las contraseñas no coinciden.", "passwords_dont_match": "Las contraseñas no coinciden.",
"admin_already_created": "Lo sentimos, ya hay un adminisitrador.", "admin_already_created": "Lo sentimos, ya hay un adminisitrador.",
"user_already_exists": "El usuario ya existe.", "user_already_exists": "El usuario ya existe.",
"email_already_exists": "El correo ya está en uso.", "email_already_exists": "El correo ya está en uso.",
"couldnt_update_user": "No se pudo actualizar el usuario.", "couldnt_update_user": "No se pudo actualizar el usuario.",
"system_err_login": "system_err_login": "Error del sistema. Intente cerrar sesión e ingresar de nuevo.",
"Error del sistema. Intente cerrar sesión e ingresar de nuevo.",
"couldnt_create_private_message": "No se pudo crear el mensaje privado.", "couldnt_create_private_message": "No se pudo crear el mensaje privado.",
"no_private_message_edit_allowed": "no_private_message_edit_allowed": "Sin permisos para editar el mensaje privado.",
"Sin permisos para editar el mensaje privado.",
"couldnt_update_private_message": "No se pudo actualizar el mensaje privado.", "couldnt_update_private_message": "No se pudo actualizar el mensaje privado.",
"old": "Antiguo", "old": "Antiguo",
"time": "Tiempo", "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é.", "no_private_message_edit_allowed": "Pas autorisé à modifier un message privé.",
"couldnt_update_private_message": "Impossible de modifier un message privé.", "couldnt_update_private_message": "Impossible de modifier un message privé.",
"time": "Temps", "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" lodash "^4.17.13"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@popperjs/core@^2.1.1": "@fortawesome/fontawesome-common-types@^0.2.28":
version "2.1.1" version "0.2.28"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.1.1.tgz#12c572ab88ef7345b43f21883fca26631c223085" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz#1091bdfe63b3f139441e9cba27aa022bff97d8b2"
integrity sha512-sLqWxCzC5/QHLhziXSCAksBxHfOnQlhPRVgPK0egEw+ktWvG75T2k+aYWVjVh9+WKeT3tlG3ZNbZQvZLmfuOIw== 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": "@samverschueren/stream-to-observable@^0.3.0":
version "0.3.0" version "0.3.0"
@ -162,10 +207,10 @@
dependencies: dependencies:
"@types/sizzle" "*" "@types/sizzle" "*"
"@types/js-cookie@^2.2.5": "@types/js-cookie@^2.2.6":
version "2.2.5" version "2.2.6"
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.5.tgz#38dfaacae8623b37cc0b0d27398e574e3fc28b1e" resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f"
integrity sha512-cpmwBRcHJmmZx0OGU7aPVwGWGbs4iKwVYchk9iuMtxNCA2zorwdaTz4GkLgs2WGxiRZRFKnV1k6tRUHX7tBMxg== integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==
"@types/json-schema@^7.0.3": "@types/json-schema@^7.0.3":
version "7.0.4" version "7.0.4"
@ -196,10 +241,10 @@
dependencies: dependencies:
"@types/linkify-it" "*" "@types/linkify-it" "*"
"@types/node@^13.9.2": "@types/node@^13.11.1":
version "13.9.2" version "13.11.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.2.tgz#ace1880c03594cc3e80206d96847157d8e7fa349" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7"
integrity sha512-bnoqK579sAYrQbp73wwglccjJ4sfRdKU7WNEZ5FW4K2U6Kc0/eZ5kvXG0JKsEKFB50zrFmfFt52/cvBbZa7eXg== integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==
"@types/normalize-package-data@^2.4.0": "@types/normalize-package-data@^2.4.0":
version "2.4.0" version "2.4.0"
@ -749,6 +794,14 @@ chalk@^3.0.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.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: chardet@^0.4.0:
version "0.4.2" version "0.4.2"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" 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" isobject "^3.0.0"
static-extend "^0.1.1" static-extend "^0.1.1"
classcat@^1.1.3: classcat@^4.0.2:
version "1.1.3" version "4.0.2"
resolved "https://registry.yarnpkg.com/classcat/-/classcat-1.1.3.tgz#ec748eecd962ec195a5d8f73f01d67c3d9040912" resolved "https://registry.yarnpkg.com/classcat/-/classcat-4.0.2.tgz#bd5d51b656e01e9cdd21c1aae3d29ed035a52126"
integrity sha512-nuf6HJ5RlEgUUPqN/giIy1wsfA0LJwCHpo/aMGMwEIAxYypbLW/ZdPH4SNrF+OwdrkL3wxJmAs4GPyoE3ZkQ4w== integrity sha512-RlMPOPp8VDu3CJOUVorPumhz/CI+t9ft6f0uexxxCguk28/M+Kf27eQXjNWeDTisEQWei/30oDfITOQqr1TNpQ==
clean-css@^4.1.9: clean-css@^4.1.9:
version "4.2.3" version "4.2.3"
@ -890,10 +943,10 @@ commander@^4.0.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83"
integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw==
compare-versions@^3.5.1: compare-versions@^3.6.0:
version "3.5.1" version "3.6.0"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
integrity sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg== integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
component-emitter@^1.2.1: component-emitter@^1.2.1:
version "1.3.0" version "1.3.0"
@ -1283,10 +1336,10 @@ eslint-plugin-inferno@^7.14.3:
object.values "^1.1.0" object.values "^1.1.0"
resolve "^1.12.0" resolve "^1.12.0"
eslint-plugin-jane@^7.2.0: eslint-plugin-jane@^7.2.1:
version "7.2.0" version "7.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jane/-/eslint-plugin-jane-7.2.0.tgz#a2454a6700c644e6c86821ca294adf303e75eddc" resolved "https://registry.yarnpkg.com/eslint-plugin-jane/-/eslint-plugin-jane-7.2.1.tgz#5ffba9ce75e0a5e5dbe3918fc0c5332d2cd89c13"
integrity sha512-/BPZrfxWX9T45gJSf4/2GHfBYgsBYTW7StAQfxL8PxWABZIQKWPWy/5ZokX7UaJlgKHAoC42rJHCQLK5hmfJNA== integrity sha512-hUmhEkHTDq6lQ4oLWZV5cLut9L67fcTiy0USbTsEOx658i9Jdikedt8NJhtamRqO5OUHBGSPU0JkOqBtVNUD+A==
dependencies: dependencies:
"@typescript-eslint/eslint-plugin" "2.24.0" "@typescript-eslint/eslint-plugin" "2.24.0"
"@typescript-eslint/parser" "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-prettier "3.1.2"
eslint-plugin-promise "4.2.1" eslint-plugin-promise "4.2.1"
eslint-plugin-react "7.19.0" 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-unicorn "17.2.0"
eslint-plugin-jest@23.8.2: 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" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==
eslint-plugin-react-hooks@2.5.0: eslint-plugin-react-hooks@2.5.1:
version "2.5.0" version "2.5.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.0.tgz#c50ab7ca5945ce6d1cf8248d9e185c80b54171b6" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.1.tgz#4ef5930592588ce171abeb26f400c7fbcbc23cd0"
integrity sha512-bzvdX47Jx847bgAYf0FPX3u1oxU+mKU8tqrpj4UX9A96SbAmj/HVEefEy6rJUog5u8QIlOPTKZcBpGn5kkKfAQ== integrity sha512-Y2c4b55R+6ZzwtTppKwSmK/Kar8AdLiC2f9NADCuxbcTgPPg41Gyqa6b9GppgXSvCtkRw43ZE86CT5sejKC6/g==
eslint-plugin-react@7.19.0: eslint-plugin-react@7.19.0:
version "7.19.0" version "7.19.0"
@ -1829,6 +1882,14 @@ fliplog@^0.3.13:
dependencies: dependencies:
chain-able "^1.0.1" 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: for-in@^1.0.1, for-in@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" resolved "https://registry.yarnpkg.com/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" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
husky@^4.2.3: husky@^4.2.5:
version "4.2.3" version "4.2.5"
resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.3.tgz#3b18d2ee5febe99e27f2983500202daffbc3151e" resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.5.tgz#2b4f7622673a71579f901d9885ed448394b5fa36"
integrity sha512-VxTsSTRwYveKXN4SaH1/FefRJYCtx+wx04sSVcOpD7N2zjoHxa+cEJ07Qg5NmV3HAK+IRKOyNVpi2YBIVccIfQ== integrity sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==
dependencies: dependencies:
chalk "^3.0.0" chalk "^4.0.0"
ci-info "^2.0.0" ci-info "^2.0.0"
compare-versions "^3.5.1" compare-versions "^3.6.0"
cosmiconfig "^6.0.0" cosmiconfig "^6.0.0"
find-versions "^3.2.0" find-versions "^3.2.0"
opencollective-postinstall "^2.0.2" opencollective-postinstall "^2.0.2"
@ -2198,10 +2259,10 @@ husky@^4.2.3:
slash "^3.0.0" slash "^3.0.0"
which-pm-runs "^1.0.0" which-pm-runs "^1.0.0"
i18next@^19.3.3: i18next@^19.4.1:
version "19.3.3" version "19.4.1"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.3.3.tgz#04bd79b315e5fe2c87ab8f411e5d55eda0a17bd8" resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.4.1.tgz#4929d15d3d01e4712350a368d005cefa50ff5455"
integrity sha512-CnuPqep5/JsltkGvQqzYN4d79eCe0TreCBRF3a8qHHi8x4SON1qqZ/pvR2X7BfNkNqpA5HXIqw0E731H+VsgSg== integrity sha512-dC3ue15jkLebN2je4xEjfjVYd/fSAo+UVK9f+JxvceCJRowkI+S0lGohgKejqU+FYLfvw9IAPylIIEWwR8Djrg==
dependencies: dependencies:
"@babel/runtime" "^7.3.1" "@babel/runtime" "^7.3.1"
@ -2814,10 +2875,10 @@ linkify-it@^2.0.0:
dependencies: dependencies:
uc.micro "^1.0.1" uc.micro "^1.0.1"
lint-staged@^10.0.8: lint-staged@^10.1.3:
version "10.0.8" version "10.1.3"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.0.8.tgz#0f7849cdc336061f25f5d4fcbcfa385701ff4739" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.1.3.tgz#da27713d3ac519da305381b4de87d5f866b1d2f1"
integrity sha512-Oa9eS4DJqvQMVdywXfEor6F4vP+21fPHF8LUXgBbVWUSWBddjqsvO6Bv1LwMChmgQZZqwUvgJSHlu8HFHAPZmA== integrity sha512-o2OkLxgVns5RwSC5QF7waeAjJA5nz5gnUfqL311LkZcFipKV7TztrSlhNUK5nQX9H0E5NELAdduMQ+M/JPT7RQ==
dependencies: dependencies:
chalk "^3.0.0" chalk "^3.0.0"
commander "^4.0.1" commander "^4.0.1"
@ -3612,10 +3673,10 @@ prettier-linter-helpers@^1.0.0:
dependencies: dependencies:
fast-diff "^1.1.2" fast-diff "^1.1.2"
prettier@^1.18.2: prettier@^2.0.4:
version "1.19.1" version "2.0.4"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.4.tgz#2d1bae173e355996ee355ec9830a7a1ee05457ef"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== integrity sha512-SVJIQ51spzFDvh4fIbCLvciiDMCrRhlN3mbZvv/+ycjvmF5E73bKdGfU8QDLNmjYJf+lsGnDBC4UUnvTe5OO0w==
pretty-time@^0.2.0: pretty-time@^0.2.0:
version "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" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ= 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" version "6.5.4"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c"
integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==
dependencies: dependencies:
tslib "^1.9.0" 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: safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 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" source-map-resolve "^0.5.0"
use "^3.1.0" use "^3.1.0"
sortpack@^2.1.2: sortpack@^2.1.4:
version "2.1.2" version "2.1.4"
resolved "https://registry.yarnpkg.com/sortpack/-/sortpack-2.1.2.tgz#25bf86f2923c81f43a00a2166ff4d271fafeed11" resolved "https://registry.yarnpkg.com/sortpack/-/sortpack-2.1.4.tgz#a2e251c5868455135cc41d3c98a53756a6de5282"
integrity sha512-43fSND1vmAdyfgC38aOkVxZBV331f4blF8acjwQmx7Gba4nuL2ene/Cq5eixNmDhKA/qQHnvSeAl+jEWb31rfg== integrity sha512-RGD0l9kGmuPelXMT8WMMiSv1MkUkaqElB39nMkboIaqVkYns1aaNx263B2EE5QzF1YVUOrBlXnQpd7RX68SSow==
source-map-resolve@^0.5.0: source-map-resolve@^0.5.0:
version "0.5.3" 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" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== 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: table@^5.2.3:
version "5.4.6" version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" 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" slice-ansi "^2.1.0"
string-width "^3.0.0" string-width "^3.0.0"
terser@^4.6.7: terser@^4.6.11:
version "4.6.7" version "4.6.11"
resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.7.tgz#478d7f9394ec1907f0e488c5f6a6a9a2bad55e72" resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.11.tgz#12ff99fdd62a26de2a82f508515407eb6ccd8a9f"
integrity sha512-fmr7M1f7DBly5cX2+rFDvmGBAaaZyPrHYK4mMdHEDAdNTqXSZgSOfqsfGq2HqPGT/1V0foZZuCZFx8CHKgAk3g== integrity sha512-76Ynm7OXUG5xhOpblhytE7X58oeNSmC8xnNhjWVo8CksHit0U0kO4hfNbPrrYwowLWFgM2n9L176VNx2QaHmtA==
dependencies: dependencies:
commander "^2.20.0" commander "^2.20.0"
source-map "~0.6.1" 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" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= 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: tiny-invariant@^1.0.2:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" 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" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tippy.js@^6.1.0: tippy.js@^6.1.1:
version "6.1.0" version "6.1.1"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.1.0.tgz#9c58b94f92f3044d5e861b9d83da3c2a6d3d4323" resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.1.1.tgz#9ed09aa4f9c47fb06a0e280e03055f898f5ddfff"
integrity sha512-cRFydlVZlvo4soQSUfVNbH2K77zDUhDAzaAjxseyn81gGIa+j72y98yDL2yB0n8gas/E+Zlr1iOyR5ckslUFqA== integrity sha512-Sk+FPihack9XFbPOc2jRbn6iRLA9my2a8qhaGY6wwD3EeW57/xY5PAPkZOutKVYDWLyNZ/laCkJqg7QJG/gqQw==
dependencies: dependencies:
"@popperjs/core" "^2.1.1" "@popperjs/core" "^2.2.0"
tmp@^0.0.33: tmp@^0.0.33:
version "0.0.33" version "0.0.33"
@ -4581,15 +4659,15 @@ tough-cookie@~2.4.3:
psl "^1.1.24" psl "^1.1.24"
punycode "^1.4.1" punycode "^1.4.1"
tributejs@^5.1.2: tributejs@^5.1.3:
version "5.1.2" version "5.1.3"
resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-5.1.2.tgz#d8492d974d3098d6016248d689fb063cda6e77f7" resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-5.1.3.tgz#980600fc72865be5868893078b4bfde721129eae"
integrity sha512-R9ff/q6w4T5f3Y9+RL+qinog3X1eAj1UnR/yfZaGJ8D3wuJs4/vicrGYul9+fgS9EJ/iYgwARekTb92xwark0g== integrity sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==
ts-node@^8.7.0: ts-node@^8.8.2:
version "8.7.0" version "8.8.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.7.0.tgz#266186947596bef9f3a034687595b30e31b20976" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.8.2.tgz#0b39e690bee39ea5111513a9d2bcdc0bc121755f"
integrity sha512-s659CsHrsxaRVDEleuOkGvbsA0rWHtszUNEt1r0CgAFN5ZZTQtDzpsluS7W5pOGJIa1xZE8R/zK4dEs+ldFezg== integrity sha512-duVj6BpSpUpD/oM4MfhO98ozgkp3Gt9qIp3jGxwU2DFvl/3IRaEAvbLa8G60uS7C77457e/m5TMowjedeRxI1Q==
dependencies: dependencies:
arg "^4.1.0" arg "^4.1.0"
diff "^4.0.1" diff "^4.0.1"
@ -4597,17 +4675,20 @@ ts-node@^8.7.0:
source-map-support "^0.5.6" source-map-support "^0.5.6"
yn "3.1.1" yn "3.1.1"
ts-transform-classcat@^0.0.2: ts-transform-classcat@^1.0.0:
version "0.0.2" version "1.0.0"
resolved "https://registry.yarnpkg.com/ts-transform-classcat/-/ts-transform-classcat-0.0.2.tgz#2386c9418f3a7c1f03261ff51225b70d0a7664fb" resolved "https://registry.yarnpkg.com/ts-transform-classcat/-/ts-transform-classcat-1.0.0.tgz#6ae1be1b32f1f3c6b1c4232daf8a28e3ced0b62f"
integrity sha512-7laOOhgVxWVqvhK10mIEfedJx2nnNOS8J4P/6a/ehXtHFvsBVRRS9/FcTifgzJweOScZsF5BRD5VOGeNidMSqQ== integrity sha512-LWXEYvBwHDOqBBtoDWSUmbPMsw8FI9vD4XZm98RgziN9UCIj5MRtpmXuP5YYoimCTlPU+D4TFR3IqS+5xSzWsQ==
dependencies:
typescript "^2.6.2"
ts-transform-inferno@^4.0.2: ts-transform-inferno@^4.0.3:
version "4.0.2" version "4.0.3"
resolved "https://registry.yarnpkg.com/ts-transform-inferno/-/ts-transform-inferno-4.0.2.tgz#06b9be45edf874ba7a6ebfb6107ba782509c6afe" resolved "https://registry.yarnpkg.com/ts-transform-inferno/-/ts-transform-inferno-4.0.3.tgz#2cc0eb125abdaff24b8298106a618ab7c6319edc"
integrity sha512-CZb4+w/2l2zikPZ/c51fi3n+qnR2HCEfAS73oGQB80aqRLffkZqm25kYYTMmqUW2+oVfs4M5AZa0z14cvxlQ5w== 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: tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
version "1.10.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" resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-12.1.3.tgz#916c0153e77bd5f1011e7a99cbeacf52e43c9371"
integrity sha512-ND4LZXF4X92/PFrzSgGkq6KPPg8swy/U0yRw1k/+izWRVmq1HYi3khPwV3XIB6FRudgVICAaBhJfW8e8G3HC7Q== integrity sha512-ND4LZXF4X92/PFrzSgGkq6KPPg8swy/U0yRw1k/+izWRVmq1HYi3khPwV3XIB6FRudgVICAaBhJfW8e8G3HC7Q==
twemoji@^12.1.2: twemoji@^12.1.2, twemoji@^12.1.5:
version "12.1.5" version "12.1.5"
resolved "https://registry.yarnpkg.com/twemoji/-/twemoji-12.1.5.tgz#a961fb65a1afcb1f729ad7e59391f9fe969820b9" resolved "https://registry.yarnpkg.com/twemoji/-/twemoji-12.1.5.tgz#a961fb65a1afcb1f729ad7e59391f9fe969820b9"
integrity sha512-B0PBVy5xomwb1M/WZxf/IqPZfnoIYy1skXnlHjMwLwTNfZ9ljh8VgWQktAPcJXu8080WoEh6YwQGPVhDVqvrVQ== integrity sha512-B0PBVy5xomwb1M/WZxf/IqPZfnoIYy1skXnlHjMwLwTNfZ9ljh8VgWQktAPcJXu8080WoEh6YwQGPVhDVqvrVQ==
@ -4673,11 +4754,6 @@ type-is@~1.6.17, type-is@~1.6.18:
media-typer "0.3.0" media-typer "0.3.0"
mime-types "~2.1.24" 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: typescript@^3.8.3:
version "3.8.3" version "3.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
@ -4890,6 +4966,11 @@ xregexp@^4.3.0:
dependencies: dependencies:
"@babel/runtime-corejs3" "^7.8.3" "@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: yaml@^1.7.2:
version "1.7.2" version "1.7.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.7.2.tgz#f26aabf738590ab61efaca502358e48dc9f348b2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.7.2.tgz#f26aabf738590ab61efaca502358e48dc9f348b2"