]> Untitled Git - lemmy.git/commitdiff
Adding emoji support.
authorDessalines <tyhou13@gmx.com>
Thu, 29 Aug 2019 23:14:28 +0000 (16:14 -0700)
committerDessalines <tyhou13@gmx.com>
Thu, 29 Aug 2019 23:14:28 +0000 (16:14 -0700)
115 files changed:
.dockerignore
.gitattributes [new file with mode: 0644]
.github/FUNDING.yml [new file with mode: 0644]
.gitignore [new file with mode: 0644]
README.md
ansible/ansible.cfg [new file with mode: 0644]
ansible/inventory.example [new file with mode: 0644]
ansible/lemmy.yml [new file with mode: 0644]
ansible/templates/env [new file with mode: 0644]
ansible/templates/nginx.conf [new file with mode: 0644]
docker-compose.yml [deleted file]
docker/dev/.env [new file with mode: 0644]
docker/dev/Dockerfile [moved from Dockerfile with 96% similarity]
docker/dev/deploy.sh [new file with mode: 0755]
docker/dev/docker-compose.yml [new file with mode: 0644]
docker/dev/docker_update.sh [moved from docker_update.sh with 74% similarity]
docker/docker-compose.yml [deleted file]
docker/docker_db_backup.sh [new file with mode: 0755]
docker/prod/.env [new file with mode: 0644]
docker/prod/docker-compose.yml [new file with mode: 0644]
docker/prod/nginx.conf [new file with mode: 0644]
docker_db_backup.sh [deleted file]
docs/api.md
docs/goals.md
docs/ranking.md
server/Cargo.lock
server/Cargo.toml
server/Dockerfile.dev [deleted file]
server/Dockerfile.prod [deleted file]
server/migrations/2019-06-01-222649_remove_admin/down.sql [new file with mode: 0644]
server/migrations/2019-06-01-222649_remove_admin/up.sql [new file with mode: 0644]
server/migrations/2019-08-11-000918_add_nsfw_columns/down.sql [new file with mode: 0644]
server/migrations/2019-08-11-000918_add_nsfw_columns/up.sql [new file with mode: 0644]
server/migrations/2019-08-29-040006_add_community_count/down.sql [new file with mode: 0644]
server/migrations/2019-08-29-040006_add_community_count/up.sql [new file with mode: 0644]
server/src/api/comment.rs
server/src/api/community.rs
server/src/api/mod.rs
server/src/api/post.rs
server/src/api/site.rs
server/src/api/user.rs
server/src/apub.rs
server/src/db/category.rs
server/src/db/comment.rs
server/src/db/comment_view.rs
server/src/db/community.rs
server/src/db/community_view.rs
server/src/db/mod.rs
server/src/db/moderator.rs
server/src/db/post.rs
server/src/db/post_view.rs
server/src/db/user.rs
server/src/db/user_view.rs
server/src/lib.rs
server/src/main.rs
server/src/schema.rs
server/src/websocket/server.rs
server/stack.dev.yaml [deleted file]
server/stack.prod.yaml [deleted file]
skaffold.yaml [deleted file]
ui/Dockerfile.dev [deleted file]
ui/Dockerfile.prod [deleted file]
ui/assets/favicon.svg
ui/fuse.js
ui/package.json
ui/src/components/comment-form.tsx
ui/src/components/comment-node.tsx
ui/src/components/comment-nodes.tsx
ui/src/components/communities.tsx
ui/src/components/community-form.tsx
ui/src/components/community.tsx
ui/src/components/create-community.tsx
ui/src/components/create-post.tsx
ui/src/components/footer.tsx
ui/src/components/home.tsx [deleted file]
ui/src/components/inbox.tsx
ui/src/components/login.tsx
ui/src/components/main.tsx
ui/src/components/modlog.tsx
ui/src/components/moment-time.tsx
ui/src/components/navbar.tsx
ui/src/components/post-form.tsx
ui/src/components/post-listing.tsx
ui/src/components/post-listings.tsx
ui/src/components/post.tsx
ui/src/components/search.tsx
ui/src/components/setup.tsx
ui/src/components/sidebar.tsx
ui/src/components/site-form.tsx
ui/src/components/sponsors.tsx
ui/src/components/symbols.tsx
ui/src/components/user.tsx
ui/src/css/main.css
ui/src/i18next.ts [new file with mode: 0644]
ui/src/index.html
ui/src/index.tsx
ui/src/interfaces.ts
ui/src/services/UserService.ts
ui/src/services/WebSocketService.ts
ui/src/translations/de.ts [new file with mode: 0644]
ui/src/translations/en.ts [new file with mode: 0644]
ui/src/translations/eo.ts [new file with mode: 0644]
ui/src/translations/es.ts [new file with mode: 0644]
ui/src/translations/fr.ts [new file with mode: 0644]
ui/src/translations/nl.ts [new file with mode: 0644]
ui/src/translations/ru.ts [new file with mode: 0644]
ui/src/translations/sv.ts [new file with mode: 0644]
ui/src/translations/zh.ts [new file with mode: 0644]
ui/src/utils.ts
ui/src/version.ts
ui/stack.dev.yaml [deleted file]
ui/stack.prod.yaml [deleted file]
ui/translation_report.ts [new file with mode: 0644]
ui/tslint.json
ui/yarn.lock

index 73c4755425c8979b70d81c068008497645e74d07..03466f0a3f5c3274bd7f76631a6deff67609365f 100644 (file)
@@ -1,4 +1,5 @@
 ui/node_modules
 ui/dist
 server/target
+docs
 .git
diff --git a/.gitattributes b/.gitattributes
new file mode 100644 (file)
index 0000000..fa728e4
--- /dev/null
@@ -0,0 +1,2 @@
+* linguist-vendored
+*.rs linguist-vendored=false
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644 (file)
index 0000000..e2aa054
--- /dev/null
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+patreon: dessalines
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..2feec03
--- /dev/null
@@ -0,0 +1,2 @@
+ansible/inventory
+ansible/passwords/
index 836a003b4e84f099d5d9c55b9377fe569371bbcb..4d2d9c90ba56e7f99808219dc39f2cc00b665d00 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,16 +1,17 @@
-<h1><img src="https://image.flaticon.com/icons/svg/194/194242.svg" width="50px" height="50px" /> Lemmy</h1>
+<h1><img src="ui/assets/favicon.svg" width="50px" height="50px" style="vertical-align:bottom" /><span>Lemmy</span></h1>
 
+[![Github](https://img.shields.io/badge/-Github-blue)](https://github.com/dessalines/lemmy)
+[![Gitlab](https://img.shields.io/badge/-Gitlab-yellowgreen)](https://gitlab.com/dessalines/lemmy)
 ![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
 [![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/lemmy)
-![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/dessalines/lemmy.svg)
 [![star this repo](http://githubbadges.com/star.svg?user=dessalines&repo=lemmy&style=flat)](https://github.com/dessalines/lemmy)
 [![fork this repo](http://githubbadges.com/fork.svg?user=dessalines&repo=lemmy&style=flat)](https://github.com/dessalines/lemmy/fork)
-![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)
+[![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/)
 [![GitHub issues](https://img.shields.io/github/issues-raw/dessalines/lemmy.svg)](https://github.com/dessalines/lemmy/issues)
 ![GitHub repo size](https://img.shields.io/github/repo-size/dessalines/lemmy.svg)
 ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/dessalines/lemmy.svg)
 [![License](https://img.shields.io/github/license/dessalines/lemmy.svg)](LICENSE)
-[![Mastodon](https://img.shields.io/badge/Mastodon-follow-lightgrey.svg)](https://mastodon.social/@LemmyDev)
+[![Mastodon](https://img.shields.io/badge/Mastodon-@LemmyDev-lightgrey.svg)](https://mastodon.social/@LemmyDev)
 [![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)
 [![Patreon](https://img.shields.io/badge/-Support%20on%20Patreon-blueviolet.svg)](https://www.patreon.com/dessalines)
 
@@ -24,9 +25,10 @@ Front Page|Post
 ---|---
 ![main screen](https://i.imgur.com/y64BtXC.png)|![chat screen](https://i.imgur.com/vsOr87q.png)
 ## Features
+
 - Open source, [AGPL License](/LICENSE).
 - Self hostable, easy to deploy.
-  - Comes with [Docker](#docker), [Kubernetes](#kubernetes).
+  - Comes with [Docker](#docker), [Ansible](#ansible).
 - Live-updating Comment threads.
 - Full vote scores `(+/-)` like old reddit.
 - Moderation abilities.
@@ -35,10 +37,16 @@ Front Page|Post
   - Can lock, remove, and restore posts and comments.
   - Can ban and unban users from communities and the site.
 - Clean, mobile-friendly interface.
+- i18n / internationalization support.
+- NSFW post / community support.
+- Cross-posting support.
+- Can transfer site and communities to others.
 - High performance.
   - Server is written in rust.
   - Front end is `~80kB` gzipped.
+
 ## About
+
 [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).
 
 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.
@@ -48,70 +56,85 @@ The overall goal is to create an easily self-hostable, decentralized alternative
 Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
 
 ## Why's it called Lemmy?
+
 - Lead singer from [motorhead](https://invidio.us/watch?v=pWB5JZRGl0U).
 - The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
 - The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
 - The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
 
 Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
+
 ## Install
+
 ### Docker
-Make sure you have both docker and docker-compose installed.
 
-```
-git clone https://github.com/dessalines/lemmy
-cd lemmy/docker
+Make sure you have both docker and docker-compose(>=`1.24.0`) installed.
+
+```bash
+mkdir lemmy/
+cd lemmy/
+wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
+wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/.env
+# Edit the .env if you want custom passwords
 docker-compose up -d
 ```
 
 and goto http://localhost:8536
-## Develop
-### Docker Development
 
-```
-git clone https://github.com/dessalines/lemmy
-cd lemmy
-./docker_update.sh # This pulls the newest version, builds and runs it
-```
+[A sample nginx config](/docker/prod/nginx.conf), could be setup with:
 
-and goto http://localhost:8536
-### Kubernetes
-#### Requirements
-- Local or remote Kubernetes cluster, i.e. [`minikube`](https://kubernetes.io/docs/tasks/tools/install-minikube/)
-- [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
-- [`skaffold`](https://skaffold.dev/)
-#### Production
 ```bash
-# Deploy the Traefik Ingress
-kubectl apply -f https://raw.githubusercontent.com/containous/traefik/v1.7/examples/k8s/traefik-rbac.yaml
-kubectl apply -f https://raw.githubusercontent.com/containous/traefik/v1.7/examples/k8s/traefik-ds.yaml
-# Replace ${IP} with your Ingress' IP
-echo "${IP} dev.lemmy.local" >> /etc/hosts
+wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/nginx.conf
+# Replace the {{ vars }}
+sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
 ```
 
+### Ansible
+
+First, you need to [install Ansible on your local computer](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html),
+eg using `sudo apt install ansible`, or the equivalent for you platform.
+
+Then run the following commands on your local computer:
+
 ```bash
-skaffold run -p lemmy--prod
+git clone https://github.com/dessalines/lemmy.git
+cd lemmy/ansible/
+cp inventory.example inventory
+nano inventory # enter your server, domain, contact email
+ansible-playbook lemmy.yml
 ```
 
-Now go to http://dev.lemmy.local.
-#### Development
+## Develop
+
+### Docker Development
+
 ```bash
-skaffold dev -p lemmy--dev
+git clone https://github.com/dessalines/lemmy
+cd lemmy/docker/dev
+./docker_update.sh # This builds and runs it, updating for your changes
 ```
 
-Now go to http://localhost:4444. It automatically proxies to localhost, both if the cluster is local or remote; it also hot-reloads the UI and automatically recompiles and restarts the server.
+and goto http://localhost:8536
+
 ### Local Development
+
 #### Requirements
+
 - [Rust](https://www.rust-lang.org/)
 - [Yarn](https://yarnpkg.com/en/)
-- [Postgres](https://www.sqlite.org/index.html)
+- [Postgres](https://www.postgresql.org/)
+
 #### Set up Postgres DB
+
+```bash
+ psql -c "create user lemmy with password 'password' superuser;" -U postgres
+ psql -c 'create database lemmy with owner lemmy;' -U postgres
+ export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
 ```
- psql -c "create user rrr with password 'rrr' superuser;" -U postgres
- psql -c 'create database rrr with owner rrr;' -U postgres
-```
+
 #### Running
-```
+
+```bash
 git clone https://github.com/dessalines/lemmy
 cd lemmy
 ./install.sh
@@ -120,17 +143,41 @@ cd lemmy
 # cd server && cargo watch -x run
 ```
 
-and goto http://localhost:8536
 ## Documentation
+
 - [Websocket API for App developers](docs/api.md)
 - [ActivityPub API.md](docs/apub_api_outline.md)
 - [Goals](docs/goals.md)
 - [Ranking Algorithm](docs/ranking.md)
+
 ## Support
+
 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 Patreon](https://www.patreon.com/dessalines).
-- [Sponsor List](https://dev.lemmy.ml/#/sponsors).
+- [Sponsor List](https://dev.lemmy.ml/sponsors).
 - bitcoin: `1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK`
 - ethereum: `0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01`
+- monero: `41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV`
+
+## Translations 
+
+If you'd like to add translations, take a look a look at the [english translation file](ui/src/translations/en.ts).
+
+- Languages supported: English (`en`), Chinese (`zh`), Dutch (`nl`), Esperanto (`eo`), French (`fr`), Spanish (`es`), Swedish (`sv`), German (`de`), Russian (`ru`).
+
+### Report
+
+lang | done | missing
+--- | --- | ---
+de | 88% | cross_posts,cross_post,users,number_of_communities,settings,subscribed,expires,recent_comments,nsfw,show_nsfw,crypto,monero,joined,by,to,transfer_community,transfer_site,are_you_sure,yes,no 
+eo | 98% | number_of_communities,are_you_sure,yes,no 
+es | 98% | number_of_communities,are_you_sure,yes,no 
+fr | 91% | cross_posts,cross_post,users,number_of_communities,settings,recent_comments,nsfw,show_nsfw,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no 
+nl | 100% |  
+ru | 93% | cross_posts,cross_post,number_of_communities,recent_comments,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no 
+sv | 91% | cross_posts,cross_post,number_of_communities,settings,recent_comments,nsfw,show_nsfw,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no 
+zh | 91% | cross_posts,cross_post,users,number_of_communities,settings,recent_comments,nsfw,show_nsfw,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no 
+
 ## Credits
-Icons made by [Freepik](https://www.freepik.com/) licensed by [CC 3.0](http://creativecommons.org/licenses/by/3.0/)
+
+Logo made by Andy Cuccaro (@andycuccaro) under the CC-BY-SA 4.0 license
diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg
new file mode 100644 (file)
index 0000000..960a7c4
--- /dev/null
@@ -0,0 +1,5 @@
+[defaults]
+inventory=inventory
+
+[ssh_connection]
+pipelining = True
diff --git a/ansible/inventory.example b/ansible/inventory.example
new file mode 100644 (file)
index 0000000..52b45d3
--- /dev/null
@@ -0,0 +1,6 @@
+[lemmy]
+# define the username and hostname that you use for ssh connection, and specify the domain
+myuser@example.com  domain=example.com  letsencrypt_contact_email=your@email.com
+
+[all:vars]
+ansible_connection=ssh
diff --git a/ansible/lemmy.yml b/ansible/lemmy.yml
new file mode 100644 (file)
index 0000000..4ba80e9
--- /dev/null
@@ -0,0 +1,70 @@
+---
+- hosts: all
+
+  # Install python if required
+  # https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/
+  gather_facts: False
+  pre_tasks:
+    - name: install python for Ansible
+      raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools)
+      args:
+        executable: /bin/bash
+      register: output
+      changed_when: output.stdout != ""
+    - setup: # gather facts
+
+  tasks:
+  - name: install dependencies
+    apt:
+      pkg: ['nginx', 'docker-compose', 'docker.io', 'certbot', 'python-certbot-nginx']
+
+  - name: request initial letsencrypt certificate
+    command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}'
+    args:
+      creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
+
+  - name: create lemmy folder
+    file: path={{item.path}} state=directory
+    with_items:
+      - { path: '/lemmy/' }
+      - { path: '/lemmy/volumes/' }
+
+  - name:  add all template files
+    template: src={{item.src}} dest={{item.dest}}
+    with_items:
+      - { src: 'templates/env', dest: '/lemmy/.env' }
+      - { src: '../docker/prod/docker-compose.yml', dest: '/lemmy/docker-compose.yml' }
+      - { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf' }
+    vars:
+      postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
+      jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
+
+  - name: set env file permissions
+    file:
+      path: "/lemmy/.env"
+      state: touch
+      mode: 0600
+      access_time: preserve
+      modification_time: preserve
+
+  - name: enable and start docker service
+    systemd:
+      name: docker
+      enabled: yes
+      state: started
+
+  - name: start docker-compose
+    docker_compose:
+      project_src: /lemmy/
+      state: present
+      pull: yes
+
+  - name: reload nginx with new config
+    shell: nginx -s reload
+
+  - name: certbot renewal cronjob
+    cron:
+      special_time=daily
+      name=certbot-renew-lemmy
+      user=root
+      job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'docker-compose -f /peertube/docker-compose.yml exec nginx nginx -s reload'"
diff --git a/ansible/templates/env b/ansible/templates/env
new file mode 100644 (file)
index 0000000..12ff850
--- /dev/null
@@ -0,0 +1,4 @@
+DOMAIN={{ domain }}
+DATABASE_PASSWORD={{ postgres_password }}
+DATABASE_URL=postgres://lemmy:{{ postgres_password }}@db:5432/lemmy
+JWT_SECRET={{ jwt_password }}
diff --git a/ansible/templates/nginx.conf b/ansible/templates/nginx.conf
new file mode 100644 (file)
index 0000000..74fbcda
--- /dev/null
@@ -0,0 +1,61 @@
+server {
+    listen 80;
+    server_name {{ domain }};
+    location /.well-known/acme-challenge/ {
+        root /var/www/certbot;
+    }
+    location / {
+        return 301 https://$host$request_uri;
+    }
+}
+
+server {
+    listen 443 ssl http2;
+    server_name {{ domain }};
+
+    ssl_certificate /etc/letsencrypt/live/{{domain}}/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/{{domain}}/privkey.pem;
+
+    # Various TLS hardening settings
+    # https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_prefer_server_ciphers on;
+    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
+    ssl_session_timeout  10m;
+    ssl_session_cache shared:SSL:10m;
+    ssl_session_tickets off;
+    ssl_stapling on;
+    ssl_stapling_verify on;
+
+    # Hide nginx version
+    server_tokens off;
+
+    # Enable compression for JS/CSS/HTML bundle, for improved client load times.
+    # It might be nice to compress JSON, but leaving that out to protect against potential
+    # compression+encryption information leak attacks like BREACH.
+    gzip on;
+    gzip_types text/css application/javascript;
+    gzip_vary on;
+
+    # Only connect to this site via HTTPS for the two years
+    add_header Strict-Transport-Security "max-age=63072000";
+
+    # Various content security headers
+    add_header Referrer-Policy "same-origin";
+    add_header X-Content-Type-Options "nosniff";
+    add_header X-Frame-Options "DENY";
+    add_header X-XSS-Protection "1; mode=block";
+
+    location / {
+        rewrite (\/(user|u\/|inbox|post|community|c\/|login|search|sponsors|communities|modlog|home)+) /static/index.html break;
+        proxy_pass http://0.0.0.0:8536;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header Host $host;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+        # WebSocket support
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+    }
+}
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644 (file)
index 2b9f317..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-version: '2.4'
-
-services:
-  db:
-    image: postgres
-    restart: always
-    environment:
-      POSTGRES_USER: rrr
-      POSTGRES_PASSWORD: rrr
-      POSTGRES_DB: rrr
-    healthcheck:
-      test: ["CMD-SHELL", "pg_isready -U rrr"]
-      interval: 5s
-      timeout: 5s
-      retries: 20
-  lemmy:
-    build: 
-      context: .
-    ports:
-      - "8536:8536"
-    environment:
-      LEMMY_FRONT_END_DIR: /app/dist
-      DATABASE_URL: postgres://rrr:rrr@db:5432/rrr
-      JWT_SECRET: changeme
-      HOSTNAME: rrr
-    restart: always
-    depends_on: 
-      db: 
-        condition: service_healthy 
diff --git a/docker/dev/.env b/docker/dev/.env
new file mode 100644 (file)
index 0000000..f82502d
--- /dev/null
@@ -0,0 +1,4 @@
+DOMAIN=my_domain
+DATABASE_PASSWORD=password
+DATABASE_URL=postgres://lemmy:password@lemmy_db:5432/lemmy
+JWT_SECRET=changeme
similarity index 96%
rename from Dockerfile
rename to docker/dev/Dockerfile
index 0eb5f60d142ead344acfbdf9ec770fd3870aa475..69ef3aae0618530b04ed71be52ac31aa2913f014 100644 (file)
@@ -1,4 +1,5 @@
 FROM node:10-jessie as node
+
 WORKDIR /app/ui
 
 # Cache deps
@@ -9,7 +10,7 @@ RUN yarn install --pure-lockfile
 COPY ui /app/ui
 RUN yarn build
 
-FROM rust:latest as rust
+FROM rust:1.37 as rust
 
 # Install musl
 RUN apt-get update
@@ -34,7 +35,7 @@ RUN RUSTFLAGS=-Clinker=musl-gcc cargo build --frozen --release --target=x86_64-u
 # Get diesel-cli on there just in case
 # RUN cargo install diesel_cli --no-default-features --features postgres
 
-FROM alpine:latest
+FROM alpine:3.10
 
 # Install libpq for postgres
 RUN apk add libpq
diff --git a/docker/dev/deploy.sh b/docker/dev/deploy.sh
new file mode 100755 (executable)
index 0000000..5982318
--- /dev/null
@@ -0,0 +1,27 @@
+#!/bin/sh
+git checkout master
+
+# Creating the new tag
+new_tag="$1"
+git tag $new_tag
+
+# Setting the version on the front end
+pushd ../../ui/
+node set_version.js
+git add src/version.ts
+popd
+
+# Changing the docker-compose prod
+sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../prod/docker-compose.yml
+git add ../prod/docker-compose.yml
+
+# The commit
+git commit -m"Upping version."
+
+git push origin $new_tag
+git push
+
+# Rebuilding docker
+./docker_update.sh
+docker tag dev_lemmy:latest dessalines/lemmy:$new_tag
+docker push dessalines/lemmy:$new_tag
diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml
new file mode 100644 (file)
index 0000000..b7ab8c2
--- /dev/null
@@ -0,0 +1,26 @@
+version: '3.3'
+
+services:
+  lemmy_db:
+    image: postgres:12-alpine
+    environment:
+      - POSTGRES_USER=lemmy
+      - POSTGRES_PASSWORD=${DATABASE_PASSWORD}
+      - POSTGRES_DB=lemmy
+    volumes:
+      - lemmy_db:/var/lib/postgresql/data
+  lemmy:
+    build: 
+      context: ../../
+      dockerfile: docker/dev/Dockerfile
+    ports:
+      - "8536:8536"
+    environment:
+      - LEMMY_FRONT_END_DIR=/app/dist
+      - DATABASE_URL=${DATABASE_URL}
+      - JWT_SECRET=${JWT_SECRET}
+      - HOSTNAME=${DOMAIN}
+    depends_on: 
+      - lemmy_db
+volumes:
+  lemmy_db:
similarity index 74%
rename from docker_update.sh
rename to docker/dev/docker_update.sh
index 4a52b5220a8ea1a2bb8480cf0a5fa3be8534f49f..9d0f45429952c5b44ba073fc3c725f5e6d262862 100755 (executable)
@@ -1,5 +1,2 @@
 #!/bin/sh
-set -e
-
-git pull
 docker-compose up -d --no-deps --build
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
deleted file mode 100644 (file)
index 4f5d51a..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-version: '2.4'
-
-services:
-  db:
-    image: postgres
-    restart: always
-    environment:
-      POSTGRES_USER: rrr
-      POSTGRES_PASSWORD: rrr
-      POSTGRES_DB: rrr
-    healthcheck:
-      test: ["CMD-SHELL", "pg_isready -U rrr"]
-      interval: 5s
-      timeout: 5s
-      retries: 20
-  lemmy:
-    image: dessalines/lemmy:latest
-    ports:
-      - "8536:8536"
-    environment:
-      LEMMY_FRONT_END_DIR: /app/dist
-      DATABASE_URL: postgres://rrr:rrr@db:5432/rrr
-      JWT_SECRET: changeme
-      HOSTNAME: rrr
-    restart: always
-    depends_on: 
-      db: 
-        condition: service_healthy 
diff --git a/docker/docker_db_backup.sh b/docker/docker_db_backup.sh
new file mode 100755 (executable)
index 0000000..d42826e
--- /dev/null
@@ -0,0 +1 @@
+docker exec -it dev_lemmy_db_1 pg_dumpall -c -U rrr > dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql
diff --git a/docker/prod/.env b/docker/prod/.env
new file mode 100644 (file)
index 0000000..f82502d
--- /dev/null
@@ -0,0 +1,4 @@
+DOMAIN=my_domain
+DATABASE_PASSWORD=password
+DATABASE_URL=postgres://lemmy:password@lemmy_db:5432/lemmy
+JWT_SECRET=changeme
diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml
new file mode 100644 (file)
index 0000000..9e438cc
--- /dev/null
@@ -0,0 +1,24 @@
+version: '3.3'
+
+services:
+  lemmy_db:
+    image: postgres:12-alpine
+    environment:
+      - POSTGRES_USER=lemmy
+      - POSTGRES_PASSWORD=${DATABASE_PASSWORD}
+      - POSTGRES_DB=lemmy
+    volumes:
+      - lemmy_db:/var/lib/postgresql/data
+  lemmy:
+    image: dessalines/lemmy:v0.0.8.7
+    ports:
+      - "8536:8536"
+    environment:
+      - LEMMY_FRONT_END_DIR=/app/dist
+      - DATABASE_URL=${DATABASE_URL}
+      - JWT_SECRET=${JWT_SECRET}
+      - HOSTNAME=${DOMAIN}
+    depends_on:
+      - lemmy_db
+volumes:
+    lemmy_db:
diff --git a/docker/prod/nginx.conf b/docker/prod/nginx.conf
new file mode 100644 (file)
index 0000000..918851a
--- /dev/null
@@ -0,0 +1,61 @@
+server {
+    listen 80;
+    server_name {{ your domain }};
+    location /.well-known/acme-challenge/ {
+        root /var/www/certbot;
+    }
+    location / {
+        return 301 https://$host$request_uri;
+    }
+}
+
+server {
+    listen 443 ssl http2;
+    server_name {{ your domain }};
+
+    ssl_certificate /etc/letsencrypt/live/{{ your domain }}/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/{{ your domain }}/privkey.pem;
+
+    # Various TLS hardening settings
+    # https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_prefer_server_ciphers on;
+    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
+    ssl_session_timeout  10m;
+    ssl_session_cache shared:SSL:10m;
+    ssl_session_tickets off;
+    ssl_stapling on;
+    ssl_stapling_verify on;
+
+    # Hide nginx version
+    server_tokens off;
+
+    # Enable compression for JS/CSS/HTML bundle, for improved client load times.
+    # It might be nice to compress JSON, but leaving that out to protect against potential
+    # compression+encryption information leak attacks like BREACH.
+    gzip on;
+    gzip_types text/css application/javascript;
+    gzip_vary on;
+
+    # Only connect to this site via HTTPS for the two years
+    add_header Strict-Transport-Security "max-age=63072000";
+
+    # Various content security headers
+    add_header Referrer-Policy "same-origin";
+    add_header X-Content-Type-Options "nosniff";
+    add_header X-Frame-Options "DENY";
+    add_header X-XSS-Protection "1; mode=block";
+
+    location / {
+        rewrite (\/(user|u|inbox|post|community|c|login|search|sponsors|communities|modlog|home)+) /static/index.html break;
+        proxy_pass http://0.0.0.0:8536;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header Host $host;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+        # WebSocket support
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+    }
+}
diff --git a/docker_db_backup.sh b/docker_db_backup.sh
deleted file mode 100755 (executable)
index 5b87b81..0000000
+++ /dev/null
@@ -1 +0,0 @@
-docker exec -it lemmy_db_1 pg_dumpall -c -U rrr > dump_`date +%d-%m-%Y"_"%H_%M_%S`.sql
index 744de5f3b9a49ca3766341ad71ed47a2425e63ad..dd5f6d60de3c54c5c416a0aa60e456bf2c68d26a 100644 (file)
@@ -28,7 +28,8 @@ A simple test command:
 
 ## API
 ### List
-`Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead`
+`Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead, SaveUserSettings, TransferCommunity,
+TransferSite`
 
 ### Sort Types
 These go wherever there is a `sort` field.
@@ -109,7 +110,21 @@ Only the first user will be able to be the admin.
   posts: Vec<PostView>,
 }
 ```
-
+#### Save User Settings
+##### Request
+```rust
+{
+  show_nsfw: bool,
+  auth: String,
+}
+```
+##### Response
+```rust
+{
+  op: String,
+  jwt: String
+}
+```
 #### Get Replies / Inbox
 ##### Request
 ```rust
@@ -320,6 +335,27 @@ Search types are `Both, Comments, Posts`.
 }
 ```
 
+#### Transfer Site
+##### Request
+```rust
+{
+  op: "TransferSite",
+  data: {
+    user_id: i32,
+    auth: String
+  }
+}
+```
+##### Response
+```rust
+{
+  op: String,
+  site: Option<SiteView>,
+  admins: Vec<UserView>,
+  banned: Vec<UserView>,
+}
+```
+
 ### Community
 #### Get Community
 ##### Request
@@ -498,6 +534,28 @@ Mods and admins can remove and lock a community, creators can delete it.
 }
 ```
 
+#### Transfer Community
+##### Request
+```rust
+{
+  op: "TransferCommunity",
+  data: {
+    community_id: i32,
+    user_id: i32,
+    auth: String
+  }
+}
+```
+##### Response
+```rust
+{
+  op: String,
+  community: CommunityView,
+  moderators: Vec<CommunityModeratorView>,
+  admins: Vec<UserView>,
+}
+```
+
 ### Post
 #### Create Post
 ##### Request
index 0d92ab657643e2d43dfb88fdb8d353cddce317da..7ec3c4905f7047c58971f3471bf3664ae10c990c 100644 (file)
@@ -23,7 +23,7 @@
 - [Activitypub main](https://www.w3.org/TR/activitypub/)
 - [Diesel to Postgres data types](https://kotiri.com/2018/01/31/postgresql-diesel-rust-types.html)
 - [helpful diesel examples](http://siciarz.net/24-days-rust-diesel/)
-- [Mastodan public key server example](https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/)
+- [Mastodon public key server example](https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/)
 - [Recursive query for adjacency list for nested comments](https://stackoverflow.com/questions/192220/what-is-the-most-efficient-elegant-way-to-parse-a-flat-table-into-a-tree/192462#192462)
 - https://github.com/sparksuite/simplemde-markdown-editor
 - [Markdown-it](https://github.com/markdown-it/markdown-it)
 - [RES expando - Possibly make this into a switching react component.](https://github.com/honestbleeps/Reddit-Enhancement-Suite/tree/d21f55c21e734f47d8ed03fe0ebce5b16653b0bd/lib/modules/hosts)
 - [Temp Icon](https://www.flaticon.com/free-icon/mouse_194242)
 - [Rust docker build](https://shaneutt.com/blog/rust-fast-small-docker-image-builds/)
+- [Zurb mentions](https://github.com/zurb/tribute)
 - Activitypub guides
   - https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/
   - https://raw.githubusercontent.com/w3c/activitypub/gh-pages/activitypub-tutorial.txt
   - https://github.com/tOkeshu/activitypub-example
+  - https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/
 
index 608b548482b23e35080a566816a16d73d55078a5..361dc24d8fc69e06088d7561c717ac4ef2a32ba9 100644 (file)
@@ -5,7 +5,7 @@
 - Use a log scale, since votes tend to snowball, and so the first 10 votes are just as important as the next hundred.
 
 ## Reddit Sorting
-[Reddit's comment sorting algorithm](https://medium.com/hacking-and-gonzo/how-reddit-ranking-algorithms-work-ef111e33d0d9), the wilson confidence sort, is inadequate, because it completely ignores time. What ends up happening, especially in smaller subreddits, is that the early comments end up getting upvoted, and newer comments stay at the bottom, never to be seen.
+[Reddit's comment sorting algorithm](https://medium.com/hacking-and-gonzo/how-reddit-ranking-algorithms-work-ef111e33d0d9), the wilson confidence sort, is inadequate, because it completely ignores time. What ends up happening, especially in smaller subreddits, is that the early comments end up getting upvoted, and newer comments stay at the bottom, never to be seen. Research showed that nearly all top comments are just the [first ones posted.](https://minimaxir.com/2016/11/first-comment/)
 
 ## Hacker News Sorting
 The [Hacker New's ranking algorithm](https://medium.com/hacking-and-gonzo/how-hacker-news-ranking-algorithm-works-1d9b0cf2c08d) is great, but it doesn't use a log scale for the scores.
index 659bd235c87936df926550b704745bec52c248ab..72398c2ac83fe5d16ea4f1ec133dda4c99d9b0a3 100644 (file)
@@ -2,20 +2,20 @@
 # It is not intended for manual editing.
 [[package]]
 name = "activitypub"
-version = "0.1.4"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "activitystreams-derive 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "activitystreams-derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "activitystreams-traits 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "activitystreams-types 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
+ "activitystreams-types 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "activitystreams-derive"
-version = "0.1.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -29,209 +29,379 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "activitystreams-types"
-version = "0.2.2"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "activitystreams-derive 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "activitystreams-derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "activitystreams-traits 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "mime 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "actix"
-version = "0.7.9"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "actix_derive 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-http 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-rt 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix_derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
  "crossbeam-channel 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
+ "derive_more 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hashbrown 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-signal 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "trust-dns-proto 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "trust-dns-resolver 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "uuid 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "trust-dns-resolver 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "actix-net"
-version = "0.2.6"
+name = "actix-codec"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "actix 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
- "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-current-thread 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "actix-connect"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "actix-codec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-service 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-utils 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-current-thread 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "tower-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "trust-dns-resolver 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "trust-dns-resolver 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "actix-web"
-version = "0.7.18"
+name = "actix-files"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "actix 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-net 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-http 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-service 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-web 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mime 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mime_guess 2.0.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "v_htmlescape 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "actix-http"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "actix-codec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-connect 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-server-config 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-service 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-threadpool 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-utils 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "brotli2 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "cookie 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
+ "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "copyless 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "encoding_rs 0.8.17 (registry+https://github.com/rust-lang/crates.io-index)",
  "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "flate2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "h2 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "http 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "httparse 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "flate2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "h2 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "mime 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)",
- "mime_guess 2.0.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
- "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_urlencoded 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_urlencoded 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-current-thread 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-current-thread 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "trust-dns-resolver 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "actix-router"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
+ "string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "actix-rt"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "actix-threadpool 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "copyless 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-current-thread 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "actix-server"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "actix-rt 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-server-config 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-service 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-signal 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "actix-server-config"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "actix-service"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "actix-threadpool"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "derive_more 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "actix-utils"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "actix-codec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-service 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-current-thread 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "actix-web"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "actix-codec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-http 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-router 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-rt 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-server 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-server-config 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-service 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-threadpool 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-utils 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-web-codegen 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "awc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "encoding_rs 0.8.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mime 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
+ "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_urlencoded 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "v_htmlescape 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "actix_derive"
-version = "0.3.2"
+name = "actix-web-actors"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-codec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-http 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-web 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "adler32"
-version = "1.0.3"
+name = "actix-web-codegen"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)",
+]
 
 [[package]]
-name = "aho-corasick"
-version = "0.6.10"
+name = "actix_derive"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "arc-swap"
-version = "0.3.7"
+name = "adler32"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
-name = "arrayvec"
-version = "0.4.10"
+name = "aho-corasick"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "arc-swap"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "atty"
-version = "0.2.11"
+version = "0.2.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "autocfg"
-version = "0.1.2"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
-name = "backtrace"
-version = "0.3.14"
+name = "awc"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
- "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-codec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-http 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-service 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mime 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_urlencoded 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "backtrace-sys"
-version = "0.1.28"
+name = "backtrace"
+version = "0.3.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "base64"
-version = "0.9.3"
+name = "backtrace-sys"
+version = "0.1.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -239,24 +409,24 @@ name = "base64"
 version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "bcrypt"
-version = "0.3.0"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "blowfish 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "bitflags"
-version = "1.0.4"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -264,7 +434,7 @@ name = "block-cipher-trait"
 version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -273,7 +443,7 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -282,8 +452,8 @@ name = "brotli-sys"
 version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -292,17 +462,12 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "brotli-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
-[[package]]
-name = "build_const"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
 [[package]]
 name = "byteorder"
-version = "1.3.1"
+version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -310,28 +475,38 @@ name = "bytes"
 version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "c2-chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "cc"
-version = "1.0.29"
+version = "1.0.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "cfg-if"
-version = "0.1.6"
+version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "chrono"
-version = "0.4.6"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
- "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
  "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -340,34 +515,20 @@ name = "cloudabi"
 version = "0.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "cookie"
-version = "0.11.0"
+name = "copyless"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
- "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "crc"
-version = "1.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
-]
 
 [[package]]
 name = "crc32fast"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -376,46 +537,40 @@ version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "crossbeam-deque"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "crossbeam-epoch 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "crossbeam-epoch"
-version = "0.7.1"
+name = "crossbeam-utils"
+version = "0.6.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "crossbeam-queue"
-version = "0.1.2"
+name = "derive_more"
+version = "0.14.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "crossbeam-utils"
-version = "0.6.5"
+name = "derive_more"
+version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -423,9 +578,9 @@ name = "diesel"
 version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "diesel_derives 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "pq-sys 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -435,9 +590,9 @@ name = "diesel_derives"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -451,15 +606,22 @@ dependencies = [
 
 [[package]]
 name = "dotenv"
-version = "0.9.0"
+version = "0.14.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "dtoa"
-version = "0.4.3"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "either"
+version = "1.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -520,23 +682,33 @@ version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
-name = "env_logger"
-version = "0.6.0"
+name = "encoding_rs"
+version = "0.8.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "error-chain"
-version = "0.8.1"
+name = "enum-as-inner"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "backtrace 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -544,7 +716,7 @@ name = "failure"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "backtrace 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "backtrace 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)",
  "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -553,21 +725,21 @@ name = "failure_derive"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
- "synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)",
+ "synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "flate2"
-version = "1.0.6"
+version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "miniz-sys 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "miniz_oxide_c_api 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "miniz-sys 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "miniz_oxide_c_api 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -585,7 +757,7 @@ name = "fuchsia-zircon"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -596,49 +768,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "futures"
-version = "0.1.25"
+version = "0.1.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
-name = "futures-cpupool"
-version = "0.1.8"
+name = "generic-array"
+version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "generic-array"
-version = "0.12.0"
+name = "getrandom"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "h2"
-version = "0.1.16"
+version = "0.1.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
  "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "http 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
  "indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "hashbrown"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "hashbrown"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "heck"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -646,23 +828,23 @@ name = "hostname"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
  "winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "http"
-version = "0.1.16"
+version = "0.1.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
  "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "httparse"
-version = "1.3.3"
+version = "1.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -693,38 +875,37 @@ name = "iovec"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
  "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "ipconfig"
-version = "0.1.9"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "error-chain 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "socket2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "widestring 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "winreg 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "socket2 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winreg 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "itoa"
-version = "0.4.3"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "jsonwebtoken"
-version = "5.0.1"
+version = "6.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
+ "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
  "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -746,44 +927,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 name = "lazy_static"
 version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "lazycell"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "spin 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
 
 [[package]]
 name = "lemmy_server"
 version = "0.0.1"
 dependencies = [
- "activitypub 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-web 0.7.18 (registry+https://github.com/rust-lang/crates.io-index)",
- "bcrypt 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "activitypub 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-files 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-web 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-web-actors 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bcrypt 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "diesel 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "diesel_migrations 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "dotenv 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "dotenv 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "jsonwebtoken 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "jsonwebtoken 6.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
- "strum 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "strum_macros 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
+ "strum 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "strum_macros 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "libc"
-version = "0.2.49"
+version = "0.2.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "linked-hash-map"
-version = "0.4.2"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -795,20 +976,36 @@ dependencies = [
  "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "lock_api"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "log"
-version = "0.4.6"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "lru-cache"
-version = "0.1.1"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "linked-hash-map 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -818,12 +1015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "memchr"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "memoffset"
-version = "0.2.1"
+version = "2.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -849,7 +1041,7 @@ name = "mime"
 version = "0.3.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "unicase 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicase 2.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -865,16 +1057,16 @@ dependencies = [
 
 [[package]]
 name = "miniz-sys"
-version = "0.1.11"
+version = "0.1.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "miniz_oxide"
-version = "0.2.1"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -882,27 +1074,26 @@ dependencies = [
 
 [[package]]
 name = "miniz_oxide_c_api"
-version = "0.2.1"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)",
- "crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "miniz_oxide 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "miniz_oxide 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "mio"
-version = "0.6.16"
+version = "0.6.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
  "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -915,8 +1106,8 @@ version = "0.6.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -935,44 +1126,43 @@ name = "net2"
 version = "0.2.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
-[[package]]
-name = "nodrop"
-version = "0.1.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
 [[package]]
 name = "nom"
-version = "4.2.2"
+version = "4.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "num-integer"
-version = "0.1.39"
+version = "0.1.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "num-traits"
-version = "0.2.6"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
 
 [[package]]
 name = "num_cpus"
-version = "1.10.0"
+version = "1.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -997,16 +1187,65 @@ dependencies = [
  "parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "parking_lot"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "lock_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "parking_lot_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "lock_api 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "parking_lot_core"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
  "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1049,12 +1288,17 @@ dependencies = [
  "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "ppv-lite86"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "pq-sys"
 version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "vcpkg 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1067,7 +1311,7 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "0.4.27"
+version = "0.4.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1093,22 +1337,10 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "0.6.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "rand"
-version = "0.5.6"
+version = "0.6.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1116,17 +1348,29 @@ name = "rand"
 version = "0.6.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_jitter 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_os 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_chacha 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1134,10 +1378,20 @@ name = "rand_chacha"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "rand_chacha"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "rand_core"
 version = "0.3.1"
@@ -1151,6 +1405,14 @@ name = "rand_core"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "rand_core"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "rand_hc"
 version = "0.1.0"
@@ -1159,6 +1421,14 @@ dependencies = [
  "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "rand_isaac"
 version = "0.1.1"
@@ -1169,25 +1439,25 @@ dependencies = [
 
 [[package]]
 name = "rand_jitter"
-version = "0.1.3"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "rand_os"
-version = "0.1.2"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1195,7 +1465,7 @@ name = "rand_pcg"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -1217,55 +1487,27 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.1.51"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "redox_termios"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "regex"
-version = "0.2.11"
+version = "0.1.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
-]
 
 [[package]]
 name = "regex"
-version = "1.1.2"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "aho-corasick 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex-syntax 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "utf8-ranges 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "regex-syntax"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "regex-syntax"
-version = "0.6.5"
+version = "0.6.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ucd-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1279,18 +1521,20 @@ dependencies = [
 
 [[package]]
 name = "ring"
-version = "0.13.5"
+version = "0.14.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "spin 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "rustc-demangle"
-version = "0.1.13"
+version = "0.1.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -1303,17 +1547,17 @@ dependencies = [
 
 [[package]]
 name = "ryu"
-version = "0.2.7"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
-name = "safemem"
-version = "0.3.0"
+name = "scopeguard"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "scopeguard"
-version = "0.3.3"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -1331,40 +1575,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "serde"
-version = "1.0.88"
+version = "1.0.97"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.88"
+version = "1.0.97"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.38"
+version = "1.0.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
+ "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "serde_urlencoded"
-version = "0.5.4"
+version = "0.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
+ "dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -1375,11 +1619,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "signal-hook"
-version = "0.1.8"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "arc-swap 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "signal-hook-registry 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "arc-swap 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1394,20 +1647,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "smallvec"
-version = "0.6.9"
+version = "0.6.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "socket2"
-version = "0.3.8"
+version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "spin"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "stable_deref_trait"
 version = "1.1.1"
@@ -1415,23 +1673,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "string"
-version = "0.1.3"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
+]
 
 [[package]]
 name = "strum"
-version = "0.14.0"
+version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "strum_macros"
-version = "0.14.0"
+version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1456,11 +1717,11 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "0.15.26"
+version = "0.15.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
  "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -1474,33 +1735,23 @@ dependencies = [
 
 [[package]]
 name = "synstructure"
-version = "0.10.1"
+version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)",
  "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "termcolor"
-version = "1.0.4"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
-[[package]]
-name = "termion"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
- "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
 [[package]]
 name = "thread_local"
 version = "0.3.6"
@@ -1510,36 +1761,21 @@ dependencies = [
 ]
 
 [[package]]
-name = "time"
-version = "0.1.42"
+name = "threadpool"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
-name = "tokio"
-version = "0.1.16"
+name = "time"
+version = "0.1.42"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-current-thread 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-fs 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-sync 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-threadpool 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-udp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1548,36 +1784,26 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "tokio-current-thread"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "tokio-executor"
-version = "0.1.6"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "tokio-fs"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-threadpool 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1586,8 +1812,8 @@ version = "0.1.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1596,16 +1822,16 @@ version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-sync 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-sync 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1613,24 +1839,24 @@ name = "tokio-signal"
 version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)",
  "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "signal-hook 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "signal-hook 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "tokio-sync"
-version = "0.1.3"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1639,38 +1865,22 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
  "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
-[[package]]
-name = "tokio-threadpool"
-version = "0.1.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
 [[package]]
 name = "tokio-timer"
-version = "0.2.10"
+version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
  "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1679,101 +1889,54 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "tokio-uds"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
-[[package]]
-name = "tower-service"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "trust-dns-proto"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "socket2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-udp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
 [[package]]
 name = "trust-dns-proto"
-version = "0.6.3"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "enum-as-inner 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
  "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "socket2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "socket2 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-udp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "trust-dns-resolver"
-version = "0.10.3"
+version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
- "ipconfig 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ipconfig 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "resolv-conf 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "trust-dns-proto 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "trust-dns-proto 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1783,7 +1946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "ucd-util"
-version = "0.1.3"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -1796,7 +1959,7 @@ dependencies = [
 
 [[package]]
 name = "unicase"
-version = "2.2.0"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1815,12 +1978,12 @@ name = "unicode-normalization"
 version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "unicode-segmentation"
-version = "1.2.1"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -1851,50 +2014,41 @@ dependencies = [
 
 [[package]]
 name = "utf8-ranges"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "uuid"
-version = "0.7.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
-]
 
 [[package]]
 name = "v_escape"
-version = "0.3.2"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "v_escape_derive 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "v_escape_derive 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "v_escape_derive"
-version = "0.2.1"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "nom 4.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
+ "nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "v_htmlescape"
-version = "0.3.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "v_escape 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "v_escape 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "vcpkg"
-version = "0.2.6"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -1904,7 +2058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "widestring"
-version = "0.2.2"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -1914,7 +2068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "winapi"
-version = "0.3.6"
+version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1936,7 +2090,7 @@ name = "winapi-util"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1949,16 +2103,16 @@ name = "wincolor"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "winreg"
-version = "0.5.1"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1966,7 +2120,7 @@ name = "winutil"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1979,50 +2133,60 @@ dependencies = [
 ]
 
 [metadata]
-"checksum activitypub 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08018b04725f5107d4a64e850f8a44a1f8a7e72abf0ca09125e3054921d26fd9"
-"checksum activitystreams-derive 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "48db826c588a009960d74530e7c215e21fae130f585362504dc6b6357e5ce86b"
+"checksum activitypub 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "dbb11d9099278667d3723c6491f25ea34dcae3eb54d73070e665d312c4455b25"
+"checksum activitystreams-derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "176bdecfca82b1980e4769e3d54b6a392284b724083e0bff68272e290f17458f"
 "checksum activitystreams-traits 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "670ef03168e704b0cae242e7a5d8b40506772b339687e01a3496fc4afe2e8542"
-"checksum activitystreams-types 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "224d1e28d043f4275139445475da8866f0a430ecfc9047c9a15fbe3c70c22b04"
-"checksum actix 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6c616db5fa4b0c40702fb75201c2af7f8aa8f3a2e2c1dda3b0655772aa949666"
-"checksum actix-net 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8bebfbe6629e0131730746718c9e032b58f02c6ce06ed7c982b9fef6c8545acd"
-"checksum actix-web 0.7.18 (registry+https://github.com/rust-lang/crates.io-index)" = "e9f33c941e5e69a58a6bfef33853228042ed3799fc4b5a4923a36a85776fb690"
-"checksum actix_derive 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4300e9431455322ae393d43a2ba1ef96b8080573c0fc23b196219efedfb6ba69"
+"checksum activitystreams-types 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ff74c5765278614a009f97b9ec12f9a7c732bbcc5e0337fcfcab619b784860ec"
+"checksum actix 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "671ce3d27313f236827a5dd153a1073ad03ef31fc77f562020263e7830cf1ef7"
+"checksum actix-codec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9f2c11af4b06dc935d8e1b1491dad56bfb32febc49096a91e773f8535c176453"
+"checksum actix-connect 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d161322a26e6b76d6598f48654afbdcfee644c900d4368e9962ec68abd0713b"
+"checksum actix-files 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f1e4640b28cd96059541c932f6f350c63c76688e43d68305cdb88e0004da12b5"
+"checksum actix-http 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "59698e11ceb42ea16a2e491bd5a9b48adc7268323a5b600522d408d09783828c"
+"checksum actix-router 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "23224bb527e204261d0291102cb9b52713084def67d94f7874923baefe04ccf7"
+"checksum actix-rt 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "18d9054b1cfacfa441846b9b99001010cb8fbb02130e6cfdb25cea36d3e98e87"
+"checksum actix-server 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fb3038e9e457e0a498ea682723e0f4e6cc2c4f362a1868d749808355275ad959"
+"checksum actix-server-config 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "483a34989c682d93142bacad6300375bb6ad8002d2e0bb249dbad86128b9ff30"
+"checksum actix-service 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aaecc01bbc595ebd7a563a7d4f8a607d0b964bb55273c6f362b0b02c26508cf2"
+"checksum actix-threadpool 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1c29f7c554d56b3841f4bb85d5b3dee01ba536e1307679f56eb54de28aaec3fb"
+"checksum actix-utils 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6ea501068a0173533704be321f149853f702d9e3c3ce9d57e7a96d94b1ab5aca"
+"checksum actix-web 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0147b2fd52ced64145c8370af627f12f098222a1c6ba67d021e21cd0d806f574"
+"checksum actix-web-actors 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "008c1b686048a16fef4ef2fc6b6e5fcf5f29829891ad87fc0848ade26b285627"
+"checksum actix-web-codegen 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe9e3cdec1e645b675f354766e0688c5705021c85ab3cf739be1c8999b91c76"
+"checksum actix_derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0bf5f6d7bf2d220ae8b4a7ae02a572bb35b7c4806b24049af905ab8110de156c"
 "checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c"
-"checksum aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5"
-"checksum arc-swap 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1025aeae2b664ca0ea726a89d574fe8f4e77dd712d443236ad1de00379450cf6"
-"checksum arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "92c7fb76bc8826a8b33b4ee5bb07a247a81e76764ab4d55e8f73e3a4d8808c71"
-"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
-"checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799"
-"checksum backtrace 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "cd5a90e2b463010cd0e0ce9a11d4a9d5d58d9f41d4a6ba3dcaf9e68b466e88b4"
-"checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6"
+"checksum aho-corasick 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "36b7aa1ccb7d7ea3f437cf025a2ab1c47cc6c1bc9fc84918ff449def12f5e282"
+"checksum arc-swap 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "bc4662175ead9cd84451d5c35070517777949a2ed84551764129cedb88384841"
+"checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90"
+"checksum autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "22130e92352b948e7e82a49cdb0aa94f2211761117f29e052dd397c1ac33542b"
+"checksum awc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4c4763e6aa29a801d761dc3464f081d439ea5249ba90c3c3bdfc8dd3f739d233"
+"checksum backtrace 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)" = "88fb679bc9af8fa639198790a77f52d345fe13656c08b43afa9424c206b731c6"
+"checksum backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "82a830b4ef2d1124a711c71d263c5abdc710ef8e907bd508c88be475cebc422b"
 "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
-"checksum base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643"
-"checksum bcrypt 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2a426ab63025c1d21e4e12a218c915fa22097b89ab7ed5765fa803101e004b27"
-"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
+"checksum bcrypt 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b4fd6a91ff640809cfab4ea74312a892238a7bbae53adbf717b71122deb0c85"
+"checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd"
 "checksum block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774"
 "checksum blowfish 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6aeb80d00f2688459b8542068abd974cfb101e7a82182414a99b5026c0d85cc3"
 "checksum brotli-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd"
 "checksum brotli2 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e"
-"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
-"checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb"
+"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
 "checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
-"checksum cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)" = "4390a3b5f4f6bce9c1d0c00128379df433e53777fdd30e92f16a529332baec4e"
-"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4"
-"checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878"
+"checksum c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7d64d04786e0f528460fc884753cf8dddcc466be308f6026f8e355c41a0e4101"
+"checksum cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "39f75544d7bbaf57560d2168f28fd649ff9c76153874db88bdbdfd839b1a7e7d"
+"checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33"
+"checksum chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "77d81f58b7301084de3b958691458a53c3f7e0b1d702f77e550b6a88e3a88abe"
 "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
-"checksum cookie 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1465f8134efa296b4c19db34d909637cb2bf0f7aaf21299e23e18fa29ac557cf"
-"checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb"
+"checksum copyless 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6ff9c56c9fb2a49c05ef0e431485a22400af20d33226dc0764d891d09e724127"
 "checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
 "checksum crossbeam-channel 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "0f0ed1a4de2235cabda8558ff5840bffb97fcb64c97827f354a451307df5f72b"
-"checksum crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b18cd2e169ad86297e6bc0ad9aa679aee9daa4f19e8163860faf7c164e4f5a71"
-"checksum crossbeam-epoch 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "04c9e3102cc2d69cd681412141b390abd55a362afc1540965dad0ad4d34280b4"
-"checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b"
 "checksum crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f8306fcef4a7b563b76b7dd949ca48f52bc1141aa067d2ea09565f3e2652aa5c"
+"checksum derive_more 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6d944ac6003ed268757ef1ee686753b57efc5fcf0ebe7b64c9fc81e7e32ff839"
+"checksum derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a141330240c921ec6d074a3e188a7c7ef95668bb95e7d44fa0e5778ec2a7afe"
 "checksum diesel 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8d24935ba50c4a8dc375a0fd1f8a2ba6bdbdc4125713126a74b965d6a01a06d7"
 "checksum diesel_derives 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "62a27666098617d52c487a41f70de23d44a1dc1f3aa5877ceba2790fb1f1cab4"
 "checksum diesel_migrations 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c"
-"checksum dotenv 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "400b347fe65ccfbd8f545c9d9a75d04b0caf23fec49aaa838a9a05398f94c019"
-"checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd"
+"checksum dotenv 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4424bad868b0ffe6ae351ee463526ba625bbca817978293bbe6bb7dc1804a175"
+"checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e"
+"checksum either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5527cfe0d098f36e3f8839852688e63c8fff1c90b2b405aef730615f9a7bcf7b"
 "checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
 "checksum encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
 "checksum encoding-index-korean 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81"
@@ -2030,168 +2194,170 @@ dependencies = [
 "checksum encoding-index-singlebyte 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a"
 "checksum encoding-index-tradchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18"
 "checksum encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
-"checksum env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "afb070faf94c85d17d50ca44f6ad076bce18ae92f0037d350947240a36e9d42e"
-"checksum error-chain 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6930e04918388a9a2e41d518c25cf679ccafe26733fb4127dbf21993f2575d46"
+"checksum encoding_rs 0.8.17 (registry+https://github.com/rust-lang/crates.io-index)" = "4155785c79f2f6701f185eb2e6b4caf0555ec03477cb4c70db67b465311620ed"
+"checksum enum-as-inner 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3d58266c97445680766be408285e798d3401c6d4c378ec5552e78737e681e37d"
+"checksum env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3"
 "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
 "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"
-"checksum flate2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2291c165c8e703ee54ef3055ad6188e3d51108e2ded18e9f2476e774fc5ad3d4"
+"checksum flate2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "550934ad4808d5d39365e5d61727309bf18b3b02c6c56b729cb92e7dd84bc3d8"
 "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
 "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
 "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
 "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
-"checksum futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "49e7653e374fe0d0c12de4250f0bdb60680b8c80eed558c5c7538eec9c89e21b"
-"checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4"
-"checksum generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3c0f28c2f5bfb5960175af447a2da7c18900693738343dc896ffbcabd9839592"
-"checksum h2 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "ddb2b25a33e231484694267af28fec74ac63b5ccf51ee2065a5e313b834d836e"
+"checksum futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "45dc39533a6cae6da2b56da48edae506bb767ec07370f86f70fc062e9d435869"
+"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
+"checksum getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e65cce4e5084b14874c4e7097f38cab54f47ee554f9194673456ea379dcc4c55"
+"checksum h2 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "a539b63339fbbb00e081e84b6e11bd1d9634a82d91da2984a18ac74a8823f392"
+"checksum hashbrown 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "29fba9abe4742d586dfd0c06ae4f7e73a1c2d86b856933509b269d82cdf06e18"
+"checksum hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e1de41fb8dba9714efd92241565cdff73f78508c95697dd56787d3cba27e2353"
 "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
 "checksum hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e"
-"checksum http 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "fe67e3678f2827030e89cc4b9e7ecd16d52f132c0b940ab5005f88e821500f6a"
-"checksum httparse 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e8734b0cfd3bc3e101ec59100e101c2eecd19282202e87808b3037b442777a83"
+"checksum http 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "eed324f0f0daf6ec10c474f150505af2c143f251722bf9dbd1261bd1f2ee2c1a"
+"checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
 "checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
 "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
 "checksum indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7e81a7c05f79578dbc15793d8b619db9ba32b4577003ef3af1a91c416798c58d"
 "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08"
-"checksum ipconfig 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "08f7eadeaf4b52700de180d147c4805f199854600b36faa963d91114827b2ffc"
-"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b"
-"checksum jsonwebtoken 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8d438ea707d465c230305963b67f8357a1d56fcfad9434797d7cb1c46c2e41df"
+"checksum ipconfig 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa79fa216fbe60834a9c0737d7fcd30425b32d1c58854663e24d4c4b328ed83f"
+"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
+"checksum jsonwebtoken 6.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a81d1812d731546d2614737bee92aa071d37e9afa1409bc374da9e5e70e70b22"
 "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
 "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
 "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
-"checksum lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f"
-"checksum libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)" = "413f3dfc802c5dc91dc570b05125b6cda9855edfaa9825c9849807876376e70e"
-"checksum linked-hash-map 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7860ec297f7008ff7a1e3382d7f7e1dcd69efc94751a2284bafc3d013c2aa939"
+"checksum libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)" = "d44e80633f007889c7eff624b709ab43c92d708caad982295768a7b13ca3b5eb"
+"checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83"
 "checksum lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c"
-"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
-"checksum lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d06ff7ff06f729ce5f4e227876cb88d10bc59cd4ae1e09fbb2bde15c850dc21"
+"checksum lock_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ed946d4529956a20f2d63ebe1b69996d5a2137c91913fe3ebbeff957f5bca7ff"
+"checksum lock_api 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f8912e782533a93a167888781b836336a6ca5da6175c05944c86cf28c31104dc"
+"checksum log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c275b6ad54070ac2d665eef9197db647b32239c9d244bfb6f041a766d00da5b3"
+"checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
 "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
-"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
-"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3"
+"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
 "checksum migrations_internals 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8089920229070f914b9ce9b07ef60e175b2b9bc2d35c3edd8bf4433604e863b9"
 "checksum migrations_macros 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1664412abf7db2b8a6d58be42a38b099780cc542b5b350383b805d88932833fe"
 "checksum mime 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)" = "3e27ca21f40a310bd06d9031785f4801710d566c184a6e15bad4f1d9b65f9425"
 "checksum mime_guess 2.0.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)" = "30de2e4613efcba1ec63d8133f344076952090c122992a903359be5a4f99c3ed"
-"checksum miniz-sys 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "0300eafb20369952951699b68243ab4334f4b10a88f411c221d444b36c40e649"
-"checksum miniz_oxide 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c468f2369f07d651a5d0bb2c9079f8488a66d5466efe42d0c5c6466edcb7f71e"
-"checksum miniz_oxide_c_api 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b7fe927a42e3807ef71defb191dc87d4e24479b221e67015fe38ae2b7b447bab"
-"checksum mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)" = "71646331f2619b1026cc302f87a2b8b648d5c6dd6937846a16cc8ce0f347f432"
+"checksum miniz-sys 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "1e9e3ae51cea1576ceba0dde3d484d30e6e5b86dee0b2d412fe3a16a15c98202"
+"checksum miniz_oxide 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c061edee74a88eb35d876ce88b94d77a0448a201de111c244b70d047f5820516"
+"checksum miniz_oxide_c_api 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6c675792957b0d19933816c4e1d56663c341dd9bfa31cb2140ff2267c1d8ecf4"
+"checksum mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)" = "83f51996a3ed004ef184e16818edc51fadffe8e7ca68be67f9dee67d84d0ff23"
 "checksum mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125"
 "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
 "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88"
-"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
-"checksum nom 4.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "22293d25d3f33a8567cc8a1dc20f40c7eeb761ce83d0fcca059858580790cac3"
-"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea"
-"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1"
-"checksum num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1a23f0ed30a54abaa0c7e83b1d2d87ada7c3c23078d1d87815af3e3b6385fbba"
+"checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
+"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09"
+"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32"
+"checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273"
 "checksum opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "93f5bb2e8e8dec81642920ccff6b61f1eb94fa3020c5a325c9851ff604152409"
 "checksum owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13"
 "checksum parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337"
+"checksum parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fa7767817701cce701d5585b9c4db3cdd02086398322c1d7e8bf5094a96a2ce7"
+"checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252"
 "checksum parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9"
+"checksum parking_lot_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cb88cb1cb3790baa6776844f968fea3be44956cf184fa1be5a03341f5491278c"
+"checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b"
 "checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
 "checksum phf 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18"
 "checksum phf_codegen 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e"
 "checksum phf_generator 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662"
 "checksum phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
+"checksum ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b"
 "checksum pq-sys 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda"
 "checksum proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1b06e2f335f48d24442b35a19df506a835fb3547bc3c06ef27340da9acf5cae7"
-"checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915"
+"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
 "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
 "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
 "checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8"
-"checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1"
-"checksum rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9"
+"checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
 "checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
+"checksum rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d47eab0e83d9693d40f825f86948aa16eff6750ead4bdffc4ab95b8b3a7f052c"
 "checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
+"checksum rand_chacha 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e193067942ef6f485a349a113329140d0ab9e2168ce92274499bb0e9a4190d9d"
 "checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
 "checksum rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0"
+"checksum rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "615e683324e75af5d43d8f7a39ffe3ee4a9dc42c5c701167a71dc59c3a493aca"
 "checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
+"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
 "checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
-"checksum rand_jitter 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b9ea758282efe12823e0d952ddb269d2e1897227e464919a554f2a03ef1b832"
-"checksum rand_os 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b7c690732391ae0abafced5015ffb53656abfaec61b342290e5eb56b286a679d"
+"checksum rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
+"checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
 "checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
 "checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
 "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
-"checksum redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)" = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85"
-"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
-"checksum regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384"
-"checksum regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53ee8cfdddb2e0291adfb9f13d31d3bbe0a03c9a402c01b1e24188d86c35b24f"
-"checksum regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7"
-"checksum regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8c2f35eedad5295fdf00a63d7d4b238135723f92b434ec06774dad15c7ab0861"
+"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
+"checksum regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6b23da8dfd98a84bd7e08700190a5d9f7d2d38abd4369dd1dae651bc40bfd2cc"
+"checksum regex-syntax 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "cd5485bf1523a9ed51c4964273f22f63f24e31632adb5dad134f488f86a3875c"
 "checksum resolv-conf 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b263b4aa1b5de9ffc0054a2386f96992058bb6870aab516f8cdeb8a667d56dcb"
-"checksum ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2c4db68a2e35f3497146b7e4563df7d4773a2433230c5e4b448328e31740458a"
-"checksum rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "adacaae16d02b6ec37fdc7acfcddf365978de76d1983d3ee22afc260e1ca9619"
+"checksum ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)" = "426bc186e3e95cac1e4a4be125a4aca7e84c2d616ffc02244eef36e2a60a093c"
+"checksum rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af"
 "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
-"checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7"
-"checksum safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dca453248a96cb0749e36ccdfe2b0b4e54a61bfef89fb97ec621eb8e0a93dd9"
+"checksum ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997"
 "checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
+"checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d"
 "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
 "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
-"checksum serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)" = "9f301d728f2b94c9a7691c90f07b0b4e8a4517181d9461be94c04bddeb4bd850"
-"checksum serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)" = "beed18e6f5175aef3ba670e57c60ef3b1b74d250d962a26604bff4c80e970dd4"
-"checksum serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "27dce848e7467aa0e2fcaf0a413641499c0b745452aaca1194d24dedde9e13c9"
-"checksum serde_urlencoded 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d48f9f99cd749a2de71d29da5f948de7f2764cc5a9d7f3c97e3514d4ee6eabf2"
+"checksum serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)" = "d46b3dfedb19360a74316866cef04687cd4d6a70df8e6a506c63512790769b72"
+"checksum serde_derive 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)" = "c22a0820adfe2f257b098714323563dd06426502abbbce4f51b72ef544c5027f"
+"checksum serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)" = "051c49229f282f7c6f3813f8286cc1e3323e8051823fce42c7ea80fe13521704"
+"checksum serde_urlencoded 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "642dd69105886af2efd227f75a520ec9b44a820d65bc133a9131f7d229fd165a"
 "checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
-"checksum signal-hook 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "97a47ae722318beceb0294e6f3d601205a1e6abaa4437d9d33e3a212233e3021"
+"checksum signal-hook 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4f61c4d59f3aaa9f61bba6450a9b80ba48362fd7d651689e7a10c453b1f6dc68"
+"checksum signal-hook-registry 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cded4ffa32146722ec54ab1f16320568465aa922aa9ab4708129599740da85d7"
 "checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
 "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
-"checksum smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c4488ae950c49d403731982257768f48fada354a5203fe81f9bb6f43ca9002be"
-"checksum socket2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "c4d11a52082057d87cb5caa31ad812f4504b97ab44732cd8359df2e9ff9f48e7"
+"checksum smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7"
+"checksum socket2 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "4e626972d3593207547f14bf5fc9efa4d0e7283deb73fef1dff313dae9ab8878"
+"checksum spin 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44363f6f51401c34e7be73db0db371c04705d35efbe9f7d6082e03a921a32c55"
 "checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
-"checksum string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b639411d0b9c738748b5397d5ceba08e648f4f1992231aa859af1a017f31f60b"
-"checksum strum 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1810e25f576e7ffce1ff5243b37066da5ded0310b3274c20baaeccb1145b2806"
-"checksum strum_macros 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "572a2f4e53dd4c3483fd79e5cc10ddd773a3acb1169bbfe8762365e107110579"
+"checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d"
+"checksum strum 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e5d1c33039533f051704951680f1adfd468fd37ac46816ded0d9ee068e60f05f"
+"checksum strum_macros 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "47cd23f5c7dee395a00fa20135e2ec0fffcdfa151c56182966d7a3261343432e"
 "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
 "checksum syn 0.13.11 (registry+https://github.com/rust-lang/crates.io-index)" = "14f9bf6292f3a61d2c716723fdb789a41bbe104168e6f496dc6497e531ea1b9b"
-"checksum syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)" = "f92e629aa1d9c827b2bb8297046c1ccffc57c99b947a680d3ccff1f136a3bee9"
+"checksum syn 0.15.40 (registry+https://github.com/rust-lang/crates.io-index)" = "bc945221ccf4a7e8c31222b9d1fc77aefdd6638eb901a6ce457a3dc29d4c31e8"
 "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
-"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015"
-"checksum termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4096add70612622289f2fdcdbd5086dc81c1e2675e6ae58d6c4f62a16c6d7f2f"
-"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
+"checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f"
+"checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e"
 "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
+"checksum threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865"
 "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
-"checksum tokio 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "fcaabb3cec70485d0df6e9454fe514393ad1c4070dee8915f11041e95630b230"
 "checksum tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5c501eceaf96f0e1793cf26beb63da3d11c738c4a943fdf3746d81d64684c39f"
-"checksum tokio-current-thread 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c756b04680eea21902a46fca4e9f410a2332c04995af590e07ff262e2193a9a3"
-"checksum tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "30c6dbf2d1ad1de300b393910e8a3aa272b724a400b6531da03eed99e329fbf0"
-"checksum tokio-fs 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe6dc22b08d6993916647d108a1a7d15b9cd29c4f4496c62b92c45b5041b7af"
+"checksum tokio-current-thread 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "d16217cad7f1b840c5a97dfb3c43b0c871fef423a6e8d2118c604e843662a443"
+"checksum tokio-executor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "0f27ee0e6db01c5f0b2973824547ce7e637b2ed79b891a9677b0de9bd532b6ac"
 "checksum tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "5090db468dad16e1a7a54c8c67280c5e4b544f3d3e018f0b913b400261f85926"
 "checksum tokio-reactor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6af16bfac7e112bea8b0442542161bfc41cbfa4466b580bdda7d18cb88b911ce"
 "checksum tokio-signal 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "dd6dc5276ea05ce379a16de90083ec80836440d5ef8a6a39545a3207373b8296"
-"checksum tokio-sync 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1bf2b9dac2a0509b5cfd1df5aa25eafacb616a42a491a13604d6bbeab4486363"
+"checksum tokio-sync 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2162248ff317e2bc713b261f242b69dbb838b85248ed20bb21df56d60ea4cae7"
 "checksum tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1d14b10654be682ac43efee27401d792507e30fd8d26389e1da3b185de2e4119"
-"checksum tokio-threadpool 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "742e511f6ce2298aeb86fc9ea0d8df81c2388c6ebae3dc8a7316e8c9df0df801"
-"checksum tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2910970404ba6fa78c5539126a9ae2045d62e3713041e447f695f41405a120c6"
+"checksum tokio-timer 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "f2106812d500ed25a4f38235b9cae8f78a09edf43203e16e59c3b769a342a60e"
 "checksum tokio-udp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "66268575b80f4a4a710ef83d087fdfeeabdce9b74c797535fbac18a2cb906e92"
-"checksum tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "037ffc3ba0e12a0ab4aca92e5234e0dedeb48fddf6ccd260f1f150a36a9f2445"
-"checksum tower-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b32f72af77f1bfe3d3d4da8516a238ebe7039b51dd8637a09841ac7f16d2c987"
-"checksum trust-dns-proto 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0838272e89f1c693b4df38dc353412e389cf548ceed6f9fd1af5a8d6e0e7cf74"
-"checksum trust-dns-proto 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "09144f0992b0870fa8d2972cc069cbf1e3c0fda64d1f3d45c4d68d0e0b52ad4e"
-"checksum trust-dns-resolver 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8a9f877f7a1ad821ab350505e1f1b146a4960402991787191d6d8cab2ce2de2c"
+"checksum trust-dns-proto 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5559ebdf6c2368ddd11e20b11d6bbaf9e46deb803acd7815e93f5a7b4a6d2901"
+"checksum trust-dns-resolver 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6c9992e58dba365798803c0b91018ff6c8d3fc77e06977c4539af2a6bfe0a039"
 "checksum typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "612d636f949607bdf9b123b4a6f6d966dedf3ff669f7f045890d3a4a73948169"
-"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86"
+"checksum ucd-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa9b3b49edd3468c0e6565d85783f51af95212b6fa3986a5500954f00b460874"
 "checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33"
-"checksum unicase 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9d3218ea14b4edcaccfa0df0a64a3792a2c32cc706f1b336e48867f9d3147f90"
+"checksum unicase 2.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a84e5511b2a947f3ae965dcb29b13b7b1691b6e7332cf5dbc1744138d5acb7f6"
 "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
 "checksum unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "141339a08b982d942be2ca06ff8b076563cbe223d1befd5450716790d44e2426"
-"checksum unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa6024fc12ddfd1c6dbc14a80fa2324d4568849869b779f6bd37e5e4c03344d1"
+"checksum unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1967f4cdfc355b37fd76d2a954fb2ed3871034eb4f26d60537d88795cfc332a9"
 "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
 "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
 "checksum untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f"
 "checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
-"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737"
-"checksum uuid 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0238db0c5b605dd1cf51de0f21766f97fba2645897024461d6a00c036819a768"
-"checksum v_escape 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c8b50688edb86f4c092a1a9fe8bda004b0faa3197100897653809e97e09a2814"
-"checksum v_escape_derive 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7cd994c63b487fef7aad31e5394ec04b9e24de7b32ea5251c9fb499cd2cbf44c"
-"checksum v_htmlescape 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "020cae817dc82693aa523f01087b291b1c7a9ac8cea5c12297963f21769fb27f"
-"checksum vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "def296d3eb3b12371b2c7d0e83bfe1403e4db2d7a0bba324a12b21c4ee13143d"
+"checksum utf8-ranges 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9d50aa7650df78abf942826607c62468ce18d9019673d4a2ebe1865dbb96ffde"
+"checksum v_escape 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8865501b78eef9193c1b45486acf18ba889e5662eba98854d6fc59d8ecf3542d"
+"checksum v_escape_derive 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "306896ff4b75998501263a1dc000456de442e21d68fe8c8bdf75c66a33a58e23"
+"checksum v_htmlescape 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7fbbe0fa88dd36f9c8cf61a218d4b953ba669de4d0785832f33cc72bd081e1be"
+"checksum vcpkg 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "33dd455d0f96e90a75803cfeb7f948768c08d70a6de9a8d2362461935698bf95"
 "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
-"checksum widestring 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7157704c2e12e3d2189c507b7482c52820a16dfa4465ba91add92f266667cadb"
+"checksum widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6"
 "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
-"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0"
+"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770"
 "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
 "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 "checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
 "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 "checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba"
-"checksum winreg 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a"
+"checksum winreg 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73f1f3c6c4d3cab118551b96c476a2caab920701e28875b64a458f2ecb96ec9d"
 "checksum winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e"
 "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
index 5ee8b8a636a7f428d9351788c871ab15ef211b63..6c50c6953e14bdf410a0fbac1ceff0acfa887944 100644 (file)
@@ -2,23 +2,27 @@
 name = "lemmy_server"
 version = "0.0.1"
 authors = ["Dessalines <happydooby@gmail.com>"]
+edition = "2018"
 
 [dependencies]
 diesel = { version = "1.4.2", features = ["postgres","chrono"] }
-diesel_migrations = "*"
-dotenv = "0.9.0"
-bcrypt = "0.3"
-activitypub = "0.1.4"
-chrono = { version = "0.4", features = ["serde"] }
+diesel_migrations = "1.4.0"
+dotenv = "0.14.1"
+bcrypt = "0.5.0"
+activitypub = "0.1.5"
+chrono = { version = "0.4.7", features = ["serde"] }
 failure = "0.1.5"
-serde_json = "*"
-serde = { version = "1.0", features = ["derive"] }
-actix = "*"
-actix-web = "*"
-env_logger = "*"
-rand = "0.6.5"
-strum = "0.14.0"
-strum_macros = "0.14.0"
-jsonwebtoken = "*"
-regex = "*"
-lazy_static = "*"
+serde_json = "1.0.40"
+serde = { version = "1.0.94", features = ["derive"] }
+actix = "0.8.3"
+actix-web = "1.0"
+actix-files = "0.1.3"
+actix-web-actors = "1.0"
+env_logger = "0.6.2"
+rand = "0.7.0"
+strum = "0.15.0"
+strum_macros = "0.15.0"
+jsonwebtoken = "6.0.1"
+regex = "1.1.9"
+lazy_static = "1.3.0"
+
diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev
deleted file mode 100644 (file)
index 203dd74..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-# Setup env
-FROM rust:1.33 AS build
-RUN USER=root cargo new --bin /opt/lemmy/server--dev
-WORKDIR /opt/lemmy/server--dev
-# Enable deps caching
-RUN mkdir -p src/bin
-RUN echo 'fn main() { println!("Dummy") }' >src/bin/main.rs
-# Install deps
-COPY Cargo.toml .
-COPY Cargo.lock .
-RUN cargo build --release
-RUN rm src/bin/main.rs
-# Add app
-COPY src/ src/
-COPY migrations/ migrations/
-RUN rm target/release/deps/lemmy*
-RUN cargo build --release
-
-# Setup env (no Alpine because Rust requires glibc)
-FROM ubuntu:18.04
-RUN apt update
-RUN apt install postgresql-client -y
-# Create empty directory where the frontend would normally be
-RUN mkdir -p /opt/lemmy/ui--dev/dist
-# Add app
-COPY --from=build /opt/lemmy/server--dev/target/release/lemmy .
-# Run app
-CMD ["./lemmy"]
diff --git a/server/Dockerfile.prod b/server/Dockerfile.prod
deleted file mode 100644 (file)
index b375e47..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-# Setup env
-FROM rust:1.33 AS build
-RUN USER=root cargo new --bin /opt/lemmy/server--prod
-WORKDIR /opt/lemmy/server--prod
-# Enable deps caching
-RUN mkdir -p src/bin
-RUN echo 'fn main() { println!("Dummy") }' >src/bin/main.rs
-# Install deps
-COPY Cargo.toml .
-COPY Cargo.lock .
-RUN cargo build --release
-RUN rm src/bin/main.rs
-# Add app
-COPY src/ src/
-COPY migrations/ migrations/
-RUN rm target/release/deps/lemmy*
-RUN cargo build --release
-
-# Setup env (no Alpine because Rust requires glibc)
-FROM ubuntu:18.04
-RUN apt update
-RUN apt install postgresql-client -y
-# Create empty directory where the frontend would normally be
-RUN mkdir -p /opt/lemmy/ui--prod/dist
-# Add app
-COPY --from=build /opt/lemmy/server--prod/target/release/lemmy .
-# Run app
-CMD ["./lemmy"]
diff --git a/server/migrations/2019-06-01-222649_remove_admin/down.sql b/server/migrations/2019-06-01-222649_remove_admin/down.sql
new file mode 100644 (file)
index 0000000..6178857
--- /dev/null
@@ -0,0 +1 @@
+insert into user_ (name, fedi_name, password_encrypted) values ('admin', 'TBD', 'TBD');
diff --git a/server/migrations/2019-06-01-222649_remove_admin/up.sql b/server/migrations/2019-06-01-222649_remove_admin/up.sql
new file mode 100644 (file)
index 0000000..7cec887
--- /dev/null
@@ -0,0 +1 @@
+delete from user_ where name like 'admin';
diff --git a/server/migrations/2019-08-11-000918_add_nsfw_columns/down.sql b/server/migrations/2019-08-11-000918_add_nsfw_columns/down.sql
new file mode 100644 (file)
index 0000000..2eefece
--- /dev/null
@@ -0,0 +1,80 @@
+drop view community_view;
+drop view post_view;
+alter table community drop column nsfw;
+alter table post drop column nsfw;
+alter table user_ drop column show_nsfw;
+
+-- the views
+create view community_view as 
+with all_community as
+(
+  select *,
+  (select name from user_ u where c.creator_id = u.id) as creator_name,
+  (select name from category ct where c.category_id = ct.id) as category_name,
+  (select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
+  (select count(*) from post p where p.community_id = c.id) as number_of_posts,
+  (select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
+  hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
+  from community c
+)
+
+select
+ac.*,
+u.id as user_id,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
+from user_ u
+cross join all_community ac
+
+union all
+
+select 
+ac.*,
+null as user_id,
+null as subscribed
+from all_community ac
+;
+
+
+-- Post view
+create view post_view as
+with all_post as
+(
+  select        
+  p.*,
+  (select name from user_ where p.creator_id = user_.id) as creator_name,
+  (select name from community where p.community_id = community.id) as community_name,
+  (select removed from community c where p.community_id = c.id) as community_removed,
+  (select deleted from community c where p.community_id = c.id) as community_deleted,
+  (select count(*) from comment where comment.post_id = p.id) as number_of_comments,
+  coalesce(sum(pl.score), 0) as score,
+  count (case when pl.score = 1 then 1 else null end) as upvotes,
+  count (case when pl.score = -1 then 1 else null end) as downvotes,
+  hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank
+  from post p
+  left join post_like pl on p.id = pl.post_id
+  group by p.id
+)
+
+select
+ap.*,
+u.id as user_id,
+coalesce(pl.score, 0) as my_vote,
+(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
+(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
+(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
+from user_ u
+cross join all_post ap
+left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
+
+union all
+
+select 
+ap.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from all_post ap
+;
+
diff --git a/server/migrations/2019-08-11-000918_add_nsfw_columns/up.sql b/server/migrations/2019-08-11-000918_add_nsfw_columns/up.sql
new file mode 100644 (file)
index 0000000..cc1e007
--- /dev/null
@@ -0,0 +1,79 @@
+alter table community add column nsfw boolean default false not null;
+alter table post add column nsfw boolean default false not null;
+alter table user_ add column show_nsfw boolean default false not null;
+
+-- The views
+drop view community_view;
+create view community_view as 
+with all_community as
+(
+  select *,
+  (select name from user_ u where c.creator_id = u.id) as creator_name,
+  (select name from category ct where c.category_id = ct.id) as category_name,
+  (select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
+  (select count(*) from post p where p.community_id = c.id) as number_of_posts,
+  (select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
+  hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
+  from community c
+)
+
+select
+ac.*,
+u.id as user_id,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
+from user_ u
+cross join all_community ac
+
+union all
+
+select 
+ac.*,
+null as user_id,
+null as subscribed
+from all_community ac
+;
+-- Post view
+drop view post_view;
+create view post_view as
+with all_post as
+(
+  select        
+  p.*,
+  (select name from user_ where p.creator_id = user_.id) as creator_name,
+  (select name from community where p.community_id = community.id) as community_name,
+  (select removed from community c where p.community_id = c.id) as community_removed,
+  (select deleted from community c where p.community_id = c.id) as community_deleted,
+  (select nsfw from community c where p.community_id = c.id) as community_nsfw,
+  (select count(*) from comment where comment.post_id = p.id) as number_of_comments,
+  coalesce(sum(pl.score), 0) as score,
+  count (case when pl.score = 1 then 1 else null end) as upvotes,
+  count (case when pl.score = -1 then 1 else null end) as downvotes,
+  hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank
+  from post p
+  left join post_like pl on p.id = pl.post_id
+  group by p.id
+)
+
+select
+ap.*,
+u.id as user_id,
+coalesce(pl.score, 0) as my_vote,
+(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
+(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
+(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
+from user_ u
+cross join all_post ap
+left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
+
+union all
+
+select 
+ap.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from all_post ap
+;
diff --git a/server/migrations/2019-08-29-040006_add_community_count/down.sql b/server/migrations/2019-08-29-040006_add_community_count/down.sql
new file mode 100644 (file)
index 0000000..6302f26
--- /dev/null
@@ -0,0 +1,9 @@
+drop view site_view;
+
+create view site_view as 
+select *,
+(select name from user_ u where s.creator_id = u.id) as creator_name,
+(select count(*) from user_) as number_of_users,
+(select count(*) from post) as number_of_posts,
+(select count(*) from comment) as number_of_comments
+from site s;
diff --git a/server/migrations/2019-08-29-040006_add_community_count/up.sql b/server/migrations/2019-08-29-040006_add_community_count/up.sql
new file mode 100644 (file)
index 0000000..0ec1c9c
--- /dev/null
@@ -0,0 +1,10 @@
+drop view site_view;
+
+create view site_view as 
+select *,
+(select name from user_ u where s.creator_id = u.id) as creator_name,
+(select count(*) from user_) as number_of_users,
+(select count(*) from post) as number_of_posts,
+(select count(*) from comment) as number_of_comments,
+(select count(*) from community) as number_of_communities
+from site s;
index ffd7da2ea4d3a14595a3eab980356855fe81e30e..3d18c72a744fac0b543e24b114cc51425c064d53 100644 (file)
@@ -53,7 +53,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -62,12 +62,12 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
     // Check for a community ban
     let post = Post::read(&conn, data.post_id)?;
     if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
-      return Err(APIError::err(&self.op, "You have been banned from this community"))?
+      return Err(APIError::err(&self.op, "community_ban"))?
     }
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "You have been banned from the site"))?
+      return Err(APIError::err(&self.op, "site_ban"))?
     }
 
     let content_slurs_removed = remove_slurs(&data.content.to_owned());
@@ -86,7 +86,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
     let inserted_comment = match Comment::create(&conn, &comment_form) {
       Ok(comment) => comment,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't create Comment"))?
+        return Err(APIError::err(&self.op, "couldnt_create_comment"))?
       }
     };
 
@@ -101,7 +101,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
     let _inserted_like = match CommentLike::like(&conn, &like_form) {
       Ok(like) => like,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't like comment."))?
+        return Err(APIError::err(&self.op, "couldnt_like_comment"))?
       }
     };
 
@@ -124,7 +124,7 @@ impl Perform<CommentResponse> for Oper<EditComment> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -153,17 +153,17 @@ impl Perform<CommentResponse> for Oper<EditComment> {
         );
 
       if !editors.contains(&user_id) {
-        return Err(APIError::err(&self.op, "Not allowed to edit comment."))?
+        return Err(APIError::err(&self.op, "no_comment_edit_allowed"))?
       }
 
       // Check for a community ban
       if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
-        return Err(APIError::err(&self.op, "You have been banned from this community"))?
+        return Err(APIError::err(&self.op, "community_ban"))?
       }
 
       // Check for a site ban
       if UserView::read(&conn, user_id)?.banned {
-        return Err(APIError::err(&self.op, "You have been banned from the site"))?
+        return Err(APIError::err(&self.op, "site_ban"))?
       }
 
     }
@@ -184,7 +184,7 @@ impl Perform<CommentResponse> for Oper<EditComment> {
     let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
       Ok(comment) => comment,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't update Comment"))?
+        return Err(APIError::err(&self.op, "couldnt_update_comment"))?
       }
     };
 
@@ -220,7 +220,7 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -235,14 +235,14 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
       match CommentSaved::save(&conn, &comment_saved_form) {
         Ok(comment) => comment,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Couldnt do comment save"))?
+          return Err(APIError::err(&self.op, "couldnt_save_comment"))?
         }
       };
     } else {
       match CommentSaved::unsave(&conn, &comment_saved_form) {
         Ok(comment) => comment,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Couldnt do comment save"))?
+          return Err(APIError::err(&self.op, "couldnt_save_comment"))?
         }
       };
     }
@@ -266,7 +266,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -275,12 +275,12 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
     // Check for a community ban
     let post = Post::read(&conn, data.post_id)?;
     if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
-      return Err(APIError::err(&self.op, "You have been banned from this community"))?
+      return Err(APIError::err(&self.op, "community_ban"))?
     }
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "You have been banned from the site"))?
+      return Err(APIError::err(&self.op, "site_ban"))?
     }
 
     let like_form = CommentLikeForm {
@@ -299,7 +299,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
       let _inserted_like = match CommentLike::like(&conn, &like_form) {
         Ok(like) => like,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Couldn't like comment."))?
+          return Err(APIError::err(&self.op, "couldnt_like_comment"))?
         }
       };
     }
index be4bb41aa27c5f619bedd4f13c4b5124778fe4f8..a278aa14dec90607da497c287db923c6e3608576 100644 (file)
@@ -22,7 +22,8 @@ pub struct CreateCommunity {
   name: String,
   title: String,
   description: Option<String>,
-  category_id: i32 ,
+  category_id: i32,
+  nsfw: bool,
   auth: String
 }
 
@@ -86,6 +87,7 @@ pub struct EditCommunity {
   category_id: i32,
   removed: Option<bool>,
   deleted: Option<bool>,
+  nsfw: bool,
   reason: Option<String>,
   expires: Option<i64>,
   auth: String
@@ -109,6 +111,13 @@ pub struct GetFollowedCommunitiesResponse {
   communities: Vec<CommunityFollowerView>
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct TransferCommunity {
+  community_id: i32,
+  user_id: i32,
+  auth: String
+}
+
 impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
   fn perform(&self) -> Result<GetCommunityResponse, Error> {
     let data: &GetCommunity = &self.data;
@@ -135,18 +144,22 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
     let community_view = match CommunityView::read(&conn, community_id, user_id) {
       Ok(community) => community,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't find Community"))?
+        return Err(APIError::err(&self.op, "couldnt_find_community"))?
       }
     };
 
     let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
       Ok(moderators) => moderators,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't find Community"))?
+        return Err(APIError::err(&self.op, "couldnt_find_community"))?
       }
     };
 
-    let admins = UserView::admins(&conn)?;
+    let site_creator_id = Site::read(&conn, 1)?.creator_id;
+    let mut admins = UserView::admins(&conn)?;
+    let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
+    let creator_user = admins.remove(creator_index);
+    admins.insert(0, creator_user);
 
     // Return the jwt
     Ok(
@@ -168,21 +181,21 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
     if has_slurs(&data.name) || 
       has_slurs(&data.title) || 
         (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap())) {
-          return Err(APIError::err(&self.op, "No slurs"))?
+          return Err(APIError::err(&self.op, "no_slurs"))?
         }
 
     let user_id = claims.id;
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "You have been banned from the site"))?
+      return Err(APIError::err(&self.op, "site_ban"))?
     }
 
     // When you create a community, make sure the user becomes a moderator and a follower
@@ -194,13 +207,14 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
       creator_id: user_id,
       removed: None,
       deleted: None,
+      nsfw: data.nsfw,
       updated: None,
     };
 
     let inserted_community = match Community::create(&conn, &community_form) {
       Ok(community) => community,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Community already exists."))?
+        return Err(APIError::err(&self.op, "community_already_exists"))?
       }
     };
 
@@ -212,7 +226,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
     let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) {
       Ok(user) => user,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Community moderator already exists."))?
+        return Err(APIError::err(&self.op, "community_moderator_already_exists"))?
       }
     };
 
@@ -224,7 +238,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
     let _inserted_community_follower = match CommunityFollower::follow(&conn, &community_follower_form) {
       Ok(user) => user,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Community follower already exists."))?
+        return Err(APIError::err(&self.op, "community_follower_already_exists"))?
       }
     };
 
@@ -244,7 +258,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
     let data: &EditCommunity = &self.data;
 
     if has_slurs(&data.name) || has_slurs(&data.title) {
-      return Err(APIError::err(&self.op, "No slurs"))?
+      return Err(APIError::err(&self.op, "no_slurs"))?
     }
 
     let conn = establish_connection();
@@ -252,7 +266,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -260,7 +274,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "You have been banned from the site"))?
+      return Err(APIError::err(&self.op, "site_ban"))?
     }
 
     // Verify its a mod
@@ -280,7 +294,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
       .collect()
       );
     if !editors.contains(&user_id) {
-      return Err(APIError::err(&self.op, "Not allowed to edit community"))?
+      return Err(APIError::err(&self.op, "no_community_edit_allowed"))?
     }
 
     let community_form = CommunityForm {
@@ -291,13 +305,14 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
       creator_id: user_id,
       removed: data.removed.to_owned(),
       deleted: data.deleted.to_owned(),
+      nsfw: data.nsfw,
       updated: Some(naive_now())
     };
 
     let _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
       Ok(community) => community,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't update Community"))?
+        return Err(APIError::err(&self.op, "couldnt_update_community"))?
       }
     };
 
@@ -333,22 +348,38 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
     let data: &ListCommunities = &self.data;
     let conn = establish_connection();
 
-    let user_id: Option<i32> = match &data.auth {
+    let user_claims: Option<Claims> = match &data.auth {
       Some(auth) => {
         match Claims::decode(&auth) {
           Ok(claims) => {
-            let user_id = claims.claims.id;
-            Some(user_id)
+            Some(claims.claims)
           }
           Err(_e) => None
         }
       }
       None => None
     };
+    
+    let user_id = match &user_claims {
+      Some(claims) => Some(claims.id),
+      None => None
+    };
+
+    let show_nsfw = match &user_claims {
+      Some(claims) => claims.show_nsfw,
+      None => false
+    };
 
     let sort = SortType::from_str(&data.sort)?;
 
-    let communities: Vec<CommunityView> = CommunityView::list(&conn, user_id, sort, data.page, data.limit)?;
+    let communities: Vec<CommunityView> = CommunityView::list(
+      &conn, 
+      &sort, 
+      user_id, 
+      show_nsfw, 
+      None, 
+      data.page, 
+      data.limit)?;
 
     // Return the jwt
     Ok(
@@ -369,7 +400,7 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -384,14 +415,14 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
       match CommunityFollower::follow(&conn, &community_follower_form) {
         Ok(user) => user,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Community follower already exists."))?
+          return Err(APIError::err(&self.op, "community_follower_already_exists"))?
         }
       };
     } else {
       match CommunityFollower::ignore(&conn, &community_follower_form) {
         Ok(user) => user,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Community follower already exists."))?
+          return Err(APIError::err(&self.op, "community_follower_already_exists"))?
         }
       };
     }
@@ -416,7 +447,7 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -425,7 +456,7 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
     let communities: Vec<CommunityFollowerView> = match CommunityFollowerView::for_user(&conn, user_id) {
       Ok(communities) => communities,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "System error, try logging out and back in."))?
+        return Err(APIError::err(&self.op, "system_err_login"))?
       }
     };
 
@@ -448,7 +479,7 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -463,14 +494,14 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
       match CommunityUserBan::ban(&conn, &community_user_ban_form) {
         Ok(user) => user,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Community user ban already exists"))?
+          return Err(APIError::err(&self.op, "community_user_already_banned"))?
         }
       };
     } else {
       match CommunityUserBan::unban(&conn, &community_user_ban_form) {
         Ok(user) => user,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Community user ban already exists"))?
+          return Err(APIError::err(&self.op, "community_user_already_banned"))?
         }
       };
     }
@@ -511,7 +542,7 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -526,14 +557,14 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
       match CommunityModerator::join(&conn, &community_moderator_form) {
         Ok(user) => user,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Community moderator already exists."))?
+          return Err(APIError::err(&self.op, "community_moderator_already_exists"))?
         }
       };
     } else {
       match CommunityModerator::leave(&conn, &community_moderator_form) {
         Ok(user) => user,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Community moderator already exists."))?
+          return Err(APIError::err(&self.op, "community_moderator_already_exists"))?
         }
       };
     }
@@ -557,3 +588,109 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
       )
   }
 }
+  
+impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
+  fn perform(&self) -> Result<GetCommunityResponse, Error> {
+    let data: &TransferCommunity = &self.data;
+    let conn = establish_connection();
+
+    let claims = match Claims::decode(&data.auth) {
+      Ok(claims) => claims.claims,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "not_logged_in"))?
+      }
+    };
+
+    let user_id = claims.id;
+
+    let read_community = Community::read(&conn, data.community_id)?;
+
+    let site_creator_id = Site::read(&conn, 1)?.creator_id;
+    let mut admins = UserView::admins(&conn)?;
+    let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
+    let creator_user = admins.remove(creator_index);
+    admins.insert(0, creator_user);
+
+
+    // Make sure user is the creator, or an admin
+    if user_id != read_community.creator_id && !admins.iter().map(|a| a.id).collect::<Vec<i32>>().contains(&user_id) {
+      return Err(APIError::err(&self.op, "not_an_admin"))?
+    }
+    
+    let community_form = CommunityForm {
+      name: read_community.name,
+      title: read_community.title,
+      description: read_community.description,
+      category_id: read_community.category_id,
+      creator_id: data.user_id,
+      removed: None,
+      deleted: None,
+      nsfw: read_community.nsfw,
+      updated: Some(naive_now())
+    };
+
+    let _updated_community = match Community::update(&conn, data.community_id, &community_form) {
+      Ok(community) => community,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "couldnt_update_community"))?
+      }
+    };
+
+    // You also have to re-do the community_moderator table, reordering it.
+    let mut community_mods = CommunityModeratorView::for_community(&conn, data.community_id)?;
+    let creator_index = community_mods.iter().position(|r| r.user_id == data.user_id).unwrap();
+    let creator_user = community_mods.remove(creator_index);
+    community_mods.insert(0, creator_user);
+
+    CommunityModerator::delete_for_community(&conn, data.community_id)?;
+
+    for cmod in &community_mods {
+
+      let community_moderator_form = CommunityModeratorForm {
+        community_id: cmod.community_id,
+        user_id: cmod.user_id
+      };
+
+      let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) {
+        Ok(user) => user,
+        Err(_e) => {
+          return Err(APIError::err(&self.op, "community_moderator_already_exists"))?
+        }
+      };
+    }
+
+    // Mod tables
+    let form = ModAddCommunityForm {
+      mod_user_id: user_id,
+      other_user_id: data.user_id,
+      community_id: data.community_id,
+      removed: Some(false),
+    };
+    ModAddCommunity::create(&conn, &form)?;
+
+    let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) {
+      Ok(community) => community,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "couldnt_find_community"))?
+      }
+    };
+
+    let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) {
+      Ok(moderators) => moderators,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "couldnt_find_community"))?
+      }
+    };
+
+
+    // Return the jwt
+    Ok(
+      GetCommunityResponse {
+        op: self.op.to_string(),
+        community: community_view,
+        moderators: moderators,
+        admins: admins,
+      }
+      )
+  }
+}
index 6e3e8269da4b7f393713bce48809149635891c63..ac11d30cceb77c886c5727449e700aa0f6f93bf1 100644 (file)
@@ -1,18 +1,18 @@
 use serde::{Deserialize, Serialize};
 use failure::Error;
-use db::*;
-use db::community::*;
-use db::user::*;
-use db::post::*;
-use db::comment::*;
-use db::post_view::*;
-use db::comment_view::*;
-use db::category::*;
-use db::community_view::*;
-use db::user_view::*;
-use db::moderator_views::*;
-use db::moderator::*;
-use {has_slurs, remove_slurs, Settings, naive_now, naive_from_unix};
+use crate::db::*;
+use crate::db::community::*;
+use crate::db::user::*;
+use crate::db::post::*;
+use crate::db::comment::*;
+use crate::db::post_view::*;
+use crate::db::comment_view::*;
+use crate::db::category::*;
+use crate::db::community_view::*;
+use crate::db::user_view::*;
+use crate::db::moderator_views::*;
+use crate::db::moderator::*;
+use crate::{has_slurs, remove_slurs, Settings, naive_now, naive_from_unix};
 
 pub mod user;
 pub mod community;
@@ -22,7 +22,7 @@ pub mod site;
 
 #[derive(EnumString,ToString,Debug)]
 pub enum UserOperation {
-  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead
+  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead, SaveUserSettings, TransferCommunity, TransferSite
 }
 
 #[derive(Fail, Debug)]
index a60107812013f7e5952d21872a5146de628f0761..8fc24ac1858fc6f171757874c552a5f627a71fd2 100644 (file)
@@ -6,6 +6,7 @@ pub struct CreatePost {
   name: String,
   url: Option<String>,
   body: Option<String>,
+  nsfw: bool,
   community_id: i32,
   auth: String
 }
@@ -73,6 +74,7 @@ pub struct EditPost {
   body: Option<String>,
   removed: Option<bool>,
   deleted: Option<bool>,
+  nsfw: bool,
   locked: Option<bool>,
   reason: Option<String>,
   auth: String
@@ -94,25 +96,25 @@ impl Perform<PostResponse> for Oper<CreatePost> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
     if has_slurs(&data.name) || 
       (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
-        return Err(APIError::err(&self.op, "No slurs"))?
+        return Err(APIError::err(&self.op, "no_slurs"))?
       }
 
     let user_id = claims.id;
 
     // Check for a community ban
     if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
-      return Err(APIError::err(&self.op, "You have been banned from this community"))?
+      return Err(APIError::err(&self.op, "community_ban"))?
     }
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "You have been banned from the site"))?
+      return Err(APIError::err(&self.op, "site_ban"))?
     }
 
     let post_form = PostForm {
@@ -123,6 +125,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
       creator_id: user_id,
       removed: None,
       deleted: None,
+      nsfw: data.nsfw,
       locked: None,
       updated: None
     };
@@ -130,7 +133,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
     let inserted_post = match Post::create(&conn, &post_form) {
       Ok(post) => post,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't create Post"))?
+        return Err(APIError::err(&self.op, "couldnt_create_post"))?
       }
     };
 
@@ -145,7 +148,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
     let _inserted_like = match PostLike::like(&conn, &like_form) {
       Ok(like) => like,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't like post."))?
+        return Err(APIError::err(&self.op, "couldnt_like_post"))?
       }
     };
 
@@ -153,7 +156,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
     let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
       Ok(post) => post,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't find Post"))?
+        return Err(APIError::err(&self.op, "couldnt_find_post"))?
       }
     };
 
@@ -187,7 +190,7 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
     let post_view = match PostView::read(&conn, data.id, user_id) {
       Ok(post) => post,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't find Post"))?
+        return Err(APIError::err(&self.op, "couldnt_find_post"))?
       }
     };
 
@@ -197,7 +200,11 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
 
     let moderators = CommunityModeratorView::for_community(&conn, post_view.community_id)?;
 
-    let admins = UserView::admins(&conn)?;
+    let site_creator_id = Site::read(&conn, 1)?.creator_id;
+    let mut admins = UserView::admins(&conn)?;
+    let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
+    let creator_user = admins.remove(creator_index);
+    admins.insert(0, creator_user);
 
     // Return the jwt
     Ok(
@@ -219,40 +226,51 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
     let data: &GetPosts = &self.data;
     let conn = establish_connection();
 
-    let user_id: Option<i32> = match &data.auth {
+    let user_claims: Option<Claims> = match &data.auth {
       Some(auth) => {
         match Claims::decode(&auth) {
           Ok(claims) => {
-            let user_id = claims.claims.id;
-            Some(user_id)
+            Some(claims.claims)
           }
           Err(_e) => None
         }
       }
       None => None
     };
+    
+    let user_id = match &user_claims {
+      Some(claims) => Some(claims.id),
+      None => None
+    };
+
+    let show_nsfw = match &user_claims {
+      Some(claims) => claims.show_nsfw,
+      None => false
+    };
 
     let type_ = PostListingType::from_str(&data.type_)?;
     let sort = SortType::from_str(&data.sort)?;
 
-    let posts = match PostView::list(&conn, 
-                                     type_, 
-                                     &sort, 
-                                     data.community_id, 
-                                     None,
-                                     None,
-                                     user_id, 
-                                     false, 
-                                     false, 
-                                     data.page, 
-                                     data.limit) {
+    let posts = match PostView::list(
+      &conn, 
+      type_, 
+      &sort, 
+      data.community_id, 
+      None,
+      None,
+      None,
+      user_id, 
+      show_nsfw,
+      false, 
+      false, 
+      data.page, 
+      data.limit) {
       Ok(posts) => posts,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't get posts"))?
+        return Err(APIError::err(&self.op, "couldnt_get_posts"))?
       }
     };
 
-    // Return the jwt
     Ok(
       GetPostsResponse {
         op: self.op.to_string(),
@@ -270,7 +288,7 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -279,12 +297,12 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
     // Check for a community ban
     let post = Post::read(&conn, data.post_id)?;
     if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
-      return Err(APIError::err(&self.op, "You have been banned from this community"))?
+      return Err(APIError::err(&self.op, "community_ban"))?
     }
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "You have been banned from the site"))?
+      return Err(APIError::err(&self.op, "site_ban"))?
     }
 
     let like_form = PostLikeForm {
@@ -302,7 +320,7 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
       let _inserted_like = match PostLike::like(&conn, &like_form) {
         Ok(like) => like,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Couldn't like post."))?
+          return Err(APIError::err(&self.op, "couldnt_like_post"))?
         }
       };
     }
@@ -310,7 +328,7 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
     let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
       Ok(post) => post,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't find Post"))?
+        return Err(APIError::err(&self.op, "couldnt_find_post"))?
       }
     };
 
@@ -329,7 +347,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
     let data: &EditPost = &self.data;
     if has_slurs(&data.name) || 
       (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
-        return Err(APIError::err(&self.op, "No slurs"))?
+        return Err(APIError::err(&self.op, "no_slurs"))?
       }
 
     let conn = establish_connection();
@@ -337,7 +355,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -360,17 +378,17 @@ impl Perform<PostResponse> for Oper<EditPost> {
       .collect()
       );
     if !editors.contains(&user_id) {
-      return Err(APIError::err(&self.op, "Not allowed to edit post."))?
+      return Err(APIError::err(&self.op, "no_post_edit_allowed"))?
     }
 
     // Check for a community ban
     if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
-      return Err(APIError::err(&self.op, "You have been banned from this community"))?
+      return Err(APIError::err(&self.op, "community_ban"))?
     }
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "You have been banned from the site"))?
+      return Err(APIError::err(&self.op, "site_ban"))?
     }
 
     let post_form = PostForm {
@@ -381,6 +399,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
       community_id: data.community_id,
       removed: data.removed.to_owned(),
       deleted: data.deleted.to_owned(),
+      nsfw: data.nsfw,
       locked: data.locked.to_owned(),
       updated: Some(naive_now())
     };
@@ -388,7 +407,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
     let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
       Ok(post) => post,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't update Post"))?
+        return Err(APIError::err(&self.op, "couldnt_update_post"))?
       }
     };
 
@@ -431,7 +450,7 @@ impl Perform<PostResponse> for Oper<SavePost> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -446,14 +465,14 @@ impl Perform<PostResponse> for Oper<SavePost> {
       match PostSaved::save(&conn, &post_saved_form) {
         Ok(post) => post,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Couldnt do post save"))?
+          return Err(APIError::err(&self.op, "couldnt_save_post"))?
         }
       };
     } else {
       match PostSaved::unsave(&conn, &post_saved_form) {
         Ok(post) => post,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Couldnt do post save"))?
+          return Err(APIError::err(&self.op, "couldnt_save_post"))?
         }
       };
     }
index 03ee90ff19a9ca415dd7ca56c17b8cad3378d53f..7827913b970c7563c584269f26d3eba4c8b445b1 100644 (file)
@@ -23,8 +23,11 @@ pub struct Search {
 #[derive(Serialize, Deserialize)]
 pub struct SearchResponse {
   op: String,
+  type_: String,
   comments: Vec<CommentView>,
   posts: Vec<PostView>,
+  communities: Vec<CommunityView>,
+  users: Vec<UserView>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -64,8 +67,7 @@ pub struct EditSite {
 }
 
 #[derive(Serialize, Deserialize)]
-pub struct GetSite {
-}
+pub struct GetSite;
 
 #[derive(Serialize, Deserialize)]
 pub struct SiteResponse {
@@ -81,6 +83,12 @@ pub struct GetSiteResponse {
   banned: Vec<UserView>,
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct TransferSite {
+  user_id: i32,
+  auth: String
+}
+
 impl Perform<ListCategoriesResponse> for Oper<ListCategories> {
   fn perform(&self) -> Result<ListCategoriesResponse, Error> {
     let _data: &ListCategories = &self.data;
@@ -145,20 +153,20 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
     if has_slurs(&data.name) || 
       (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap())) {
-        return Err(APIError::err(&self.op, "No slurs"))?
+        return Err(APIError::err(&self.op, "no_slurs"))?
       }
 
     let user_id = claims.id;
 
     // Make sure user is an admin
     if !UserView::read(&conn, user_id)?.admin {
-      return Err(APIError::err(&self.op, "Not an admin."))?
+      return Err(APIError::err(&self.op, "not_an_admin"))?
     }
 
     let site_form = SiteForm {
@@ -171,7 +179,7 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
     match Site::create(&conn, &site_form) {
       Ok(site) => site,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Site exists already"))?
+        return Err(APIError::err(&self.op, "site_already_exists"))?
       }
     };
 
@@ -195,20 +203,20 @@ impl Perform<SiteResponse> for Oper<EditSite> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
     if has_slurs(&data.name) || 
       (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap())) {
-        return Err(APIError::err(&self.op, "No slurs"))?
+        return Err(APIError::err(&self.op, "no_slurs"))?
       }
 
     let user_id = claims.id;
 
     // Make sure user is an admin
     if UserView::read(&conn, user_id)?.admin == false {
-      return Err(APIError::err(&self.op, "Not an admin."))?
+      return Err(APIError::err(&self.op, "not_an_admin"))?
     }
 
     let found_site = Site::read(&conn, 1)?;
@@ -223,7 +231,7 @@ impl Perform<SiteResponse> for Oper<EditSite> {
     match Site::update(&conn, 1, &site_form) {
       Ok(site) => site,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't update site."))?
+        return Err(APIError::err(&self.op, "couldnt_update_site"))?
       }
     };
 
@@ -249,7 +257,14 @@ impl Perform<GetSiteResponse> for Oper<GetSite> {
       Err(_e) => None
     };
 
-    let admins = UserView::admins(&conn)?;
+    let mut admins = UserView::admins(&conn)?;
+    if site_view.is_some() {
+      let site_creator_id = site_view.to_owned().unwrap().creator_id;
+      let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
+      let creator_user = admins.remove(creator_index);
+      admins.insert(0, creator_user);
+    }
+
     let banned = UserView::banned(&conn)?;
 
     Ok(
@@ -273,53 +288,113 @@ impl Perform<SearchResponse> for Oper<Search> {
 
     let mut posts = Vec::new();
     let mut comments = Vec::new();
+    let mut communities = Vec::new();
+    let mut users = Vec::new();
+
+    // TODO no clean / non-nsfw searching rn
 
     match type_ {
       SearchType::Posts => {
-        posts = PostView::list(&conn, 
-                               PostListingType::All, 
-                               &sort, 
-                               data.community_id, 
-                               None,
-                               Some(data.q.to_owned()),
-                               None, 
-                               false, 
-                               false, 
-                               data.page, 
-                               data.limit)?;
+        posts = PostView::list(
+          &conn, 
+          PostListingType::All, 
+          &sort, 
+          data.community_id, 
+          None,
+          Some(data.q.to_owned()),
+          None,
+          None, 
+          true,
+          false, 
+          false, 
+          data.page, 
+          data.limit)?;
       },
       SearchType::Comments => {
-        comments = CommentView::list(&conn, 
-                                     &sort, 
-                                     None, 
-                                     None, 
-                                     Some(data.q.to_owned()),
-                                     None,
-                                     false, 
-                                     data.page,
-                                     data.limit)?;
+        comments = CommentView::list(
+          &conn, 
+          &sort, 
+          None, 
+          None, 
+          Some(data.q.to_owned()),
+          None,
+          false, 
+          data.page,
+          data.limit)?;
+      },
+      SearchType::Communities => {
+        communities = CommunityView::list(
+          &conn, 
+          &sort, 
+          None, 
+          true,
+          Some(data.q.to_owned()),
+          data.page, 
+          data.limit)?;
+      }, 
+      SearchType::Users => {
+        users = UserView::list(
+          &conn, 
+          &sort, 
+          Some(data.q.to_owned()), 
+          data.page, 
+          data.limit)?;
       }, 
-      SearchType::Both => {
-        posts = PostView::list(&conn, 
-                               PostListingType::All, 
-                               &sort, 
-                               data.community_id, 
-                               None,
-                               Some(data.q.to_owned()),
-                               None, 
-                               false, 
-                               false, 
-                               data.page, 
-                               data.limit)?;
-        comments = CommentView::list(&conn, 
-                                     &sort, 
-                                     None, 
-                                     None, 
-                                     Some(data.q.to_owned()),
-                                     None,
-                                     false, 
-                                     data.page,
-                                     data.limit)?;
+      SearchType::All => {
+        posts = PostView::list(
+          &conn, 
+          PostListingType::All, 
+          &sort, 
+          data.community_id, 
+          None,
+          Some(data.q.to_owned()),
+          None,
+          None, 
+          true,
+          false, 
+          false, 
+          data.page, 
+          data.limit)?;
+        comments = CommentView::list(
+          &conn, 
+          &sort, 
+          None, 
+          None, 
+          Some(data.q.to_owned()),
+          None,
+          false, 
+          data.page,
+          data.limit)?;
+        communities = CommunityView::list(
+          &conn, 
+          &sort, 
+          None, 
+          true,
+          Some(data.q.to_owned()),
+          data.page, 
+          data.limit)?;
+        users = UserView::list(
+          &conn, 
+          &sort, 
+          Some(data.q.to_owned()), 
+          data.page, 
+          data.limit)?;
+      },
+      SearchType::Url => {
+        posts = PostView::list(
+          &conn, 
+          PostListingType::All, 
+          &sort, 
+          data.community_id, 
+          None,
+          None,
+          Some(data.q.to_owned()),
+          None, 
+          true,
+          false, 
+          false, 
+          data.page, 
+          data.limit)?;
       }
     };
 
@@ -328,9 +403,77 @@ impl Perform<SearchResponse> for Oper<Search> {
     Ok(
       SearchResponse {
         op: self.op.to_string(),
+        type_: data.type_.to_owned(),
         comments: comments,
         posts: posts,
+        communities: communities,
+        users: users,
       }
       )
   }
 }
+
+impl Perform<GetSiteResponse> for Oper<TransferSite> {
+  fn perform(&self) -> Result<GetSiteResponse, Error> {
+    let data: &TransferSite = &self.data;
+    let conn = establish_connection();
+
+    let claims = match Claims::decode(&data.auth) {
+      Ok(claims) => claims.claims,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "not_logged_in"))?
+      }
+    };
+
+    let user_id = claims.id;
+
+    let read_site = Site::read(&conn, 1)?;
+
+    // Make sure user is the creator
+    if read_site.creator_id != user_id {
+      return Err(APIError::err(&self.op, "not_an_admin"))?
+    }
+
+    let site_form = SiteForm {
+      name: read_site.name,
+      description: read_site.description,
+      creator_id: data.user_id,
+      updated: Some(naive_now()),
+    };
+
+    match Site::update(&conn, 1, &site_form) {
+      Ok(site) => site,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "couldnt_update_site"))?
+      }
+    };
+
+    // Mod tables
+    let form = ModAddForm {
+      mod_user_id: user_id,
+      other_user_id: data.user_id,
+      removed: Some(false),
+    };
+
+    ModAdd::create(&conn, &form)?;
+
+    let site_view = SiteView::read(&conn)?;
+
+    let mut admins = UserView::admins(&conn)?;
+    let creator_index = admins.iter().position(|r| r.id == site_view.creator_id).unwrap();
+    let creator_user = admins.remove(creator_index);
+    admins.insert(0, creator_user);
+
+    let banned = UserView::banned(&conn)?;
+
+    Ok(
+      GetSiteResponse {
+        op: self.op.to_string(), 
+        site: Some(site_view),
+        admins: admins,
+        banned: banned,
+      }
+      )
+  }
+}
+
index 9361ca4d8152637493e7e1500005655a82d0a191..d8610fa9a9b40d7bb3fe42bb87ad5ff6e03a6452 100644 (file)
@@ -15,6 +15,13 @@ pub struct Register {
   password: String,
   password_verify: String,
   admin: bool,
+  show_nsfw: bool,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct SaveUserSettings {
+  show_nsfw: bool,
+  auth: String,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -102,13 +109,13 @@ impl Perform<LoginResponse> for Oper<Login> {
     // Fetch that username / email
     let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) {
       Ok(user) => user,
-      Err(_e) => return Err(APIError::err(&self.op, "Couldn't find that username or email"))?
+      Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email"))?
     };
 
     // Verify the password
     let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
     if !valid {
-      return Err(APIError::err(&self.op, "Password incorrect"))?
+      return Err(APIError::err(&self.op, "password_incorrect"))?
     }
 
     // Return the jwt
@@ -129,16 +136,16 @@ impl Perform<LoginResponse> for Oper<Register> {
 
     // Make sure passwords match
     if &data.password != &data.password_verify {
-      return Err(APIError::err(&self.op, "Passwords do not match."))?
+      return Err(APIError::err(&self.op, "passwords_dont_match"))?
     }
 
     if has_slurs(&data.username) {
-      return Err(APIError::err(&self.op, "No slurs"))?
+      return Err(APIError::err(&self.op, "no_slurs"))?
     }
 
     // Make sure there are no admins
     if data.admin && UserView::admins(&conn)?.len() > 0 {
-      return Err(APIError::err(&self.op, "Sorry, there's already an admin."))?
+      return Err(APIError::err(&self.op, "admin_already_created"))?
     }
 
     // Register the new user
@@ -151,40 +158,60 @@ impl Perform<LoginResponse> for Oper<Register> {
       updated: None,
       admin: data.admin,
       banned: false,
+      show_nsfw: data.show_nsfw,
     };
 
     // Create the user
     let inserted_user = match User_::register(&conn, &user_form) {
       Ok(user) => user,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "User already exists."))?
+        return Err(APIError::err(&self.op, "user_already_exists"))?
+      }
+    };
+
+    // Create the main community if it doesn't exist
+    let main_community: Community = match Community::read(&conn, 2) {
+      Ok(c) => c,
+      Err(_e) => {
+        let community_form = CommunityForm {
+          name: "main".to_string(),
+          title: "The Default Community".to_string(),
+          description: Some("The Default Community".to_string()),
+          category_id: 1,
+          nsfw: false,
+          creator_id: inserted_user.id,
+          removed: None,
+          deleted: None,
+          updated: None,
+        };
+        Community::create(&conn, &community_form).unwrap()
       }
     };
 
     // Sign them up for main community no matter what
     let community_follower_form = CommunityFollowerForm {
-      community_id: 1,
+      community_id: main_community.id,
       user_id: inserted_user.id,
     };
 
     let _inserted_community_follower = match CommunityFollower::follow(&conn, &community_follower_form) {
       Ok(user) => user,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Community follower already exists."))?
+        return Err(APIError::err(&self.op, "community_follower_already_exists"))?
       }
     };
 
     // If its an admin, add them as a mod and follower to main
     if data.admin {
       let community_moderator_form = CommunityModeratorForm {
-        community_id: 1,
+        community_id: main_community.id,
         user_id: inserted_user.id,
       };
 
       let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) {
         Ok(user) => user,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Community moderator already exists."))?
+          return Err(APIError::err(&self.op, "community_moderator_already_exists"))?
         }
       };
 
@@ -200,24 +227,77 @@ impl Perform<LoginResponse> for Oper<Register> {
   }
 }
 
+impl Perform<LoginResponse> for Oper<SaveUserSettings> {
+  fn perform(&self) -> Result<LoginResponse, Error> {
+    let data: &SaveUserSettings = &self.data;
+    let conn = establish_connection();
+
+    let claims = match Claims::decode(&data.auth) {
+      Ok(claims) => claims.claims,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "not_logged_in"))?
+      }
+    };
+
+    let user_id = claims.id;
+    
+    let read_user = User_::read(&conn, user_id)?;
+
+    let user_form = UserForm {
+      name: read_user.name,
+      fedi_name: read_user.fedi_name,
+      email: read_user.email,
+      password_encrypted: read_user.password_encrypted,
+      preferred_username: read_user.preferred_username,
+      updated: Some(naive_now()),
+      admin: read_user.admin,
+      banned: read_user.banned,
+      show_nsfw: data.show_nsfw,
+    };
+
+    let updated_user = match User_::update(&conn, user_id, &user_form) {
+      Ok(user) => user,
+      Err(_e) => {
+        return Err(APIError::err(&self.op, "couldnt_update_user"))?
+      }
+    };
+
+    // Return the jwt
+    Ok(
+      LoginResponse {
+        op: self.op.to_string(), 
+        jwt: updated_user.jwt()
+      }
+      )
+  }
+}
 
 impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
   fn perform(&self) -> Result<GetUserDetailsResponse, Error> {
     let data: &GetUserDetails = &self.data;
     let conn = establish_connection();
 
-    let user_id: Option<i32> = match &data.auth {
+    let user_claims: Option<Claims> = match &data.auth {
       Some(auth) => {
         match Claims::decode(&auth) {
           Ok(claims) => {
-            let user_id = claims.claims.id;
-            Some(user_id)
+            Some(claims.claims)
           }
           Err(_e) => None
         }
       }
       None => None
     };
+    
+    let user_id = match &user_claims {
+      Some(claims) => Some(claims.id),
+      None => None
+    };
+
+    let show_nsfw = match &user_claims {
+      Some(claims) => claims.show_nsfw,
+      None => false
+    };
 
     //TODO add save
     let sort = SortType::from_str(&data.sort)?;
@@ -231,50 +311,58 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
 
     // If its saved only, you don't care what creator it was
     let posts = if data.saved_only {
-      PostView::list(&conn, 
-                     PostListingType::All, 
-                     &sort, 
-                     data.community_id, 
-                     None, 
-                     None,
-                     Some(user_details_id), 
-                     data.saved_only, 
-                     false, 
-                     data.page, 
-                     data.limit)?
+      PostView::list(
+        &conn, 
+        PostListingType::All, 
+        &sort, 
+        data.community_id, 
+        None, 
+        None,
+        None,
+        Some(user_details_id), 
+        show_nsfw,
+        data.saved_only, 
+        false, 
+        data.page, 
+        data.limit)?
     } else {
-      PostView::list(&conn, 
-                     PostListingType::All, 
-                     &sort, 
-                     data.community_id, 
-                     Some(user_details_id), 
-                     None, 
-                     user_id, 
-                     data.saved_only, 
-                     false, 
-                     data.page, 
-                     data.limit)?
+      PostView::list(
+        &conn, 
+        PostListingType::All, 
+        &sort, 
+        data.community_id, 
+        Some(user_details_id), 
+        None, 
+        None,
+        user_id, 
+        show_nsfw,
+        data.saved_only, 
+        false, 
+        data.page, 
+        data.limit)?
     };
     let comments = if data.saved_only {
-      CommentView::list(&conn, 
-                        &sort, 
-                        None, 
-                        None, 
-                        None, 
-                        Some(user_details_id), 
-                        data.saved_only, 
-                        data.page, 
-                        data.limit)?
+      CommentView::list(
+        &conn, 
+        &sort, 
+        None, 
+        None, 
+        None, 
+        Some(user_details_id), 
+        data.saved_only, 
+        data.page, 
+        data.limit)?
     } else {
-      CommentView::list(&conn, 
-                        &sort, 
-                        None, 
-                        Some(user_details_id), 
-                        None, 
-                        user_id, 
-                        data.saved_only, 
-                        data.page, 
-                        data.limit)?
+      CommentView::list(
+        &conn, 
+        &sort, 
+        None, 
+        Some(user_details_id), 
+        None, 
+        user_id, 
+        data.saved_only, 
+        data.page, 
+        data.limit)?
     };
 
     let follows = CommunityFollowerView::for_user(&conn, user_details_id)?;
@@ -303,7 +391,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -311,7 +399,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
 
     // Make sure user is an admin
     if UserView::read(&conn, user_id)?.admin == false {
-      return Err(APIError::err(&self.op, "Not an admin."))?
+      return Err(APIError::err(&self.op, "not_an_admin"))?
     }
 
     let read_user = User_::read(&conn, data.user_id)?;
@@ -325,12 +413,13 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
       updated: Some(naive_now()),
       admin: data.added,
       banned: read_user.banned,
+      show_nsfw: read_user.show_nsfw,
     };
 
     match User_::update(&conn, data.user_id, &user_form) {
       Ok(user) => user,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't update user"))?
+        return Err(APIError::err(&self.op, "couldnt_update_user"))?
       }
     };
 
@@ -343,7 +432,11 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
 
     ModAdd::create(&conn, &form)?;
 
-    let admins = UserView::admins(&conn)?;
+    let site_creator_id = Site::read(&conn, 1)?.creator_id;
+    let mut admins = UserView::admins(&conn)?;
+    let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
+    let creator_user = admins.remove(creator_index);
+    admins.insert(0, creator_user);
 
     Ok(
       AddAdminResponse {
@@ -362,7 +455,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -370,7 +463,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
 
     // Make sure user is an admin
     if UserView::read(&conn, user_id)?.admin == false {
-      return Err(APIError::err(&self.op, "Not an admin."))?
+      return Err(APIError::err(&self.op, "not_an_admin"))?
     }
 
     let read_user = User_::read(&conn, data.user_id)?;
@@ -384,12 +477,13 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
       updated: Some(naive_now()),
       admin: read_user.admin,
       banned: data.ban,
+      show_nsfw: read_user.show_nsfw,
     };
 
     match User_::update(&conn, data.user_id, &user_form) {
       Ok(user) => user,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Couldn't update user"))?
+        return Err(APIError::err(&self.op, "couldnt_update_user"))?
       }
     };
 
@@ -430,7 +524,7 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -458,7 +552,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
       Err(_e) => {
-        return Err(APIError::err(&self.op, "Not logged in."))?
+        return Err(APIError::err(&self.op, "not_logged_in"))?
       }
     };
 
@@ -481,7 +575,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
       let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
         Ok(comment) => comment,
         Err(_e) => {
-          return Err(APIError::err(&self.op, "Couldn't update Comment"))?
+          return Err(APIError::err(&self.op, "couldnt_update_comment"))?
         }
       };
     }
index 3d9595c810bbfc5f5409182f8c8c114da1d902da..9de32401d722d433bcc94b1c28b81833db19cb2a 100644 (file)
@@ -1,10 +1,10 @@
 extern crate activitypub;
 use self::activitypub::{context, actor::Person};
-use db::user::User_;
+use crate::db::user::User_;
 
 impl User_ {
   pub fn person(&self) -> Person {
-    use {Settings, to_datetime_utc};
+    use crate::{Settings, to_datetime_utc};
     let base_url = &format!("{}/user/{}", Settings::get().api_endpoint(), self.name);
     let mut person  = Person::default();
     person.object_props.set_context_object(context()).ok();
@@ -31,7 +31,7 @@ impl User_ {
 #[cfg(test)]
 mod tests {
   use super::User_;
-  use naive_now;
+  use crate::naive_now;
 
   #[test]
   fn test_person() {
@@ -46,7 +46,8 @@ mod tests {
       published: naive_now(),
       admin: false,
       banned: false,
-      updated: None
+      updated: None,
+      show_nsfw: false,
     };
 
     let person = expected_user.person();
index 99f906d41988b86b786bfedae1a3c99fcccc4d56..eb82258016a3388babbee2de60c6c6cfce1e85fc 100644 (file)
@@ -1,5 +1,5 @@
-use schema::{category};
-use schema::category::dsl::*;
+use crate::schema::{category};
+use crate::schema::category::dsl::*;
 use super::*;
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
index a924bd4c86f4ae09d42e7cb1772a4f93249f970a..7901357f04d43371e3fa68a4bb45d181b77f94f6 100644 (file)
@@ -1,4 +1,4 @@
-use schema::{comment, comment_like, comment_saved};
+use crate::schema::{comment, comment_like, comment_saved};
 use super::*;
 use super::post::Post;
 
@@ -40,26 +40,26 @@ pub struct CommentForm {
 
 impl Crud<CommentForm> for Comment {
   fn read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
-    use schema::comment::dsl::*;
+    use crate::schema::comment::dsl::*;
     comment.find(comment_id)
       .first::<Self>(conn)
   }
 
   fn delete(conn: &PgConnection, comment_id: i32) -> Result<usize, Error> {
-    use schema::comment::dsl::*;
+    use crate::schema::comment::dsl::*;
     diesel::delete(comment.find(comment_id))
       .execute(conn)
   }
 
   fn create(conn: &PgConnection, comment_form: &CommentForm) -> Result<Self, Error> {
-    use schema::comment::dsl::*;
+    use crate::schema::comment::dsl::*;
     insert_into(comment)
       .values(comment_form)
       .get_result::<Self>(conn)
   }
 
   fn update(conn: &PgConnection, comment_id: i32, comment_form: &CommentForm) -> Result<Self, Error> {
-    use schema::comment::dsl::*;
+    use crate::schema::comment::dsl::*;
     diesel::update(comment.find(comment_id))
       .set(comment_form)
       .get_result::<Self>(conn)
@@ -89,30 +89,31 @@ pub struct CommentLikeForm {
 
 impl Likeable <CommentLikeForm> for CommentLike {
   fn read(conn: &PgConnection, comment_id_from: i32) -> Result<Vec<Self>, Error> {
-    use schema::comment_like::dsl::*;
+    use crate::schema::comment_like::dsl::*;
     comment_like
       .filter(comment_id.eq(comment_id_from))
       .load::<Self>(conn) 
   }
 
   fn like(conn: &PgConnection, comment_like_form: &CommentLikeForm) -> Result<Self, Error> {
-    use schema::comment_like::dsl::*;
+    use crate::schema::comment_like::dsl::*;
     insert_into(comment_like)
       .values(comment_like_form)
       .get_result::<Self>(conn)
   }
   fn remove(conn: &PgConnection, comment_like_form: &CommentLikeForm) -> Result<usize, Error> {
-    use schema::comment_like::dsl::*;
-    diesel::delete(comment_like
-                   .filter(comment_id.eq(comment_like_form.comment_id))
-                   .filter(user_id.eq(comment_like_form.user_id)))
+    use crate::schema::comment_like::dsl::*;
+    diesel::delete(
+      comment_like
+      .filter(comment_id.eq(comment_like_form.comment_id))
+      .filter(user_id.eq(comment_like_form.user_id)))
       .execute(conn)
   }
 }
 
 impl CommentLike {
   pub fn from_post(conn: &PgConnection, post_id_from: i32) -> Result<Vec<Self>, Error> {
-    use schema::comment_like::dsl::*;
+    use crate::schema::comment_like::dsl::*;
     comment_like
       .filter(post_id.eq(post_id_from))
       .load::<Self>(conn) 
@@ -138,13 +139,13 @@ pub struct CommentSavedForm {
 
 impl Saveable <CommentSavedForm> for CommentSaved {
   fn save(conn: &PgConnection, comment_saved_form: &CommentSavedForm) -> Result<Self, Error> {
-    use schema::comment_saved::dsl::*;
+    use crate::schema::comment_saved::dsl::*;
     insert_into(comment_saved)
       .values(comment_saved_form)
       .get_result::<Self>(conn)
   }
   fn unsave(conn: &PgConnection, comment_saved_form: &CommentSavedForm) -> Result<usize, Error> {
-    use schema::comment_saved::dsl::*;
+    use crate::schema::comment_saved::dsl::*;
     diesel::delete(comment_saved
       .filter(comment_id.eq(comment_saved_form.comment_id))
       .filter(user_id.eq(comment_saved_form.user_id)))
@@ -170,7 +171,8 @@ mod tests {
       email: None,
       admin: false,
       banned: false,
-      updated: None
+      updated: None,
+      show_nsfw: false,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -183,7 +185,8 @@ mod tests {
       creator_id: inserted_user.id,
       removed: None,
       deleted: None,
-      updated: None
+      updated: None,
+      nsfw: false,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -197,7 +200,8 @@ mod tests {
       removed: None,
       deleted: None,
       locked: None,
-      updated: None
+      updated: None,
+      nsfw: false,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
index 9cd61e33e9ad14462dc9a65b40de71093063d8ce..8a6545ba23bb93244aa2e9ccd442a843f3744ca5 100644 (file)
@@ -94,7 +94,7 @@ impl CommentView {
     }
 
     query = match sort {
-      // SortType::Hot => query.order_by(hot_rank.desc()),
+      // SortType::Hot => query.order(hot_rank.desc(), published.desc()),
       SortType::New => query.order_by(published.desc()),
       SortType::TopAll => query.order_by(score.desc()),
       SortType::TopYear => query
@@ -261,7 +261,8 @@ mod tests {
       email: None,
       admin: false,
       banned: false,
-      updated: None
+      updated: None,
+      show_nsfw: false,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -274,7 +275,8 @@ mod tests {
       creator_id: inserted_user.id,
       removed: None,
       deleted: None,
-      updated: None
+      updated: None,
+      nsfw: false,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -288,7 +290,8 @@ mod tests {
       removed: None,
       deleted: None,
       locked: None,
-      updated: None
+      updated: None,
+      nsfw: false,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
index 4540f731afa82ca7aeadc9d835bc9ef0779802af..e07b5c00306f00e97999b9ec0d4ce393efa5ad22 100644 (file)
@@ -1,4 +1,4 @@
-use schema::{community, community_moderator, community_follower, community_user_ban, site};
+use crate::schema::{community, community_moderator, community_follower, community_user_ban, site};
 use super::*;
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
@@ -14,6 +14,7 @@ pub struct Community {
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
+  pub nsfw: bool,
 }
 
 #[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
@@ -27,30 +28,31 @@ pub struct CommunityForm {
   pub removed: Option<bool>,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: Option<bool>,
+  pub nsfw: bool,
 }
 
 impl Crud<CommunityForm> for Community {
   fn read(conn: &PgConnection, community_id: i32) -> Result<Self, Error> {
-    use schema::community::dsl::*;
+    use crate::schema::community::dsl::*;
     community.find(community_id)
       .first::<Self>(conn)
   }
 
   fn delete(conn: &PgConnection, community_id: i32) -> Result<usize, Error> {
-    use schema::community::dsl::*;
+    use crate::schema::community::dsl::*;
     diesel::delete(community.find(community_id))
       .execute(conn)
   }
 
   fn create(conn: &PgConnection, new_community: &CommunityForm) -> Result<Self, Error> {
-    use schema::community::dsl::*;
+    use crate::schema::community::dsl::*;
       insert_into(community)
         .values(new_community)
         .get_result::<Self>(conn)
   }
 
   fn update(conn: &PgConnection, community_id: i32, new_community: &CommunityForm) -> Result<Self, Error> {
-    use schema::community::dsl::*;
+    use crate::schema::community::dsl::*;
     diesel::update(community.find(community_id))
       .set(new_community)
       .get_result::<Self>(conn)
@@ -59,7 +61,7 @@ impl Crud<CommunityForm> for Community {
 
 impl Community {
   pub fn read_from_name(conn: &PgConnection, community_name: String) -> Result<Self, Error> {
-    use schema::community::dsl::*;
+    use crate::schema::community::dsl::*;
     community.filter(name.eq(community_name))
       .first::<Self>(conn)
   }
@@ -84,14 +86,14 @@ pub struct CommunityModeratorForm {
 
 impl Joinable<CommunityModeratorForm> for CommunityModerator {
   fn join(conn: &PgConnection, community_user_form: &CommunityModeratorForm) -> Result<Self, Error> {
-    use schema::community_moderator::dsl::*;
+    use crate::schema::community_moderator::dsl::*;
     insert_into(community_moderator)
       .values(community_user_form)
       .get_result::<Self>(conn)
   }
 
   fn leave(conn: &PgConnection, community_user_form: &CommunityModeratorForm) -> Result<usize, Error> {
-    use schema::community_moderator::dsl::*;
+    use crate::schema::community_moderator::dsl::*;
     diesel::delete(community_moderator
       .filter(community_id.eq(community_user_form.community_id))
       .filter(user_id.eq(community_user_form.user_id)))
@@ -99,6 +101,16 @@ impl Joinable<CommunityModeratorForm> for CommunityModerator {
   }
 }
 
+impl CommunityModerator {
+  pub fn delete_for_community(conn: &PgConnection, for_community_id: i32) -> Result<usize, Error> {
+    use crate::schema::community_moderator::dsl::*;
+    diesel::delete(
+      community_moderator
+      .filter(community_id.eq(for_community_id)))
+      .execute(conn)
+  }
+}
+
 #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
 #[belongs_to(Community)]
 #[table_name = "community_user_ban"]
@@ -118,14 +130,14 @@ pub struct CommunityUserBanForm {
 
 impl Bannable<CommunityUserBanForm> for CommunityUserBan {
   fn ban(conn: &PgConnection, community_user_ban_form: &CommunityUserBanForm) -> Result<Self, Error> {
-    use schema::community_user_ban::dsl::*;
+    use crate::schema::community_user_ban::dsl::*;
     insert_into(community_user_ban)
       .values(community_user_ban_form)
       .get_result::<Self>(conn)
   }
 
   fn unban(conn: &PgConnection, community_user_ban_form: &CommunityUserBanForm) -> Result<usize, Error> {
-    use schema::community_user_ban::dsl::*;
+    use crate::schema::community_user_ban::dsl::*;
     diesel::delete(community_user_ban
       .filter(community_id.eq(community_user_ban_form.community_id))
       .filter(user_id.eq(community_user_ban_form.user_id)))
@@ -152,13 +164,13 @@ pub struct CommunityFollowerForm {
 
 impl Followable<CommunityFollowerForm> for CommunityFollower {
   fn follow(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<Self, Error> {
-    use schema::community_follower::dsl::*;
+    use crate::schema::community_follower::dsl::*;
     insert_into(community_follower)
       .values(community_follower_form)
       .get_result::<Self>(conn)
   }
   fn ignore(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<usize, Error> {
-    use schema::community_follower::dsl::*;
+    use crate::schema::community_follower::dsl::*;
     diesel::delete(community_follower
       .filter(community_id.eq(&community_follower_form.community_id))
       .filter(user_id.eq(&community_follower_form.user_id)))
@@ -188,25 +200,25 @@ pub struct SiteForm {
 
 impl Crud<SiteForm> for Site {
   fn read(conn: &PgConnection, _site_id: i32) -> Result<Self, Error> {
-    use schema::site::dsl::*;
+    use crate::schema::site::dsl::*;
     site.first::<Self>(conn)
   }
 
   fn delete(conn: &PgConnection, site_id: i32) -> Result<usize, Error> {
-    use schema::site::dsl::*;
+    use crate::schema::site::dsl::*;
     diesel::delete(site.find(site_id))
       .execute(conn)
   }
 
   fn create(conn: &PgConnection, new_site: &SiteForm) -> Result<Self, Error> {
-    use schema::site::dsl::*;
+    use crate::schema::site::dsl::*;
       insert_into(site)
         .values(new_site)
         .get_result::<Self>(conn)
   }
 
   fn update(conn: &PgConnection, site_id: i32, new_site: &SiteForm) -> Result<Self, Error> {
-    use schema::site::dsl::*;
+    use crate::schema::site::dsl::*;
     diesel::update(site.find(site_id))
       .set(new_site)
       .get_result::<Self>(conn)
@@ -229,7 +241,8 @@ mod tests {
       email: None,
       admin: false,
       banned: false,
-      updated: None
+      updated: None,
+      show_nsfw: false,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -240,6 +253,7 @@ mod tests {
       title: "nada".to_owned(),
       description: None,
       category_id: 1,
+      nsfw: false,
       removed: None,
       deleted: None,
       updated: None,
@@ -254,6 +268,7 @@ mod tests {
       title: "nada".to_owned(),
       description: None,
       category_id: 1,
+      nsfw: false,
       removed: false,
       deleted: false,
       published: inserted_community.published,
index ec77cc8fe83533c5cfa3c5d7d05f5e22ea82e226..a12d6bf96bc184cfe7d04e4d753adee099e31769 100644 (file)
@@ -12,6 +12,7 @@ table! {
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
+    nsfw -> Bool,
     creator_name -> Varchar,
     category_name -> Varchar,
     number_of_subscribers -> BigInt,
@@ -57,18 +58,19 @@ table! {
 }
 
 table! {
-    site_view (id) {
-      id -> Int4,
-      name -> Varchar,
-      description -> Nullable<Text>,
-      creator_id -> Int4,
-      published -> Timestamp,
-      updated -> Nullable<Timestamp>,
-      creator_name -> Varchar,
-      number_of_users -> BigInt,
-      number_of_posts -> BigInt,
-      number_of_comments -> BigInt,
-    }
+  site_view (id) {
+    id -> Int4,
+    name -> Varchar,
+    description -> Nullable<Text>,
+    creator_id -> Int4,
+    published -> Timestamp,
+    updated -> Nullable<Timestamp>,
+    creator_name -> Varchar,
+    number_of_users -> BigInt,
+    number_of_posts -> BigInt,
+    number_of_comments -> BigInt,
+    number_of_communities -> BigInt,
+  }
 }
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
@@ -84,6 +86,7 @@ pub struct CommunityView {
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
+  pub nsfw: bool,
   pub creator_name: String,
   pub category_name: String,
   pub number_of_subscribers: i64,
@@ -112,30 +115,43 @@ impl CommunityView {
     query.first::<Self>(conn)
   }
 
-  pub fn list(conn: &PgConnection, 
-              from_user_id: Option<i32>, 
-              sort: SortType, 
-              page: Option<i64>,
-              limit: Option<i64>,
-              ) -> Result<Vec<Self>, Error> {
+  pub fn list(
+    conn: &PgConnection, 
+    sort: &SortType, 
+    from_user_id: Option<i32>, 
+    show_nsfw: bool,
+    search_term: Option<String>,
+    page: Option<i64>,
+    limit: Option<i64>,
+    ) -> Result<Vec<Self>, Error> {
     use super::community_view::community_view::dsl::*;
     let mut query = community_view.into_boxed();
 
     let (limit, offset) = limit_and_offset(page, limit);
 
+    if let Some(search_term) = search_term {
+      query = query.filter(name.ilike(fuzzy_search(&search_term)));
+    };
+
     // The view lets you pass a null user_id, if you're not logged in
     match sort {
-      SortType::Hot => query = query.order_by(hot_rank.desc()).filter(user_id.is_null()),
-      SortType::New => query = query.order_by(published.desc()).filter(user_id.is_null()),
-      SortType::TopAll => {
-        match from_user_id {
-          Some(from_user_id) => query = query.filter(user_id.eq(from_user_id)).order_by((subscribed.asc(), number_of_subscribers.desc())),
-          None => query = query.order_by(number_of_subscribers.desc()).filter(user_id.is_null())
+      SortType::Hot => query = query.order_by(hot_rank.desc())
+        .then_order_by(number_of_subscribers.desc())
+        .filter(user_id.is_null()),
+        SortType::New => query = query.order_by(published.desc()).filter(user_id.is_null()),
+        SortType::TopAll => {
+          match from_user_id {
+            Some(from_user_id) => query = query.filter(user_id.eq(from_user_id)).order_by((subscribed.asc(), number_of_subscribers.desc())),
+            None => query = query.order_by(number_of_subscribers.desc()).filter(user_id.is_null())
+          }
         }
-      }
       _ => ()
     };
 
+    if !show_nsfw {
+      query = query.filter(nsfw.eq(false));
+    };
+
     query
       .limit(limit)
       .offset(offset)
@@ -238,6 +254,7 @@ pub struct SiteView {
   pub number_of_users: i64,
   pub number_of_posts: i64,
   pub number_of_comments: i64,
+  pub number_of_communities: i64,
 }
 
 impl SiteView {
index c3587c476ef5a7099c557c86ffeb81e53dd67cbc..3de0abb461c0fb2cd4106ec494224fd74d8c51cd 100644 (file)
@@ -1,7 +1,7 @@
 use diesel::*;
 use diesel::dsl::*;
 use diesel::result::Error;
-use {Settings};
+use crate::{Settings};
 use serde::{Deserialize, Serialize};
 
 pub mod user;
@@ -67,7 +67,7 @@ pub enum SortType {
 
 #[derive(EnumString,ToString,Debug, Serialize, Deserialize)]
 pub enum SearchType {
-  Both, Comments, Posts
+  All, Comments, Posts, Communities, Users, Url
 }
 
 pub fn fuzzy_search(q: &str) -> String {
index 8b85e66318626c9415ca79cb65c50d7936330921..fec46aa18d6933da8d9c79aa7f11af6158ced987 100644 (file)
@@ -1,4 +1,4 @@
-use schema::{mod_remove_post, mod_lock_post, mod_remove_comment, mod_remove_community, mod_ban_from_community, mod_ban, mod_add_community, mod_add};
+use crate::schema::{mod_remove_post, mod_lock_post, mod_remove_comment, mod_remove_community, mod_ban_from_community, mod_ban, mod_add_community, mod_add};
 use super::*;
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
@@ -23,26 +23,26 @@ pub struct ModRemovePostForm {
 
 impl Crud<ModRemovePostForm> for ModRemovePost {
   fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
-    use schema::mod_remove_post::dsl::*;
+    use crate::schema::mod_remove_post::dsl::*;
     mod_remove_post.find(from_id)
       .first::<Self>(conn)
   }
 
   fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use schema::mod_remove_post::dsl::*;
+    use crate::schema::mod_remove_post::dsl::*;
     diesel::delete(mod_remove_post.find(from_id))
       .execute(conn)
   }
 
   fn create(conn: &PgConnection, form: &ModRemovePostForm) -> Result<Self, Error> {
-    use schema::mod_remove_post::dsl::*;
+    use crate::schema::mod_remove_post::dsl::*;
       insert_into(mod_remove_post)
         .values(form)
         .get_result::<Self>(conn)
   }
 
   fn update(conn: &PgConnection, from_id: i32, form: &ModRemovePostForm) -> Result<Self, Error> {
-    use schema::mod_remove_post::dsl::*;
+    use crate::schema::mod_remove_post::dsl::*;
     diesel::update(mod_remove_post.find(from_id))
       .set(form)
       .get_result::<Self>(conn)
@@ -71,26 +71,26 @@ pub struct ModLockPostForm {
 
 impl Crud<ModLockPostForm> for ModLockPost {
   fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
-    use schema::mod_lock_post::dsl::*;
+    use crate::schema::mod_lock_post::dsl::*;
     mod_lock_post.find(from_id)
       .first::<Self>(conn)
   }
 
   fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use schema::mod_lock_post::dsl::*;
+    use crate::schema::mod_lock_post::dsl::*;
     diesel::delete(mod_lock_post.find(from_id))
       .execute(conn)
   }
 
   fn create(conn: &PgConnection, form: &ModLockPostForm) -> Result<Self, Error> {
-    use schema::mod_lock_post::dsl::*;
+    use crate::schema::mod_lock_post::dsl::*;
       insert_into(mod_lock_post)
         .values(form)
         .get_result::<Self>(conn)
   }
 
   fn update(conn: &PgConnection, from_id: i32, form: &ModLockPostForm) -> Result<Self, Error> {
-    use schema::mod_lock_post::dsl::*;
+    use crate::schema::mod_lock_post::dsl::*;
     diesel::update(mod_lock_post.find(from_id))
       .set(form)
       .get_result::<Self>(conn)
@@ -119,26 +119,26 @@ pub struct ModRemoveCommentForm {
 
 impl Crud<ModRemoveCommentForm> for ModRemoveComment {
   fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
-    use schema::mod_remove_comment::dsl::*;
+    use crate::schema::mod_remove_comment::dsl::*;
     mod_remove_comment.find(from_id)
       .first::<Self>(conn)
   }
 
   fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use schema::mod_remove_comment::dsl::*;
+    use crate::schema::mod_remove_comment::dsl::*;
     diesel::delete(mod_remove_comment.find(from_id))
       .execute(conn)
   }
 
   fn create(conn: &PgConnection, form: &ModRemoveCommentForm) -> Result<Self, Error> {
-    use schema::mod_remove_comment::dsl::*;
+    use crate::schema::mod_remove_comment::dsl::*;
       insert_into(mod_remove_comment)
         .values(form)
         .get_result::<Self>(conn)
   }
 
   fn update(conn: &PgConnection, from_id: i32, form: &ModRemoveCommentForm) -> Result<Self, Error> {
-    use schema::mod_remove_comment::dsl::*;
+    use crate::schema::mod_remove_comment::dsl::*;
     diesel::update(mod_remove_comment.find(from_id))
       .set(form)
       .get_result::<Self>(conn)
@@ -169,26 +169,26 @@ pub struct ModRemoveCommunityForm {
 
 impl Crud<ModRemoveCommunityForm> for ModRemoveCommunity {
   fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
-    use schema::mod_remove_community::dsl::*;
+    use crate::schema::mod_remove_community::dsl::*;
     mod_remove_community.find(from_id)
       .first::<Self>(conn)
   }
 
   fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use schema::mod_remove_community::dsl::*;
+    use crate::schema::mod_remove_community::dsl::*;
     diesel::delete(mod_remove_community.find(from_id))
       .execute(conn)
   }
 
   fn create(conn: &PgConnection, form: &ModRemoveCommunityForm) -> Result<Self, Error> {
-    use schema::mod_remove_community::dsl::*;
+    use crate::schema::mod_remove_community::dsl::*;
       insert_into(mod_remove_community)
         .values(form)
         .get_result::<Self>(conn)
   }
 
   fn update(conn: &PgConnection, from_id: i32, form: &ModRemoveCommunityForm) -> Result<Self, Error> {
-    use schema::mod_remove_community::dsl::*;
+    use crate::schema::mod_remove_community::dsl::*;
     diesel::update(mod_remove_community.find(from_id))
       .set(form)
       .get_result::<Self>(conn)
@@ -221,26 +221,26 @@ pub struct ModBanFromCommunityForm {
 
 impl Crud<ModBanFromCommunityForm> for ModBanFromCommunity {
   fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
-    use schema::mod_ban_from_community::dsl::*;
+    use crate::schema::mod_ban_from_community::dsl::*;
     mod_ban_from_community.find(from_id)
       .first::<Self>(conn)
   }
 
   fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use schema::mod_ban_from_community::dsl::*;
+    use crate::schema::mod_ban_from_community::dsl::*;
     diesel::delete(mod_ban_from_community.find(from_id))
       .execute(conn)
   }
 
   fn create(conn: &PgConnection, form: &ModBanFromCommunityForm) -> Result<Self, Error> {
-    use schema::mod_ban_from_community::dsl::*;
+    use crate::schema::mod_ban_from_community::dsl::*;
       insert_into(mod_ban_from_community)
         .values(form)
         .get_result::<Self>(conn)
   }
 
   fn update(conn: &PgConnection, from_id: i32, form: &ModBanFromCommunityForm) -> Result<Self, Error> {
-    use schema::mod_ban_from_community::dsl::*;
+    use crate::schema::mod_ban_from_community::dsl::*;
     diesel::update(mod_ban_from_community.find(from_id))
       .set(form)
       .get_result::<Self>(conn)
@@ -272,26 +272,26 @@ pub struct ModBanForm {
 
 impl Crud<ModBanForm> for ModBan {
   fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
-    use schema::mod_ban::dsl::*;
+    use crate::schema::mod_ban::dsl::*;
     mod_ban.find(from_id)
       .first::<Self>(conn)
   }
 
   fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use schema::mod_ban::dsl::*;
+    use crate::schema::mod_ban::dsl::*;
     diesel::delete(mod_ban.find(from_id))
       .execute(conn)
   }
 
   fn create(conn: &PgConnection, form: &ModBanForm) -> Result<Self, Error> {
-    use schema::mod_ban::dsl::*;
+    use crate::schema::mod_ban::dsl::*;
       insert_into(mod_ban)
         .values(form)
         .get_result::<Self>(conn)
   }
 
   fn update(conn: &PgConnection, from_id: i32, form: &ModBanForm) -> Result<Self, Error> {
-    use schema::mod_ban::dsl::*;
+    use crate::schema::mod_ban::dsl::*;
     diesel::update(mod_ban.find(from_id))
       .set(form)
       .get_result::<Self>(conn)
@@ -320,26 +320,26 @@ pub struct ModAddCommunityForm {
 
 impl Crud<ModAddCommunityForm> for ModAddCommunity {
   fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
-    use schema::mod_add_community::dsl::*;
+    use crate::schema::mod_add_community::dsl::*;
     mod_add_community.find(from_id)
       .first::<Self>(conn)
   }
 
   fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use schema::mod_add_community::dsl::*;
+    use crate::schema::mod_add_community::dsl::*;
     diesel::delete(mod_add_community.find(from_id))
       .execute(conn)
   }
 
   fn create(conn: &PgConnection, form: &ModAddCommunityForm) -> Result<Self, Error> {
-    use schema::mod_add_community::dsl::*;
+    use crate::schema::mod_add_community::dsl::*;
       insert_into(mod_add_community)
         .values(form)
         .get_result::<Self>(conn)
   }
 
   fn update(conn: &PgConnection, from_id: i32, form: &ModAddCommunityForm) -> Result<Self, Error> {
-    use schema::mod_add_community::dsl::*;
+    use crate::schema::mod_add_community::dsl::*;
     diesel::update(mod_add_community.find(from_id))
       .set(form)
       .get_result::<Self>(conn)
@@ -366,26 +366,26 @@ pub struct ModAddForm {
 
 impl Crud<ModAddForm> for ModAdd {
   fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
-    use schema::mod_add::dsl::*;
+    use crate::schema::mod_add::dsl::*;
     mod_add.find(from_id)
       .first::<Self>(conn)
   }
 
   fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use schema::mod_add::dsl::*;
+    use crate::schema::mod_add::dsl::*;
     diesel::delete(mod_add.find(from_id))
       .execute(conn)
   }
 
   fn create(conn: &PgConnection, form: &ModAddForm) -> Result<Self, Error> {
-    use schema::mod_add::dsl::*;
+    use crate::schema::mod_add::dsl::*;
       insert_into(mod_add)
         .values(form)
         .get_result::<Self>(conn)
   }
 
   fn update(conn: &PgConnection, from_id: i32, form: &ModAddForm) -> Result<Self, Error> {
-    use schema::mod_add::dsl::*;
+    use crate::schema::mod_add::dsl::*;
     diesel::update(mod_add.find(from_id))
       .set(form)
       .get_result::<Self>(conn)
@@ -412,7 +412,8 @@ mod tests {
       email: None,
       admin: false,
       banned: false,
-      updated: None
+      updated: None,
+      show_nsfw: false,
     };
 
     let inserted_mod = User_::create(&conn, &new_mod).unwrap();
@@ -425,7 +426,8 @@ mod tests {
       email: None,
       admin: false,
       banned: false,
-      updated: None
+      updated: None,
+      show_nsfw: false,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -438,7 +440,8 @@ mod tests {
       creator_id: inserted_user.id,
       removed: None,
       deleted: None,
-      updated: None
+      updated: None,
+      nsfw: false,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -452,7 +455,8 @@ mod tests {
       removed: None,
       deleted: None,
       locked: None,
-      updated: None
+      updated: None,
+      nsfw: false,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
index f030227171217dbeeb657e67b1c475b1ae2cb5b5..12ea10818150db46d26bf376b08e8210e34cad78 100644 (file)
@@ -1,4 +1,4 @@
-use schema::{post, post_like, post_saved, post_read};
+use crate::schema::{post, post_like, post_saved, post_read};
 use super::*;
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
@@ -15,6 +15,7 @@ pub struct Post {
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
+  pub nsfw: bool,
 }
 
 #[derive(Insertable, AsChangeset, Clone)]
@@ -29,30 +30,31 @@ pub struct PostForm {
   pub locked: Option<bool>,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: Option<bool>,
+  pub nsfw: bool,
 }
 
 impl Crud<PostForm> for Post {
   fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
-    use schema::post::dsl::*;
+    use crate::schema::post::dsl::*;
     post.find(post_id)
       .first::<Self>(conn)
   }
 
   fn delete(conn: &PgConnection, post_id: i32) -> Result<usize, Error> {
-    use schema::post::dsl::*;
+    use crate::schema::post::dsl::*;
     diesel::delete(post.find(post_id))
       .execute(conn)
   }
 
   fn create(conn: &PgConnection, new_post: &PostForm) -> Result<Self, Error> {
-    use schema::post::dsl::*;
+    use crate::schema::post::dsl::*;
       insert_into(post)
         .values(new_post)
         .get_result::<Self>(conn)
   }
 
   fn update(conn: &PgConnection, post_id: i32, new_post: &PostForm) -> Result<Self, Error> {
-    use schema::post::dsl::*;
+    use crate::schema::post::dsl::*;
     diesel::update(post.find(post_id))
       .set(new_post)
       .get_result::<Self>(conn)
@@ -80,19 +82,19 @@ pub struct PostLikeForm {
 
 impl Likeable <PostLikeForm> for PostLike {
   fn read(conn: &PgConnection, post_id_from: i32) -> Result<Vec<Self>, Error> {
-    use schema::post_like::dsl::*;
+    use crate::schema::post_like::dsl::*;
     post_like
       .filter(post_id.eq(post_id_from))
       .load::<Self>(conn) 
   }
   fn like(conn: &PgConnection, post_like_form: &PostLikeForm) -> Result<Self, Error> {
-    use schema::post_like::dsl::*;
+    use crate::schema::post_like::dsl::*;
     insert_into(post_like)
       .values(post_like_form)
       .get_result::<Self>(conn)
   }
   fn remove(conn: &PgConnection, post_like_form: &PostLikeForm) -> Result<usize, Error> {
-    use schema::post_like::dsl::*;
+    use crate::schema::post_like::dsl::*;
     diesel::delete(post_like
       .filter(post_id.eq(post_like_form.post_id))
       .filter(user_id.eq(post_like_form.user_id)))
@@ -119,13 +121,13 @@ pub struct PostSavedForm {
 
 impl Saveable <PostSavedForm> for PostSaved {
   fn save(conn: &PgConnection, post_saved_form: &PostSavedForm) -> Result<Self, Error> {
-    use schema::post_saved::dsl::*;
+    use crate::schema::post_saved::dsl::*;
     insert_into(post_saved)
       .values(post_saved_form)
       .get_result::<Self>(conn)
   }
   fn unsave(conn: &PgConnection, post_saved_form: &PostSavedForm) -> Result<usize, Error> {
-    use schema::post_saved::dsl::*;
+    use crate::schema::post_saved::dsl::*;
     diesel::delete(post_saved
       .filter(post_id.eq(post_saved_form.post_id))
       .filter(user_id.eq(post_saved_form.user_id)))
@@ -152,13 +154,13 @@ pub struct PostReadForm {
 
 impl Readable <PostReadForm> for PostRead {
   fn mark_as_read(conn: &PgConnection, post_read_form: &PostReadForm) -> Result<Self, Error> {
-    use schema::post_read::dsl::*;
+    use crate::schema::post_read::dsl::*;
     insert_into(post_read)
       .values(post_read_form)
       .get_result::<Self>(conn)
   }
   fn mark_as_unread(conn: &PgConnection, post_read_form: &PostReadForm) -> Result<usize, Error> {
-    use schema::post_read::dsl::*;
+    use crate::schema::post_read::dsl::*;
     diesel::delete(post_read
       .filter(post_id.eq(post_read_form.post_id))
       .filter(user_id.eq(post_read_form.user_id)))
@@ -183,7 +185,8 @@ mod tests {
       email: None,
       admin: false,
       banned: false,
-      updated: None
+      updated: None,
+      show_nsfw: false,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -196,7 +199,8 @@ mod tests {
       creator_id: inserted_user.id,
       removed: None,
       deleted: None,
-      updated: None
+      updated: None,
+      nsfw: false,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -210,6 +214,7 @@ mod tests {
       removed: None,
       deleted: None,
       locked: None,
+      nsfw: false,
       updated: None
     };
 
@@ -225,6 +230,7 @@ mod tests {
       published: inserted_post.published,
       removed: false,
       locked: false,
+      nsfw: false,
       deleted: false,
       updated: None
     };
index bfe730a2b3a9913324d0f5825fef6dfc8cba377b..c9d8cff7b47fb1405b80cb68e56fd4fa8b82c58f 100644 (file)
@@ -19,10 +19,12 @@ table! {
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
+    nsfw -> Bool,
     creator_name -> Varchar,
     community_name -> Varchar,
     community_removed -> Bool,
     community_deleted -> Bool,
+    community_nsfw -> Bool,
     number_of_comments -> BigInt,
     score -> BigInt,
     upvotes -> BigInt,
@@ -51,10 +53,12 @@ pub struct PostView {
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
+  pub nsfw: bool,
   pub creator_name: String,
   pub community_name: String,
   pub community_removed: bool,
   pub community_deleted: bool,
+  pub community_nsfw: bool,
   pub number_of_comments: i64,
   pub score: i64,
   pub upvotes: i64,
@@ -68,18 +72,21 @@ pub struct PostView {
 }
 
 impl PostView {
-  pub fn list(conn: &PgConnection, 
-              type_: PostListingType, 
-              sort: &SortType, 
-              for_community_id: Option<i32>, 
-              for_creator_id: Option<i32>, 
-              search_term: Option<String>,
-              my_user_id: Option<i32>, 
-              saved_only: bool,
-              unread_only: bool,
-              page: Option<i64>,
-              limit: Option<i64>,
-              ) -> Result<Vec<Self>, Error> {
+  pub fn list(
+    conn: &PgConnection, 
+    type_: PostListingType, 
+    sort: &SortType, 
+    for_community_id: Option<i32>, 
+    for_creator_id: Option<i32>, 
+    search_term: Option<String>,
+    url_search: Option<String>,
+    my_user_id: Option<i32>, 
+    show_nsfw: bool,
+    saved_only: bool,
+    unread_only: bool,
+    page: Option<i64>,
+    limit: Option<i64>,
+    ) -> Result<Vec<Self>, Error> {
     use super::post_view::post_view::dsl::*;
 
     let (limit, offset) = limit_and_offset(page, limit);
@@ -98,6 +105,10 @@ impl PostView {
       query = query.filter(name.ilike(fuzzy_search(&search_term)));
     };
 
+    if let Some(url_search) = url_search {
+      query = query.filter(url.eq(url_search));
+    };
+
     // TODO these are wrong, bc they'll only show saved for your logged in user, not theirs
     if saved_only {
       query = query.filter(saved.eq(true));
@@ -121,8 +132,15 @@ impl PostView {
       query = query.filter(user_id.is_null());
     }
 
+    if !show_nsfw {
+      query = query
+        .filter(nsfw.eq(false))
+        .filter(community_nsfw.eq(false));
+    };
+
     query = match sort {
-      SortType::Hot => query.order_by(hot_rank.desc()),
+      SortType::Hot => query.order_by(hot_rank.desc())
+        .then_order_by(published.desc()),
       SortType::New => query.order_by(published.desc()),
       SortType::TopAll => query.order_by(score.desc()),
       SortType::TopYear => query
@@ -195,6 +213,7 @@ mod tests {
       updated: None,
       admin: false,
       banned: false,
+      show_nsfw: false,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -207,7 +226,8 @@ mod tests {
       category_id: 1,
       removed: None,
       deleted: None,
-      updated: None
+      updated: None,
+      nsfw: false,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -221,7 +241,8 @@ mod tests {
       removed: None,
       deleted: None,
       locked: None,
-      updated: None
+      updated: None,
+      nsfw: false,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
@@ -265,6 +286,7 @@ mod tests {
       community_name: community_name.to_owned(),
       community_removed: false,
       community_deleted: false,
+      community_nsfw: false,
       number_of_comments: 0,
       score: 1,
       upvotes: 1,
@@ -275,6 +297,7 @@ mod tests {
       subscribed: None,
       read: None,
       saved: None,
+      nsfw: false,
     };
 
     let expected_post_listing_with_user = PostView {
@@ -293,6 +316,7 @@ mod tests {
       community_name: community_name.to_owned(),
       community_removed: false,
       community_deleted: false,
+      community_nsfw: false,
       number_of_comments: 0,
       score: 1,
       upvotes: 1,
@@ -303,30 +327,38 @@ mod tests {
       subscribed: None,
       read: None,
       saved: None,
+      nsfw: false,
     };
 
 
-    let read_post_listings_with_user = PostView::list(&conn, 
-                                                      PostListingType::Community, 
-                                                      &SortType::New, Some(inserted_community.id), 
-                                                      None, 
-                                                      None,
-                                                      Some(inserted_user.id), 
-                                                      false, 
-                                                      false, 
-                                                      None, 
-                                                      None).unwrap();
-    let read_post_listings_no_user = PostView::list(&conn, 
-                                                    PostListingType::Community, 
-                                                    &SortType::New, 
-                                                    Some(inserted_community.id), 
-                                                    None, 
-                                                    None, 
-                                                    None,
-                                                    false, 
-                                                    false, 
-                                                    None, 
-                                                    None).unwrap();
+    let read_post_listings_with_user = PostView::list(
+      &conn, 
+      PostListingType::Community, 
+      &SortType::New, 
+      Some(inserted_community.id), 
+      None, 
+      None,
+      None,
+      Some(inserted_user.id), 
+      false,
+      false, 
+      false, 
+      None, 
+      None).unwrap();
+    let read_post_listings_no_user = PostView::list(
+      &conn, 
+      PostListingType::Community, 
+      &SortType::New, 
+      Some(inserted_community.id), 
+      None, 
+      None, 
+      None,
+      None,
+      false,
+      false, 
+      false, 
+      None, 
+      None).unwrap();
     let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap();
     let read_post_listing_with_user = PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap();
 
index a4a7be43f58c1036a855d6ddbc70d2d3515151fa..b794524cb0ca49334e8c44f44d5077db7d9810cb 100644 (file)
@@ -1,7 +1,7 @@
-use schema::user_;
-use schema::user_::dsl::*;
+use crate::schema::user_;
+use crate::schema::user_::dsl::*;
 use super::*;
-use {Settings, is_email_regex};
+use crate::{Settings, is_email_regex};
 use jsonwebtoken::{encode, decode, Header, Validation, TokenData};
 use bcrypt::{DEFAULT_COST, hash};
 
@@ -18,7 +18,8 @@ pub struct User_ {
   pub admin: bool,
   pub banned: bool,
   pub published: chrono::NaiveDateTime,
-  pub updated: Option<chrono::NaiveDateTime>
+  pub updated: Option<chrono::NaiveDateTime>,
+  pub show_nsfw: bool,
 }
 
 #[derive(Insertable, AsChangeset, Clone)]
@@ -31,12 +32,13 @@ pub struct UserForm {
     pub admin: bool,
     pub banned: bool,
     pub email: Option<String>,
-    pub updated: Option<chrono::NaiveDateTime>
+    pub updated: Option<chrono::NaiveDateTime>,
+    pub show_nsfw: bool,
 }
 
 impl Crud<UserForm> for User_ {
   fn read(conn: &PgConnection, user_id: i32) -> Result<Self, Error> {
-    use schema::user_::dsl::*;
+    use crate::schema::user_::dsl::*;
     user_.find(user_id)
       .first::<Self>(conn)
   }
@@ -77,6 +79,7 @@ pub struct Claims {
   pub id: i32,
   pub username: String,
   pub iss: String,
+  pub show_nsfw: bool,
 }
 
 impl Claims {
@@ -96,6 +99,7 @@ impl User_ {
       id: self.id,
       username: self.name.to_owned(),
       iss: self.fedi_name.to_owned(),
+      show_nsfw: self.show_nsfw,
     };
     encode(&Header::default(), &my_claims, Settings::get().jwt_secret.as_ref()).unwrap()
   }
@@ -133,7 +137,8 @@ mod tests {
       email: None,
       admin: false,
       banned: false,
-      updated: None
+      updated: None,
+      show_nsfw: false,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -149,7 +154,8 @@ mod tests {
       admin: false,
       banned: false,
       published: inserted_user.published,
-      updated: None
+      updated: None,
+      show_nsfw: false,
     };
     
     let read_user = User_::read(&conn, inserted_user.id).unwrap();
index 3d78ae1a0aa47a84c9e35c85895938f5330c70cf..897ee23ada1bc5e31eefc5f3ebd316be832866ee 100644 (file)
@@ -31,6 +31,49 @@ pub struct UserView {
 }
 
 impl UserView {
+
+  pub fn list(conn: &PgConnection, 
+              sort: &SortType, 
+              search_term: Option<String>,
+              page: Option<i64>,
+              limit: Option<i64>,
+              ) -> Result<Vec<Self>, Error> {
+    use super::user_view::user_view::dsl::*;
+
+    let (limit, offset) = limit_and_offset(page, limit);
+
+    let mut query = user_view.into_boxed();
+
+    if let Some(search_term) = search_term {
+      query = query.filter(name.ilike(fuzzy_search(&search_term)));
+    };
+
+    query = match sort {
+      SortType::Hot => query.order_by(comment_score.desc())
+        .then_order_by(published.desc()),
+      SortType::New => query.order_by(published.desc()),
+      SortType::TopAll => query.order_by(comment_score.desc()),
+      SortType::TopYear => query
+        .filter(published.gt(now - 1.years()))
+        .order_by(comment_score.desc()),
+        SortType::TopMonth => query
+          .filter(published.gt(now - 1.months()))
+          .order_by(comment_score.desc()),
+          SortType::TopWeek => query
+            .filter(published.gt(now - 1.weeks()))
+            .order_by(comment_score.desc()),
+            SortType::TopDay => query
+              .filter(published.gt(now - 1.days()))
+              .order_by(comment_score.desc())
+    };
+
+    query = query
+      .limit(limit)
+      .offset(offset);
+
+    query.load::<Self>(conn) 
+  }
+
   pub fn read(conn: &PgConnection, from_user_id: i32) -> Result<Self, Error> {
     use super::user_view::user_view::dsl::*;
 
index 7dd090d6749df7c7af99d844b933f449c6277c31..04076771b5e69725c7b5eb0cae845dcbeb7efd31 100644 (file)
@@ -1,3 +1,4 @@
+#![recursion_limit = "512"]
 #[macro_use] pub extern crate strum_macros;
 #[macro_use] pub extern crate lazy_static;
 #[macro_use] pub extern crate failure;
@@ -72,7 +73,7 @@ pub fn has_slurs(test: &str) -> bool {
 
 #[cfg(test)]
 mod tests {
-  use {Settings, is_email_regex, remove_slurs, has_slurs};
+  use crate::{Settings, is_email_regex, remove_slurs, has_slurs};
   #[test]
   fn test_api() {
     assert_eq!(Settings::get().api_endpoint(), "rrr/api/v1");
index 48074316612c64170b779570a1478b9641803ca8..5e9a1dae8831ad8dc8f558d8543fbde0e2293489 100644 (file)
@@ -1,13 +1,15 @@
 extern crate lemmy_server;
-#[macro_use] extern crate diesel_migrations;
+#[macro_use]
+extern crate diesel_migrations;
 
-use std::time::{Instant, Duration};
-use std::env;
-use lemmy_server::actix::*;
-use lemmy_server::actix_web::server::HttpServer;
-use lemmy_server::actix_web::{ws, App, Error, HttpRequest, HttpResponse, fs::NamedFile, fs};
-use lemmy_server::websocket::server::*;
+use actix::prelude::*;
+use actix_files::NamedFile;
+use actix_web::*;
+use actix_web_actors::ws;
 use lemmy_server::db::establish_connection;
+use lemmy_server::websocket::server::*;
+use std::env;
+use std::time::{Duration, Instant};
 
 embed_migrations!();
 
@@ -16,41 +18,43 @@ const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
 /// How long before lack of client response causes a timeout
 const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
 
-/// This is our websocket route state, this state is shared with all route
-/// instances via `HttpContext::state()`
-struct WsChatSessionState {
-  addr: Addr<ChatServer>,
-}
-
 /// Entry point for our route
-fn chat_route(req: &HttpRequest<WsChatSessionState>) -> Result<HttpResponse, Error> {
+fn chat_route(
+  req: HttpRequest,
+  stream: web::Payload,
+  chat_server: web::Data<Addr<ChatServer>>,
+  ) -> Result<HttpResponse, Error> {
   ws::start(
-    req,
     WSSession {
+      cs_addr: chat_server.get_ref().to_owned(),
       id: 0,
       hb: Instant::now(),
-      ip: req.connection_info()
+      ip: req
+        .connection_info()
         .remote()
         .unwrap_or("127.0.0.1:12345")
         .split(":")
         .next()
         .unwrap_or("127.0.0.1")
-        .to_string()
+        .to_string(),
     },
+    &req,
+    stream,
     )
 }
 
 struct WSSession {
+  cs_addr: Addr<ChatServer>,
   /// unique session id
   id: usize,
   ip: String,
   /// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
   /// otherwise we drop connection.
-  hb: Instant
+  hb: Instant,
 }
 
 impl Actor for WSSession {
-  type Context = ws::WebsocketContext<Self, WsChatSessionState>;
+  type Context = ws::WebsocketContext<Self>;
 
   /// Method is called on actor start.
   /// We register ws session with ChatServer
@@ -61,11 +65,9 @@ impl Actor for WSSession {
     // register self in chat server. `AsyncContext::wait` register
     // future within context, but context waits until this future resolves
     // before processing any other events.
-    // HttpContext::state() is instance of WsChatSessionState, state is shared
     // across all routes within application
     let addr = ctx.address();
-    ctx.state()
-      .addr
+    self.cs_addr
       .send(Connect {
         addr: addr.recipient(),
         ip: self.ip.to_owned(),
@@ -82,9 +84,9 @@ impl Actor for WSSession {
     .wait(ctx);
   }
 
-  fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
+  fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
     // notify chat server
-    ctx.state().addr.do_send(Disconnect { 
+    self.cs_addr.do_send(Disconnect {
       id: self.id,
       ip: self.ip.to_owned(),
     });
@@ -118,9 +120,8 @@ impl StreamHandler<ws::Message, ws::ProtocolError> for WSSession {
       ws::Message::Text(text) => {
         let m = text.trim().to_owned();
         println!("WEBSOCKET MESSAGE: {:?} from id: {}", &m, self.id);
-        
-        ctx.state()
-          .addr
+
+        self.cs_addr
           .send(StandardMessage {
             id: self.id,
             msg: m,
@@ -140,7 +141,8 @@ impl StreamHandler<ws::Message, ws::ProtocolError> for WSSession {
       ws::Message::Binary(_bin) => println!("Unexpected binary"),
       ws::Message::Close(_) => {
         ctx.stop();
-      },
+      }
+      _ => {}
     }
   }
 }
@@ -149,7 +151,7 @@ impl WSSession {
   /// helper method that sends ping to client every second.
   ///
   /// also this method checks heartbeats from client
-  fn hb(&self, ctx: &mut ws::WebsocketContext<Self, WsChatSessionState>) {
+  fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
     ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
       // check client heartbeats
       if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
@@ -157,9 +159,10 @@ impl WSSession {
         println!("Websocket Client heartbeat failed, disconnecting!");
 
         // notify chat server
-        ctx.state()
-          .addr
-          .do_send(Disconnect { id: act.id, ip: act.ip.to_owned() });
+        act.cs_addr.do_send(Disconnect {
+          id: act.id,
+          ip: act.ip.to_owned(),
+        });
 
         // stop actor
         ctx.stop();
@@ -182,34 +185,26 @@ fn main() {
   embedded_migrations::run(&conn).unwrap();
 
   // Start chat server actor in separate thread
-  let server = Arbiter::start(|_| ChatServer::default());
-
+  let server = ChatServer::default().start();
   // Create Http server with websocket support
   HttpServer::new(move || {
-    // Websocket sessions state
-    let state = WsChatSessionState {
-      addr: server.clone(),
-    };
-
-    App::with_state(state)
-      // .resource("/api/v1/rest", |r| r.method(http::Method::POST).f(|_| {})
-      .resource("/api/v1/ws", |r| r.route().f(chat_route))
+    App::new()
+      .data(server.clone())
+      .service(web::resource("/api/v1/ws").to(chat_route))
+      //            .service(web::resource("/api/v1/rest").route(web::post().to(||{})))
+      .service(web::resource("/").to(index))
       // static resources
-      .resource("/", |r| r.route().f(index))
-      .handler(
-        "/static",
-        fs::StaticFiles::new(front_end_dir()).unwrap()
-      )
-      .finish()
-  }).bind("0.0.0.0:8536")
-  .unwrap()
+      .service(actix_files::Files::new("/static", front_end_dir()))
+  })
+  .bind("0.0.0.0:8536")
+    .unwrap()
     .start();
 
   println!("Started http server: 0.0.0.0:8536");
   let _ = sys.run();
 }
 
-fn index(_req: &HttpRequest<WsChatSessionState>) -> Result<NamedFile, actix_web::error::Error> {
+fn index() -> Result<NamedFile, actix_web::error::Error> {
   Ok(NamedFile::open(front_end_dir() + "/index.html")?)
 }
 
index 27bc3f941c901f8b44c431597a052441cd264559..635c8c467bbb5f1451a38203320c47f0a632c9bb 100644 (file)
@@ -52,6 +52,7 @@ table! {
         published -> Timestamp,
         updated -> Nullable<Timestamp>,
         deleted -> Bool,
+        nsfw -> Bool,
     }
 }
 
@@ -185,6 +186,7 @@ table! {
         published -> Timestamp,
         updated -> Nullable<Timestamp>,
         deleted -> Bool,
+        nsfw -> Bool,
     }
 }
 
@@ -240,6 +242,7 @@ table! {
         banned -> Bool,
         published -> Timestamp,
         updated -> Nullable<Timestamp>,
+        show_nsfw -> Bool,
     }
 }
 
index abdf9ea9bb1b6bb9aa8cdc74e92d273a622b89c3..b4cbce3af8c49b0aed296356cd3395d15f9491c4 100644 (file)
@@ -11,12 +11,12 @@ use std::str::FromStr;
 use failure::Error;
 use std::time::{SystemTime};
 
-use api::*;
-use api::user::*;
-use api::community::*;
-use api::post::*;
-use api::comment::*;
-use api::site::*;
+use crate::api::*;
+use crate::api::user::*;
+use crate::api::community::*;
+use crate::api::post::*;
+use crate::api::comment::*;
+use crate::api::site::*;
 
 const RATE_LIMIT_MESSAGES: i32 = 30;
 const RATE_LIMIT_PER_SECOND: i32 = 60;
@@ -118,7 +118,7 @@ impl ChatServer {
 
   fn join_room(&mut self, room_id: i32, id: usize) {
     // remove session from all rooms
-    for (_n, mut sessions) in &mut self.rooms {
+    for (_n, sessions) in &mut self.rooms {
       sessions.remove(&id);
     }
 
@@ -131,20 +131,23 @@ impl ChatServer {
   }
 
   fn send_community_message(&self, community_id: &i32, message: &str, skip_id: usize) -> Result<(), Error> {
-    use db::*;
-    use db::post_view::*;
+    use crate::db::*;
+    use crate::db::post_view::*;
     let conn = establish_connection();
-    let posts = PostView::list(&conn,
-                               PostListingType::Community, 
-                               &SortType::New, 
-                               Some(*community_id), 
-                               None,
-                               None, 
-                               None,
-                               false,
-                               false,
-                               None,
-                               Some(9999))?;
+    let posts = PostView::list(
+      &conn,
+      PostListingType::Community, 
+      &SortType::New, 
+      Some(*community_id), 
+      None,
+      None, 
+      None,
+      None,
+      false,
+      false,
+      false,
+      None,
+      Some(9999))?;
     for post in posts {
       self.send_room_message(&post.id, message, skip_id);
     }
@@ -303,6 +306,11 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
       let res = Oper::new(user_operation, get_user_details).perform()?;
       Ok(serde_json::to_string(&res)?)
     },
+    UserOperation::SaveUserSettings => {
+      let save_user_settings: SaveUserSettings = serde_json::from_str(data)?;
+      let res = Oper::new(user_operation, save_user_settings).perform()?;
+      Ok(serde_json::to_string(&res)?)
+    },
     UserOperation::AddAdmin => {
       let add_admin: AddAdmin = serde_json::from_str(data)?;
       let res = Oper::new(user_operation, add_admin).perform()?;
@@ -482,5 +490,15 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
       let res = Oper::new(user_operation, search).perform()?;
       Ok(serde_json::to_string(&res)?)
     },
+    UserOperation::TransferCommunity => {
+      let transfer_community: TransferCommunity = serde_json::from_str(data)?;
+      let res = Oper::new(user_operation, transfer_community).perform()?;
+      Ok(serde_json::to_string(&res)?)
+    },
+    UserOperation::TransferSite => {
+      let transfer_site: TransferSite = serde_json::from_str(data)?;
+      let res = Oper::new(user_operation, transfer_site).perform()?;
+      Ok(serde_json::to_string(&res)?)
+    },
   }
 }
diff --git a/server/stack.dev.yaml b/server/stack.dev.yaml
deleted file mode 100644 (file)
index 7c6905b..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: postgres
-data:
-  POSTGRES_PASSWORD: rrr
-  POSTGRES_USER: rrr
-  POSTGRES_DB: rrr
-  PGDATA: /var/lib/postgresql/data/pgdata
-  DATABASE_URL: postgres://rrr:rrr@postgres:5432/rrr
----
-apiVersion: v1
-kind: PersistentVolumeClaim
-metadata:
-  name: postgres
-spec:
-  accessModes:
-    - ReadWriteOnce
-  resources:
-    requests:
-      storage: 5Gi
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: postgres
-spec:
-  selector:
-    matchLabels:
-      app: postgres
-  template:
-    metadata:
-      labels:
-        app: postgres
-    spec:
-      containers:
-        - name: postgres
-          image: postgres:11.2-alpine
-          resources:
-            limits:
-              memory: 256Mi
-              cpu: 512m
-          ports:
-            - containerPort: 5432
-          envFrom:
-            - configMapRef:
-                name: postgres
-          volumeMounts:
-            - name: postgres
-              mountPath: /var/lib/postgresql/data
-      volumes:
-        - name: postgres
-          persistentVolumeClaim:
-            claimName: postgres
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: postgres
-spec:
-  selector:
-    app: postgres
-  ports:
-    - port: 5432
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: lemmy-server--dev
-data:
-  LEMMY_FRONT_END_DIR: /opt/lemmy/ui--dev/dist # not actually used here, polyfill for monolith
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: lemmy-server--dev
-spec:
-  selector:
-    matchLabels:
-      app: lemmy-server--dev
-  template:
-    metadata:
-      labels:
-        app: lemmy-server--dev
-    spec:
-      containers:
-        - name: lemmy-server--dev
-          image: registry.gitlab.com/pojntfx/lemmy/server.dev
-          envFrom:
-            - configMapRef:
-                name: postgres
-            - configMapRef:
-                name: lemmy-server--dev
-          resources:
-            limits:
-              memory: 512Mi
-              cpu: 512m
-          ports:
-            - containerPort: 8536
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: lemmy-server--dev
-spec:
-  type: NodePort
-  selector:
-    app: lemmy-server--dev
-  ports:
-    - port: 8536
-      nodePort: 30001
diff --git a/server/stack.prod.yaml b/server/stack.prod.yaml
deleted file mode 100644 (file)
index d221de1..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: postgres
-data:
-  POSTGRES_PASSWORD: rrr
-  POSTGRES_USER: rrr
-  POSTGRES_DB: rrr
-  PGDATA: /var/lib/postgresql/data/pgdata
-  DATABASE_URL: postgres://rrr:rrr@postgres:5432/rrr
----
-apiVersion: v1
-kind: PersistentVolumeClaim
-metadata:
-  name: postgres
-spec:
-  accessModes:
-    - ReadWriteOnce
-  resources:
-    requests:
-      storage: 5Gi
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: postgres
-spec:
-  selector:
-    matchLabels:
-      app: postgres
-  template:
-    metadata:
-      labels:
-        app: postgres
-    spec:
-      containers:
-        - name: postgres
-          image: postgres:11.2-alpine
-          resources:
-            limits:
-              memory: 256Mi
-              cpu: 512m
-          ports:
-            - containerPort: 5432
-          envFrom:
-            - configMapRef:
-                name: postgres
-          volumeMounts:
-            - name: postgres
-              mountPath: /var/lib/postgresql/data
-      volumes:
-        - name: postgres
-          persistentVolumeClaim:
-            claimName: postgres
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: postgres
-spec:
-  selector:
-    app: postgres
-  ports:
-    - port: 5432
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: lemmy-server--prod
-data:
-  LEMMY_FRONT_END_DIR: /opt/lemmy/ui--prod/dist # not actually used here, polyfill for monolith
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: lemmy-server--prod
-spec:
-  selector:
-    matchLabels:
-      app: lemmy-server--prod
-  template:
-    metadata:
-      labels:
-        app: lemmy-server--prod
-    spec:
-      containers:
-        - name: lemmy-server--prod
-          image: registry.gitlab.com/pojntfx/lemmy/server.prod
-          envFrom:
-            - configMapRef:
-                name: postgres
-            - configMapRef:
-                name: lemmy-server--prod
-          resources:
-            limits:
-              memory: 512Mi
-              cpu: 512m
-          ports:
-            - containerPort: 8536
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: lemmy-server--prod
-spec:
-  selector:
-    app: lemmy-server--prod
-  ports:
-    - port: 8536
-      targetPort: 8536
diff --git a/skaffold.yaml b/skaffold.yaml
deleted file mode 100644 (file)
index 9aeaa58..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-apiVersion: skaffold/v1beta9
-kind: Config
-profiles:
-  - name: lemmy--dev
-    build:
-      artifacts:
-        - image: registry.gitlab.com/pojntfx/lemmy/server.dev
-          context: server
-          docker:
-            dockerfile: Dockerfile.dev
-        - image: registry.gitlab.com/pojntfx/lemmy/ui.dev
-          context: ui
-          docker:
-            dockerfile: Dockerfile.dev
-          sync:
-            "***/*.ts": .
-            "***/*.tsx": .
-            "***/*.css": .
-    deploy:
-      kubectl:
-        manifests:
-          - "**/*.dev.yaml"
-  - name: lemmy--prod
-    build:
-      artifacts:
-        - image: registry.gitlab.com/pojntfx/lemmy/server.prod
-          context: server
-          docker:
-            dockerfile: Dockerfile.prod
-        - image: registry.gitlab.com/pojntfx/lemmy/ui.prod
-          context: ui
-          docker:
-            dockerfile: Dockerfile.prod
-    deploy:
-      kubectl:
-        manifests:
-          - "**/*.prod.yaml"
diff --git a/ui/Dockerfile.dev b/ui/Dockerfile.dev
deleted file mode 100644 (file)
index 37f9e34..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-# Setup env
-FROM node:10-alpine
-RUN mkdir -p /opt/lemmy/ui--dev
-WORKDIR /opt/lemmy/ui--dev
-# Install deps
-COPY package.json .
-COPY yarn.lock .
-RUN npm install
-# Add app
-COPY . .
-# Run app
-CMD ["npm", "start"]
diff --git a/ui/Dockerfile.prod b/ui/Dockerfile.prod
deleted file mode 100644 (file)
index 9c478e6..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-# Setup env
-FROM node:10-alpine AS build
-RUN mkdir -p /opt/lemmy/ui--prod
-WORKDIR /opt/lemmy/ui--prod
-# Install deps
-COPY package.json .
-COPY yarn.lock .
-RUN npm install
-# Add app
-COPY . .
-# Build app
-RUN npm run build
-
-# Setup env
-FROM node:10-alpine
-RUN mkdir -p /opt/lemmy/ui--prod
-WORKDIR /opt/lemmy/ui--prod
-RUN npm install serve
-# Add app
-COPY --from=build /opt/lemmy/ui--prod/dist .
-# Run app
-CMD ["/opt/lemmy/ui--prod/node_modules/.bin/serve", "."]
index 7a1b2c2ae3aeb6e1a7239759356292f337388668..825459657114516ea51479533a013834f0178ac1 100644 (file)
-<?xml version="1.0" encoding="iso-8859-1"?>\r
-<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->\r
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"\r
-        viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">\r
-<g>\r
-       <g>\r
-               <path d="M499.059,323.505l-7.52-32.532l-70.047,16.19c1.513-11.983,2.297-24.042,2.297-36.037c0-18.334-1.801-35.785-5.316-52.19\r
-                       c29.365-12.101,55.143-28.885,69.372-45.529c17.524-20.498,25.985-46.568,23.822-73.406\r
-                       c-2.163-26.862-14.706-51.268-35.316-68.724C433.879-4.694,369.917,0.439,333.774,42.718\r
-                       c-9.546,11.168-18.318,27.381-25.379,46.649c-16.512-5.419-34.132-8.243-52.395-8.243s-35.885,2.824-52.395,8.243\r
-                       c-7.06-19.267-15.832-35.481-25.379-46.649C142.082,0.44,78.123-4.695,35.648,31.277C15.038,48.733,2.494,73.141,0.332,100.001\r
-                       c-2.161,26.838,6.297,52.907,23.822,73.406c14.229,16.644,40.006,33.427,69.372,45.529c-3.515,16.405-5.316,33.856-5.316,52.189\r
-                       c0,11.995,0.785,24.053,2.297,36.037l-70.047-16.19l-7.52,32.532l84.337,19.492c4.349,17.217,10.201,33.953,17.421,49.752\r
-                       L12.941,416.27l7.52,32.532l110.634-25.57c1.38,2.197,2.779,4.373,4.218,6.509c32.548,48.323,75.409,74.934,120.687,74.934\r
-                       c45.278,0,88.138-26.612,120.687-74.934c1.439-2.136,2.839-4.313,4.218-6.509l110.634,25.57l7.52-32.532l-101.758-23.519\r
-                       c7.221-15.799,13.072-32.535,17.421-49.752L499.059,323.505z M183.578,220.372c0-11.41,9.189-20.65,20.482-20.65\r
-                       c11.306,0,20.494,9.24,20.494,20.65c0,11.408-9.188,20.656-20.494,20.656C192.768,241.028,183.578,231.78,183.578,220.372z\r
-                        M256,413.29c-29.895,0-54.216-19.471-54.216-43.403c0-23.932,24.322-43.403,54.216-43.403s54.216,19.471,54.216,43.403\r
-                       C310.216,393.819,285.895,413.29,256,413.29z M307.785,241.183c-11.402,0-20.65-9.317-20.65-20.81\r
-                       c0-11.494,9.248-20.81,20.65-20.81c11.387,0,20.635,9.317,20.635,20.81C328.422,231.866,319.173,241.183,307.785,241.183z"/>\r
-       </g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-<g>\r
-</g>\r
-</svg>\r
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="1024"
+   height="1024"
+   viewBox="0 0 1024 1024"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.4 (unknown)"
+   sodipodi:docname="lemmy-logo-border.svg"
+   inkscape:export-filename="/home/andres/Pictures/References/Logos/Lemmy/lemmy-logo-border.png"
+   inkscape:export-xdpi="300"
+   inkscape:export-ydpi="300"
+   enable-background="new">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.49497475"
+     inkscape:cx="452.38625"
+     inkscape:cy="470.53357"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:showpageshadow="false"
+     inkscape:window-width="1366"
+     inkscape:window-height="740"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     inkscape:snap-global="true"
+     inkscape:snap-midpoints="false"
+     inkscape:snap-smooth-nodes="false"
+     inkscape:object-paths="false"
+     inkscape:pagecheckerboard="true" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-26.066658)"
+     style="display:inline">
+    <path
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="m 167.03908,270.78735 c -0.94784,-0.002 -1.8939,0.004 -2.83789,0.0215 -4.31538,0.0778 -8.58934,0.3593 -12.8125,0.8457 -33.78522,3.89116 -64.215716,21.86394 -82.871086,53.27344 -18.27982,30.77718 -22.77749,64.66635 -13.46094,96.06837 9.31655,31.40203 31.88488,59.93174 65.296886,82.5332 0.20163,0.13618 0.40678,0.26709 0.61523,0.39258 28.65434,17.27768 57.18167,28.93179 87.74218,34.95508 -0.74566,12.61339 -0.72532,25.5717 0.082,38.84375 2.43989,40.10943 16.60718,77.03742 38.0957,109.67187 l -77.00781,31.4375 c -8.30605,3.25932 -12.34178,12.68234 -8.96967,20.94324 3.37211,8.2609 12.84919,12.16798 21.06342,8.68371 l 84.69727,-34.57617 c 15.70675,18.72702 33.75346,35.68305 53.12109,50.57032 0.74013,0.56891 1.4904,1.12236 2.23437,1.68554 l -49.61132,65.69141 c -5.45446,7.0474 -4.10058,17.19288 3.01098,22.5634 7.11156,5.37052 17.24028,3.89649 22.52612,-3.27824 l 50.38672,-66.71876 c 27.68572,17.53469 57.07524,31.20388 86.07227,40.25196 14.88153,27.28008 43.96965,44.64648 77.58789,44.64648 33.93762,0 63.04252,-18.68693 77.80082,-45.4375 28.7072,-9.21295 57.7527,-22.93196 85.1484,-40.40234 l 51.0977,67.66016 c 5.2858,7.17473 15.4145,8.64876 22.5261,3.27824 7.1115,-5.37052 8.4654,-15.516 3.011,-22.5634 l -50.3614,-66.68555 c 0.334,-0.25394 0.6727,-0.50077 1.0059,-0.75586 19.1376,-14.64919 37.0259,-31.28581 52.7031,-49.63476 l 82.5625,33.70507 c 8.2143,3.48427 17.6913,-0.42281 21.0634,-8.68371 3.3722,-8.2609 -0.6636,-17.68392 -8.9696,-20.94324 l -74.5391,-30.42773 c 22.1722,-32.82971 37.0383,-70.03397 40.1426,-110.46094 1.0253,-13.35251 1.2292,-26.42535 0.6387,-39.17578 30.3557,-6.05408 58.7164,-17.66833 87.2011,-34.84375 0.2085,-0.12549 0.4136,-0.2564 0.6153,-0.39258 33.412,-22.60147 55.9803,-51.13117 65.2968,-82.5332 9.3166,-31.40202 4.8189,-65.29118 -13.4609,-96.06837 -18.6553,-31.40951 -49.0859,-49.38228 -82.8711,-53.27344 -4.2231,-0.4864 -8.4971,-0.76791 -12.8125,-0.8457 -30.2077,-0.54448 -62.4407,8.82427 -93.4316,26.71484 -22.7976,13.16063 -43.3521,33.31423 -59.4375,55.30469 -44.9968,-25.75094 -103.5444,-40.25065 -175.4785,-41.43945 -6.4522,-0.10663 -13.0125,-0.10696 -19.67974,0.002 -80.18875,1.30929 -144.38284,16.5086 -192.87109,43.9922 -0.11914,-0.19111 -0.24287,-0.37932 -0.37109,-0.56446 -16.29,-22.764 -37.41085,-43.73706 -60.89649,-57.29493 -30.02247,-17.33149 -61.21051,-26.66489 -90.59375,-26.73633 z"
+       id="path817-3"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccssccccccscccccscccscccscccccsccscccssccscscccscc"
+       inkscape:label="white-border"
+       sodipodi:insensitive="true" />
+    <path
+       id="path1087"
+       style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 716.85595,362.96478 c 15.29075,-21.36763 35.36198,-41.10921 56.50979,-53.31749 66.66377,-38.48393 137.02617,-33.22172 170.08018,22.43043 33.09493,55.72093 14.98656,117.48866 -47.64399,159.85496 -31.95554,19.26819 -62.93318,30.92309 -97.22892,35.54473 M 307.14407,362.96478 C 291.85332,341.59715 271.78209,321.85557 250.63429,309.64729 183.97051,271.16336 113.60811,276.42557 80.554051,332.07772 47.459131,387.79865 65.56752,449.56638 128.19809,491.93268 c 31.95554,19.26819 62.93319,30.92309 97.22893,35.54473"
+       inkscape:connector-curvature="0"
+       inkscape:label="ears"
+       sodipodi:insensitive="true" />
+    <path
+       style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 801.23205,576.8699 C 812.73478,427.06971 720.58431,321.98291 511.99999,325.38859 303.41568,328.79426 213.71393,428.0311 222.76794,576.8699 c 8.64289,142.08048 176.80223,246.40388 288.12038,246.40388 111.31815,0 279.45076,-104.5447 290.34373,-246.40388 z"
+       id="path969"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="szszs"
+       inkscape:label="head"
+       sodipodi:insensitive="true" />
+    <path
+       id="path1084"
+       style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 610.4991,644.28932 c 0,23.11198 18.70595,41.84795 41.78091,41.84795 23.07495,0 41.7809,-18.73597 41.7809,-41.84795 0,-23.112 -18.70594,-41.84796 -41.7809,-41.84796 -23.07496,0 -41.78091,18.73596 -41.78091,41.84796 z m -280.56002,0 c 0,23.32492 18.87829,42.23352 42.16586,42.23352 23.28755,0 42.16585,-18.9086 42.16585,-42.23352 0,-23.32494 -18.87829,-42.23353 -42.16585,-42.23353 -23.28757,0 -42.16586,18.90859 -42.16586,42.23353 z"
+       inkscape:connector-curvature="0"
+       inkscape:label="eyes"
+       sodipodi:nodetypes="ssssssssss"
+       sodipodi:insensitive="true" />
+    <path
+       id="path1008"
+       style="display:inline;opacity:1;fill:none;stroke:#000000;stroke-width:32;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 339.72919,769.2467 -54.54422,72.22481 m 399.08582,-72.22481 54.54423,72.22481 M 263.68341,697.82002 175.92752,733.64353 m 579.85765,-35.82351 87.7559,35.82351"
+       inkscape:connector-curvature="0"
+       inkscape:label="whiskers"
+       sodipodi:nodetypes="cccccccc"
+       sodipodi:insensitive="true" />
+    <path
+       style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 512.00082,713.08977 c -45.86417,0 -75.13006,31.84485 -74.14159,71.10084 1.07048,42.51275 32.46865,71.10323 74.14159,71.10323 41.67296,0 74.05118,-32.99608 74.14161,-71.10323 0.0932,-39.26839 -28.27742,-71.10084 -74.14161,-71.10084 z"
+       id="path1115"
+       inkscape:connector-curvature="0"
+       inkscape:label="nose"
+       sodipodi:nodetypes="zszsz"
+       sodipodi:insensitive="true" />
+  </g>
+</svg>
index 4755d9f5abf69016649ead5aca545e2e091833a3..85eb75e2abf29752f6472664cd0383425ba8ead4 100644 (file)
@@ -24,6 +24,9 @@ Sparky.task('config', _ => {
     transformers: {
       before: [transformClasscat(), transformInferno()],
     },
+    alias: {
+      'locale': 'moment/locale'
+               },
     plugins: [
       EnvPlugin({ NODE_ENV: isProduction ? 'production' : 'development' }),
       CSSPlugin(),
@@ -45,7 +48,7 @@ Sparky.task('config', _ => {
 // Sparky.task('version', _ => setVersion());
 Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
 Sparky.task('env', _ => (isProduction = true));
-Sparky.task('copy-assets', () => Sparky.src('assets/**/**.*').dest('dist/'));
+Sparky.task('copy-assets', () => Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static'));
 Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
   fuse.dev();
   app.hmr().watch();
index 7f99e62fa44d02b4bf4c7b24d1670c0c69e1d9f6..797e45bc8c1915fce17e59c2fdb912118ea8a5d9 100644 (file)
@@ -23,7 +23,9 @@
     "autosize": "^4.0.2",
     "classcat": "^1.1.3",
     "dotenv": "^6.1.0",
+    "i18next": "^17.0.9",
     "inferno": "^7.0.1",
+    "inferno-i18next": "nimbusec-oss/inferno-i18next",
     "inferno-router": "^7.0.1",
     "js-cookie": "^2.2.0",
     "jwt-decode": "^2.2.0",
     "markdown-it-emoji": "^1.4.0",
     "moment": "^2.24.0",
     "rxjs": "^6.4.0",
-    "terser": "^3.17.0"
+    "terser": "^3.17.0",
+    "ws": "^7.0.0"
   },
   "devDependencies": {
+    "@types/i18next": "^12.1.0",
     "fuse-box": "^3.1.3",
     "ts-transform-classcat": "^0.0.2",
     "ts-transform-inferno": "^4.0.2",
-    "typescript": "^3.3.3333"
+    "typescript": "^3.5.3"
   }
 }
index a69ae06fc65911021df78a0c8ea0e8c869e1e181..7a75d9e4d4e9f4e362d5b8d5f5ecf45ae6bfa0f1 100644 (file)
@@ -1,7 +1,10 @@
 import { Component, linkEvent } from 'inferno';
 import { CommentNode as CommentNodeI, CommentForm as CommentFormI } from '../interfaces';
+import { capitalizeFirstLetter } from '../utils';
 import { WebSocketService, UserService } from '../services';
 import * as autosize from 'autosize';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 interface CommentFormProps {
   postId?: number;
@@ -25,12 +28,13 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
       post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId,
       creator_id: UserService.Instance.user ? UserService.Instance.user.id : null,
     },
-    buttonTitle: !this.props.node ? "Post" : this.props.edit ? "Edit" : "Reply",
+    buttonTitle: !this.props.node ? capitalizeFirstLetter(i18n.t('post')) : this.props.edit ? capitalizeFirstLetter(i18n.t('edit')) : capitalizeFirstLetter(i18n.t('reply')),
   }
 
   constructor(props: any, context: any) {
     super(props, context);
 
+
     this.state = this.emptyState;
 
     if (this.props.node) {
@@ -52,7 +56,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
 
   render() {
     return (
-      <div>
+      <div class="mb-3">
         <form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
           <div class="form-group row">
             <div class="col-sm-12">
@@ -62,7 +66,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
           <div class="row">
             <div class="col-sm-12">
               <button type="submit" class="btn btn-sm btn-secondary mr-2" disabled={this.props.disabled}>{this.state.buttonTitle}</button>
-              {this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>}
+              {this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}><T i18nKey="cancel">#</T></button>}
             </div>
           </div>
         </form>
@@ -84,6 +88,8 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
     if (i.props.node) {
       i.props.onReplyCancel();
     }
+
+    autosize.update(document.querySelector('textarea'));
   }
 
   handleCommentContentChange(i: CommentForm, event: any) {
index 126966a7badc90c7ac70f1e01c2723d6556e1e09..8779f1f9d96de95a725370a37665de592fc0bfd9 100644 (file)
@@ -1,12 +1,14 @@
 import { Component, linkEvent } from 'inferno';
 import { Link } from 'inferno-router';
-import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, SaveCommentForm, BanFromCommunityForm, BanUserForm, CommunityUser, UserView, AddModToCommunityForm, AddAdminForm } from '../interfaces';
+import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, SaveCommentForm, BanFromCommunityForm, BanUserForm, CommunityUser, UserView, AddModToCommunityForm, AddAdminForm, TransferCommunityForm, TransferSiteForm } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { mdToHtml, getUnixTime, canMod, isMod } from '../utils';
 import * as moment from 'moment';
 import { MomentTime } from './moment-time';
 import { CommentForm } from './comment-form';
 import { CommentNodes } from './comment-nodes';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 enum BanType {Community, Site};
 
@@ -19,6 +21,9 @@ interface CommentNodeState {
   banReason: string;
   banExpires: string;
   banType: BanType;
+  collapsed: boolean;
+  showConfirmTransferSite: boolean;
+  showConfirmTransferCommunity: boolean;
 }
 
 interface CommentNodeProps {
@@ -41,7 +46,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     showBanDialog: false,
     banReason: null,
     banExpires: null,
-    banType: BanType.Community
+    banType: BanType.Community,
+    collapsed: false,
+    showConfirmTransferSite: false,
+    showConfirmTransferCommunity: false,
   }
 
   constructor(props: any, context: any) {
@@ -57,25 +65,25 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     let node = this.props.node;
     return (
       <div className={`comment ${node.comment.parent_id  && !this.props.noIndent ? 'ml-4' : ''}`}>
-        <div className={`mr-1 float-left small text-center ${this.props.viewOnly && 'no-click'}`}>
-          <div className={`pointer ${node.comment.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentLike)}>
+        <div className={`vote-bar mr-2 float-left small text-center ${this.props.viewOnly && 'no-click'}`}>
+          <button className={`btn p-0 ${node.comment.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentLike)}>
             <svg class="icon upvote"><use xlinkHref="#icon-arrow-up"></use></svg>
-          </div>
+          </button>
           <div class={`font-weight-bold text-muted`}>{node.comment.score}</div>
-          <div className={`pointer ${node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentDisLike)}>
+          <button className={`btn p-0 ${node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentDisLike)}>
             <svg class="icon downvote"><use xlinkHref="#icon-arrow-down"></use></svg>
-          </div>
+          </button>
         </div>
-        <div id={`comment-${node.comment.id}`} className={`details ml-4 ${this.isCommentNew ? 'mark' : ''}`}>
+        <div id={`comment-${node.comment.id}`} className={`details comment-node ml-4 ${this.isCommentNew ? 'mark' : ''}`}>
           <ul class="list-inline mb-0 text-muted small">
             <li className="list-inline-item">
               <Link className="text-info" to={`/u/${node.comment.creator_name}`}>{node.comment.creator_name}</Link>
             </li>
             {this.isMod && 
-              <li className="list-inline-item badge badge-light">mod</li>
+              <li className="list-inline-item badge badge-light"><T i18nKey="mod">#</T></li>
             }
             {this.isAdmin && 
-              <li className="list-inline-item badge badge-light">admin</li>
+              <li className="list-inline-item badge badge-light"><T i18nKey="admin">#</T></li>
             }
             <li className="list-inline-item">
               <span>(
@@ -88,28 +96,31 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
             <li className="list-inline-item">
               <span><MomentTime data={node.comment} /></span>
             </li>
+            <li className="list-inline-item">
+              <div className="pointer text-monospace" onClick={linkEvent(this, this.handleCommentCollapse)}>{this.state.collapsed ? '[+]' : '[-]'}</div>
+            </li>
           </ul>
           {this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
-          {!this.state.showEdit &&
+          {!this.state.showEdit && !this.state.collapsed &&
             <div>
-              <div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.removed ? '*removed*' : node.comment.deleted ? '*deleted*' : node.comment.content)} />
+              <div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.removed ? `*${i18n.t('removed')}*` : node.comment.deleted ? `*${i18n.t('deleted')}*` : node.comment.content)} />
               <ul class="list-inline mb-1 text-muted small font-weight-bold">
                 {UserService.Instance.user && !this.props.viewOnly && 
                   <>
                     <li className="list-inline-item">
-                      <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span>
+                      <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}><T i18nKey="reply">#</T></span>
                     </li>
                     <li className="list-inline-item mr-2">
-                      <span class="pointer" onClick={linkEvent(this, this.handleSaveCommentClick)}>{node.comment.saved ? 'unsave' : 'save'}</span>
+                      <span class="pointer" onClick={linkEvent(this, this.handleSaveCommentClick)}>{node.comment.saved ? i18n.t('unsave') : i18n.t('save')}</span>
                     </li>
                     {this.myComment && 
                       <>
                         <li className="list-inline-item">
-                          <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
+                          <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
                         </li>
                         <li className="list-inline-item">
                           <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
-                            {!this.props.node.comment.deleted ? 'delete' : 'restore'}
+                            {!this.props.node.comment.deleted ? i18n.t('delete') : i18n.t('restore')}
                           </span>
                         </li>
                       </>
@@ -118,8 +129,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                     {this.canMod && 
                       <li className="list-inline-item">
                         {!this.props.node.comment.removed ? 
-                        <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
-                        <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
+                        <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
+                        <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
                         }
                       </li>
                     }
@@ -129,44 +140,70 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                         {!this.isMod && 
                           <li className="list-inline-item">
                             {!this.props.node.comment.banned_from_community ? 
-                            <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}>ban</span> :
-                            <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}>unban</span>
+                            <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}><T i18nKey="ban">#</T></span> :
+                            <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}><T i18nKey="unban">#</T></span>
                             }
                           </li>
                         }
                         {!this.props.node.comment.banned_from_community &&
                           <li className="list-inline-item">
-                            <span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{`${this.isMod ? 'remove' : 'appoint'} as mod`}</span>
+                            <span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{this.isMod ? i18n.t('remove_as_mod') : i18n.t('appoint_as_mod')}</span>
                           </li>
                         }
                       </>
                     }
+                    {/* Community creators and admins can transfer community to another mod */}
+                    {(this.amCommunityCreator || this.canAdmin) && this.isMod &&
+                      <li className="list-inline-item">
+                        {!this.state.showConfirmTransferCommunity ?
+                        <span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferCommunity)}><T i18nKey="transfer_community">#</T>
+                      </span> : <>
+                        <span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
+                        <span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferCommunity)}><T i18nKey="yes">#</T></span>
+                        <span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferCommunity)}><T i18nKey="no">#</T></span>
+                      </>
+                        }
+                      </li>
+                    }
                     {/* Admins can ban from all, and appoint other admins */}
                     {this.canAdmin &&
                       <>
                         {!this.isAdmin && 
                           <li className="list-inline-item">
                             {!this.props.node.comment.banned ? 
-                            <span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}>ban from site</span> :
-                            <span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}>unban from site</span>
+                            <span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}><T i18nKey="ban_from_site">#</T></span> :
+                            <span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}><T i18nKey="unban_from_site">#</T></span>
                             }
                           </li>
                         }
                         {!this.props.node.comment.banned &&
                           <li className="list-inline-item">
-                            <span class="pointer" onClick={linkEvent(this, this.handleAddAdmin)}>{`${this.isAdmin ? 'remove' : 'appoint'} as admin`}</span>
+                            <span class="pointer" onClick={linkEvent(this, this.handleAddAdmin)}>{this.isAdmin ? i18n.t('remove_as_admin') : i18n.t('appoint_as_admin')}</span>
                           </li>
                         }
                       </>
                     }
+                    {/* Site Creator can transfer to another admin */}
+                    {this.amSiteCreator && this.isAdmin &&
+                      <li className="list-inline-item">
+                        {!this.state.showConfirmTransferSite ?
+                        <span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferSite)}><T i18nKey="transfer_site">#</T>
+                      </span> : <>
+                        <span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
+                        <span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferSite)}><T i18nKey="yes">#</T></span>
+                        <span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferSite)}><T i18nKey="no">#</T></span>
+                      </>
+                        }
+                      </li>
+                    }
                   </>
                 }
                 <li className="list-inline-item">
-                  <Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}>link</Link>
+                  <Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}><T i18nKey="link">#</T></Link>
                 </li>
                 {this.props.markable && 
                   <li className="list-inline-item">
-                    <span class="pointer" onClick={linkEvent(this, this.handleMarkRead)}>{`mark as ${node.comment.read ? 'unread' : 'read'}`}</span>
+                    <span class="pointer" onClick={linkEvent(this, this.handleMarkRead)}>{node.comment.read ? i18n.t('mark_as_unread') : i18n.t('mark_as_read')}</span>
                   </li>
                 }
               </ul>
@@ -175,23 +212,23 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
         </div>
         {this.state.showRemoveDialog && 
           <form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
-            <input type="text" class="form-control mr-2" placeholder="Reason" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
-            <button type="submit" class="btn btn-secondary">Remove Comment</button>
+            <input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
+            <button type="submit" class="btn btn-secondary"><T i18nKey="remove_comment">#</T></button>
           </form>
         }
         {this.state.showBanDialog && 
           <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
             <div class="form-group row">
-              <label class="col-form-label">Reason</label>
-              <input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
+              <label class="col-form-label"><T i18nKey="reason">#</T></label>
+              <input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
             </div>
             {/* TODO hold off on expires until later */}
             {/* <div class="form-group row"> */}
             {/*   <label class="col-form-label">Expires</label> */}
-            {/*   <input type="date" class="form-control mr-2" placeholder="Expires" value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
+            {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
             {/* </div> */}
             <div class="form-group row">
-              <button type="submit" class="btn btn-secondary">Ban {this.props.node.comment.creator_name}</button>
+              <button type="submit" class="btn btn-secondary">{i18n.t('ban')} {this.props.node.comment.creator_name}</button>
             </div>
           </form>
         }
@@ -202,7 +239,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
             disabled={this.props.locked} 
           />
         }
-        {this.props.node.children && 
+        {this.props.node.children && !this.state.collapsed &&
           <CommentNodes 
             nodes={this.props.node.children} 
             locked={this.props.locked} 
@@ -210,6 +247,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
             admins={this.props.admins}
           />
         }
+        {/* A collapsed clearfix */}
+        {this.state.collapsed && <div class="row col-12"></div>}
       </div>
     )
   }
@@ -242,6 +281,20 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
   }
 
+  get amCommunityCreator(): boolean {
+    return this.props.moderators && 
+      UserService.Instance.user && 
+      (this.props.node.comment.creator_id != UserService.Instance.user.id) &&
+      (UserService.Instance.user.id == this.props.moderators[0].user_id);
+  }
+
+  get amSiteCreator(): boolean {
+    return this.props.admins && 
+      UserService.Instance.user && 
+      (this.props.node.comment.creator_id != UserService.Instance.user.id) &&
+      (UserService.Instance.user.id == this.props.admins[0].id);
+  }
+
   handleReplyClick(i: CommentNode) {
     i.state.showReply = true;
     i.setState(i.state);
@@ -380,9 +433,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   handleModBanBothSubmit(i: CommentNode) {
     event.preventDefault();
 
-    console.log(BanType[i.state.banType]);
-    console.log(i.props.node.comment.banned);
-
     if (i.state.banType == BanType.Community) {
       let form: BanFromCommunityForm = {
         user_id: i.props.node.comment.creator_id,
@@ -425,9 +475,53 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     i.setState(i.state);
   }
 
+  handleShowConfirmTransferCommunity(i: CommentNode) { 
+    i.state.showConfirmTransferCommunity = true;
+    i.setState(i.state);
+  }
+
+  handleCancelShowConfirmTransferCommunity(i: CommentNode) { 
+    i.state.showConfirmTransferCommunity = false;
+    i.setState(i.state);
+  }
+
+  handleTransferCommunity(i: CommentNode) {
+    let form: TransferCommunityForm = {
+      community_id: i.props.node.comment.community_id,
+      user_id: i.props.node.comment.creator_id,
+    };
+    WebSocketService.Instance.transferCommunity(form);
+    i.state.showConfirmTransferCommunity = false;
+    i.setState(i.state);
+  }
+
+  handleShowConfirmTransferSite(i: CommentNode) { 
+    i.state.showConfirmTransferSite = true;
+    i.setState(i.state);
+  }
+
+  handleCancelShowConfirmTransferSite(i: CommentNode) { 
+    i.state.showConfirmTransferSite = false;
+    i.setState(i.state);
+  }
+
+  handleTransferSite(i: CommentNode) {
+    let form: TransferSiteForm = {
+      user_id: i.props.node.comment.creator_id,
+    };
+    WebSocketService.Instance.transferSite(form);
+    i.state.showConfirmTransferSite = false;
+    i.setState(i.state);
+  }
+
   get isCommentNew(): boolean {
     let now = moment.utc().subtract(10, 'minutes');
     let then = moment.utc(this.props.node.comment.published);
     return now.isBefore(then);
   }
+
+  handleCommentCollapse(i: CommentNode) {
+    i.state.collapsed = !i.state.collapsed;
+    i.setState(i.state);
+  }
 }
index da67bbc7f395e1bb64d3e6132b070f085ad41dde..fca323e395b78e6cc4e9a77fbe2158c6ef756864 100644 (file)
@@ -32,7 +32,7 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
             moderators={this.props.moderators}
             admins={this.props.admins}
             markable={this.props.markable}
-            />
+          />
         )}
       </div>
     )
index 96864e9a584bdb96f4705fdd0b077faf8e313abc..49b982dc95df564674ad980d1ebda18d9125638e 100644 (file)
@@ -5,6 +5,8 @@ import { retryWhen, delay, take } from 'rxjs/operators';
 import { UserOperation, Community, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm, ListCommunitiesForm, SortType } from '../interfaces';
 import { WebSocketService } from '../services';
 import { msgOp } from '../utils';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 declare const Sortable: any;
 
@@ -26,12 +28,12 @@ export class Communities extends Component<any, CommunitiesState> {
     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),
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+      .subscribe(
+        (msg) => this.parseMessage(msg),
         (err) => console.error(err),
         () => console.log('complete')
-    );
+      );
 
     this.refetch();
 
@@ -46,9 +48,7 @@ export class Communities extends Component<any, CommunitiesState> {
   }
 
   componentDidMount() {
-    document.title = "Communities - Lemmy";
-    let table = document.querySelector('#community_table');
-    Sortable.initTable(table);
+    document.title = `${i18n.t('communities')} - ${WebSocketService.Instance.site.name}`;
   }
 
   // Necessary for back button for some reason
@@ -66,17 +66,17 @@ export class Communities extends Component<any, CommunitiesState> {
         {this.state.loading ? 
         <h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : 
         <div>
-          <h5>List of communities</h5>
+          <h5><T i18nKey="list_of_communities">#</T></h5>
           <div class="table-responsive">
             <table id="community_table" class="table table-sm table-hover">
               <thead class="pointer">
                 <tr>
-                  <th>Name</th>
-                  <th class="d-none d-lg-table-cell">Title</th>
-                  <th>Category</th>
-                  <th class="text-right">Subscribers</th>
-                  <th class="text-right d-none d-lg-table-cell">Posts</th>
-                  <th class="text-right d-none d-lg-table-cell">Comments</th>
+                  <th><T i18nKey="name">#</T></th>
+                  <th class="d-none d-lg-table-cell"><T i18nKey="title">#</T></th>
+                  <th><T i18nKey="category">#</T></th>
+                  <th class="text-right"><T i18nKey="subscribers">#</T></th>
+                  <th class="text-right d-none d-lg-table-cell"><T i18nKey="posts">#</T></th>
+                  <th class="text-right d-none d-lg-table-cell"><T i18nKey="comments">#</T></th>
                   <th></th>
                 </tr>
               </thead>
@@ -91,8 +91,8 @@ export class Communities extends Component<any, CommunitiesState> {
                     <td class="text-right d-none d-lg-table-cell">{community.number_of_comments}</td>
                     <td class="text-right">
                       {community.subscribed ? 
-                      <span class="pointer btn-link" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</span> : 
-                      <span class="pointer btn-link" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</span>
+                      <span class="pointer btn-link" onClick={linkEvent(community.id, this.handleUnsubscribe)}><T i18nKey="unsubscribe">#</T></span> : 
+                      <span class="pointer btn-link" onClick={linkEvent(community.id, this.handleSubscribe)}><T i18nKey="subscribe">#</T></span>
                       }
                     </td>
                   </tr>
@@ -111,9 +111,9 @@ export class Communities extends Component<any, CommunitiesState> {
     return (
       <div class="mt-2">
         {this.state.page > 1 && 
-          <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
+          <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
         }
-        <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
+        <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
       </div>
     );
   }
@@ -167,7 +167,7 @@ export class Communities extends Component<any, CommunitiesState> {
     console.log(msg);
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
-      alert(msg.error);
+      alert(i18n.t(msg.error));
       return;
     } else if (op == UserOperation.ListCommunities) {
       let res: ListCommunitiesResponse = msg;
@@ -176,6 +176,8 @@ export class Communities extends Component<any, CommunitiesState> {
       this.state.loading = false;
       window.scrollTo(0,0);
       this.setState(this.state);
+      let table = document.querySelector('#community_table');
+      Sortable.initTable(table);
     } else if (op == UserOperation.FollowCommunity) {
       let res: CommunityResponse = msg;
       let found = this.state.communities.find(c => c.id == res.community.id);
index e295dcbedd340a9588107931efaaa529e2204c0a..833d8a3f0acf3aaeefcbe3352142abb5b6cd8570 100644 (file)
@@ -3,8 +3,10 @@ import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
 import { CommunityForm as CommunityFormI, UserOperation, Category, ListCategoriesResponse, CommunityResponse } from '../interfaces';
 import { WebSocketService } from '../services';
-import { msgOp } from '../utils';
+import { msgOp, capitalizeFirstLetter } from '../utils';
 import * as autosize from 'autosize';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 import { Community } from '../interfaces';
 
@@ -28,7 +30,8 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
     communityForm: {
       name: null,
       title: null,
-      category_id: null
+      category_id: null,
+      nsfw: false,
     },
     categories: [],
     loading: false
@@ -46,6 +49,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
         category_id: this.props.community.category_id,
         description: this.props.community.description,
         edit_id: this.props.community.id,
+        nsfw: this.props.community.nsfw,
         auth: null
       }
     }
@@ -74,25 +78,25 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
     return (
       <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
         <div class="form-group row">
-          <label class="col-12 col-form-label">Name</label>
+          <label class="col-12 col-form-label"><T i18nKey="name">#</T></label>
           <div class="col-12">
-            <input type="text" class="form-control" value={this.state.communityForm.name} onInput={linkEvent(this, this.handleCommunityNameChange)} required minLength={3} maxLength={20} pattern="[a-z0-9_]+" title="lowercase, underscores, and no spaces."/>
+            <input type="text" class="form-control" value={this.state.communityForm.name} onInput={linkEvent(this, this.handleCommunityNameChange)} required minLength={3} maxLength={20} pattern="[a-z0-9_]+" title={i18n.t('community_reqs')}/>
           </div>
         </div>
         <div class="form-group row">
-          <label class="col-12 col-form-label">Title</label>
+          <label class="col-12 col-form-label"><T i18nKey="title">#</T></label>
           <div class="col-12">
             <input type="text" value={this.state.communityForm.title} onInput={linkEvent(this, this.handleCommunityTitleChange)} class="form-control" required minLength={3} maxLength={100} />
           </div>
         </div>
         <div class="form-group row">
-          <label class="col-12 col-form-label">Sidebar</label>
+          <label class="col-12 col-form-label"><T i18nKey="sidebar">#</T></label>
           <div class="col-12">
             <textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={3} maxLength={10000} />
           </div>
         </div>
         <div class="form-group row">
-          <label class="col-12 col-form-label">Category</label>
+          <label class="col-12 col-form-label"><T i18nKey="category">#</T></label>
           <div class="col-12">
             <select class="form-control" value={this.state.communityForm.category_id} onInput={linkEvent(this, this.handleCommunityCategoryChange)}>
               {this.state.categories.map(category =>
@@ -101,13 +105,21 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
             </select>
           </div>
         </div>
+        <div class="form-group row">
+          <div class="col-12">
+            <div class="form-check">
+              <input class="form-check-input" type="checkbox" checked={this.state.communityForm.nsfw} onChange={linkEvent(this, this.handleCommunityNsfwChange)}/>
+              <label class="form-check-label"><T i18nKey="nsfw">#</T></label>
+            </div>
+          </div>
+        </div>
         <div class="form-group row">
           <div class="col-12">
             <button type="submit" class="btn btn-secondary mr-2">
               {this.state.loading ? 
               <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 
-              this.props.community ? 'Save' : 'Create'}</button>
-              {this.props.community && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>}
+              this.props.community ? capitalizeFirstLetter(i18n.t('save')) : capitalizeFirstLetter(i18n.t('create'))}</button>
+              {this.props.community && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}><T i18nKey="cancel">#</T></button>}
           </div>
         </div>
       </form>
@@ -145,6 +157,11 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
     i.setState(i.state);
   }
 
+  handleCommunityNsfwChange(i: CommunityForm, event: any) {
+    i.state.communityForm.nsfw = event.target.checked;
+    i.setState(i.state);
+  }
+
   handleCancel(i: CommunityForm) {
     i.props.onCancel();
   }
@@ -153,7 +170,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
     let op: UserOperation = msgOp(msg);
     console.log(msg);
     if (msg.error) {
-      alert(msg.error);
+      alert(i18n.t(msg.error));
       this.state.loading = false;
       this.setState(this.state);
       return;
@@ -169,8 +186,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
       this.state.loading = false;
       this.props.onCreate(res.community);
     } 
-
-    // TODO is this necessary?
+    // TODO is ths necessary
     else if (op == UserOperation.EditCommunity) {
       let res: CommunityResponse = msg;
       this.state.loading = false;
index efa908f165829855ba2eca6bdcf87d58a9ac7f94..3459320c496297ea10d39a2e724885b3d95a8655 100644 (file)
@@ -6,6 +6,7 @@ import { WebSocketService } from '../services';
 import { PostListings } from './post-listings';
 import { Sidebar } from './sidebar';
 import { msgOp, routeSortTypeToEnum, fetchLimit } from '../utils';
+import { T, i18n } from 'inferno-i18next';
 
 interface State {
   community: CommunityI;
@@ -36,6 +37,7 @@ export class Community extends Component<any, State> {
       number_of_comments: null,
       published: null,
       removed: null,
+      nsfw: false,
       deleted: null,
     },
     moderators: [],
@@ -102,7 +104,10 @@ export class Community extends Component<any, State> {
           <div class="col-12 col-md-8">
             <h5>{this.state.community.title}
             {this.state.community.removed &&
-              <small className="ml-2 text-muted font-italic">removed</small>
+              <small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
+            }
+            {this.state.community.nsfw &&
+              <small className="ml-2 text-muted font-italic"><T i18nKey="nsfw">#</T></small>
             }
           </h5>
           {this.selects()}
@@ -126,15 +131,15 @@ export class Community extends Component<any, State> {
     return (
       <div className="mb-2">
         <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto">
-          <option disabled>Sort Type</option>
-          <option value={SortType.Hot}>Hot</option>
-          <option value={SortType.New}>New</option>
-          <option disabled>──────────</option>
-          <option value={SortType.TopDay}>Top Day</option>
-          <option value={SortType.TopWeek}>Week</option>
-          <option value={SortType.TopMonth}>Month</option>
-          <option value={SortType.TopYear}>Year</option>
-          <option value={SortType.TopAll}>All</option>
+          <option disabled><T i18nKey="sort_type">#</T></option>
+          <option value={SortType.Hot}><T i18nKey="hot">#</T></option>
+          <option value={SortType.New}><T i18nKey="new">#</T></option>
+          <option disabled>──────</option>
+          <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
+          <option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
+          <option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
+          <option value={SortType.TopYear}><T i18nKey="year">#</T></option>
+          <option value={SortType.TopAll}><T i18nKey="all">#</T></option>
         </select>
       </div>
     )
@@ -142,11 +147,11 @@ export class Community extends Component<any, State> {
 
   paginator() {
     return (
-      <div class="mt-2">
+      <div class="my-2">
         {this.state.page > 1 && 
-          <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
+          <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
         }
-        <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
+        <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
       </div>
     );
   }
@@ -193,14 +198,14 @@ export class Community extends Component<any, State> {
     console.log(msg);
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
-      alert(msg.error);
+      alert(i18n.t(msg.error));
       return;
     } else if (op == UserOperation.GetCommunity) {
       let res: GetCommunityResponse = msg;
       this.state.community = res.community;
       this.state.moderators = res.moderators;
       this.state.admins = res.admins;
-      document.title = `/c/${this.state.community.name} - Lemmy`;
+      document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`;
       this.setState(this.state);
       this.fetchPosts();
     } else if (op == UserOperation.EditCommunity) {
index a9345f72ab922fab9836b4e34c74cb2b836dda9f..61245e739f659fdc2d38ebbe1e74c5d01538eb5f 100644 (file)
@@ -1,6 +1,9 @@
 import { Component } from 'inferno';
 import { CommunityForm } from './community-form';
 import { Community } from '../interfaces';
+import { WebSocketService } from '../services';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 export class CreateCommunity extends Component<any, any> {
 
@@ -10,15 +13,15 @@ export class CreateCommunity extends Component<any, any> {
   }
 
   componentDidMount() {
-    document.title = "Create Community - Lemmy";
+    document.title = `${i18n.t('create_community')} - ${WebSocketService.Instance.site.name}`;
   }
 
   render() {
     return (
       <div class="container">
         <div class="row">
-          <div class="col-12 col-lg-6 mb-4">
-            <h5>Create Community</h5>
+          <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+            <h5><T i18nKey="create_community">#</T></h5>
             <CommunityForm onCreate={this.handleCommunityCreate}/>
           </div>
         </div>
index fdee01f66c16565ca48aac9bb341651570c252d5..3e00bd80af9020510e0092833fd77201de40b635 100644 (file)
@@ -1,5 +1,9 @@
 import { Component } from 'inferno';
 import { PostForm } from './post-form';
+import { WebSocketService } from '../services';
+import { PostFormParams } from '../interfaces';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 export class CreatePost extends Component<any, any> {
 
@@ -9,22 +13,34 @@ export class CreatePost extends Component<any, any> {
   }
 
   componentDidMount() {
-    document.title = "Create Post - Lemmy";
+    document.title = `${i18n.t('create_post')} - ${WebSocketService.Instance.site.name}`;
   }
 
   render() {
     return (
       <div class="container">
         <div class="row">
-          <div class="col-12 col-lg-6 mb-4">
-            <h5>Create a Post</h5>
-            <PostForm onCreate={this.handlePostCreate} prevCommunityName={this.prevCommunityName} />
+          <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+            <h5><T i18nKey="create_post">#</T></h5>
+            <PostForm onCreate={this.handlePostCreate} params={this.params} />
           </div>
         </div>
       </div>
     )
   }
 
+  get params(): PostFormParams {
+    let urlParams = new URLSearchParams(this.props.location.search);
+    let params: PostFormParams = {
+      name: urlParams.get("name"),
+      community: urlParams.get("community") || this.prevCommunityName,
+      body: urlParams.get("body"),
+      url: urlParams.get("url"),
+    };
+
+    return params;
+  }
+
   get prevCommunityName(): string {
     if (this.props.match.params.name) {
       return this.props.match.params.name;
index 31403d7cad286dbbf4ab721119f218d60dcf01b1..87d7097e0394688d860556a9441e18b7126f20e7 100644 (file)
@@ -2,6 +2,7 @@ import { Component } from 'inferno';
 import { Link } from 'inferno-router';
 import { repoUrl } from '../utils';
 import { version } from '../version';
+import { T } from 'inferno-i18next';
 
 export class Footer extends Component<any, any> {
 
@@ -19,16 +20,16 @@ export class Footer extends Component<any, any> {
               <span class="navbar-text">{version}</span>
             </li>
             <li class="nav-item">
-              <Link class="nav-link" to="/modlog">Modlog</Link>
+              <Link class="nav-link" to="/modlog"><T i18nKey="modlog">#</T></Link>
             </li>
             <li class="nav-item">
-              <a class="nav-link" href={`${repoUrl}/blob/master/docs/api.md`}>API</a>
+              <a class="nav-link" href={`${repoUrl}/blob/master/docs/api.md`}><T i18nKey="api">#</T></a>
             </li>
             <li class="nav-item">
-              <Link class="nav-link" to="/sponsors">Sponsors</Link>
+              <Link class="nav-link" to="/sponsors"><T i18nKey="sponsors">#</T></Link>
             </li>
             <li class="nav-item">
-              <a class="nav-link" href={repoUrl}>Code</a>
+              <a class="nav-link" href={repoUrl}><T i18nKey="code">#</T></a>
             </li>
           </ul>
         </div>
diff --git a/ui/src/components/home.tsx b/ui/src/components/home.tsx
deleted file mode 100644 (file)
index e69de29..0000000
index 69ddc44b4beba8d9c3a6f183a9c111c76b11f259..c9f46b36af6521c97a59e9ccb15acf010cf36115 100644 (file)
@@ -6,6 +6,8 @@ import { UserOperation, Comment, SortType, GetRepliesForm, GetRepliesResponse, C
 import { WebSocketService, UserService } from '../services';
 import { msgOp } from '../utils';
 import { CommentNodes } from './comment-nodes';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 enum UnreadType {
   Unread, All
@@ -49,7 +51,7 @@ export class Inbox extends Component<any, InboxState> {
   }
 
   componentDidMount() {
-    document.title = `/u/${UserService.Instance.user.username} Inbox - Lemmy`;
+    document.title = `/u/${UserService.Instance.user.username} ${i18n.t('inbox')} - ${WebSocketService.Instance.site.name}`;
   }
 
   render() {
@@ -59,12 +61,12 @@ export class Inbox extends Component<any, InboxState> {
         <div class="row">
           <div class="col-12">
             <h5 class="mb-0">
-              <span>Inbox for <Link to={`/u/${user.username}`}>{user.username}</Link></span>
+              <span><T i18nKey="inbox_for" interpolation={{user: user.username}}>#<Link to={`/u/${user.username}`}>#</Link></T></span>
             </h5>
             {this.state.replies.length > 0 && this.state.unreadType == UnreadType.Unread &&
               <ul class="list-inline mb-1 text-muted small font-weight-bold">
                 <li className="list-inline-item">
-                  <span class="pointer" onClick={this.markAllAsRead}>mark all as read</span>
+                  <span class="pointer" onClick={this.markAllAsRead}><T i18nKey="mark_all_as_read">#</T></span>
                 </li>
               </ul>
             }
@@ -81,18 +83,18 @@ export class Inbox extends Component<any, InboxState> {
     return (
       <div className="mb-2">
         <select value={this.state.unreadType} onChange={linkEvent(this, this.handleUnreadTypeChange)} class="custom-select custom-select-sm w-auto">
-          <option disabled>Type</option>
-          <option value={UnreadType.Unread}>Unread</option>
-          <option value={UnreadType.All}>All</option>
+          <option disabled><T i18nKey="type">#</T></option>
+          <option value={UnreadType.Unread}><T i18nKey="unread">#</T></option>
+          <option value={UnreadType.All}><T i18nKey="all">#</T></option>
         </select>
         <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
-          <option disabled>Sort Type</option>
-          <option value={SortType.New}>New</option>
-          <option value={SortType.TopDay}>Top Day</option>
-          <option value={SortType.TopWeek}>Week</option>
-          <option value={SortType.TopMonth}>Month</option>
-          <option value={SortType.TopYear}>Year</option>
-          <option value={SortType.TopAll}>All</option>
+          <option disabled><T i18nKey="sort_type">#</T></option>
+          <option value={SortType.New}><T i18nKey="new">#</T></option>
+          <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
+          <option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
+          <option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
+          <option value={SortType.TopYear}><T i18nKey="year">#</T></option>
+          <option value={SortType.TopAll}><T i18nKey="all">#</T></option>
         </select>
       </div>
     )
@@ -113,9 +115,9 @@ export class Inbox extends Component<any, InboxState> {
     return (
       <div class="mt-2">
         {this.state.page > 1 && 
-          <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
+          <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
         }
-        <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
+        <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
       </div>
     );
   }
@@ -164,7 +166,7 @@ export class Inbox extends Component<any, InboxState> {
     console.log(msg);
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
-      alert(msg.error);
+      alert(i18n.t(msg.error));
       return;
     } else if (op == UserOperation.GetReplies || op == UserOperation.MarkAllAsRead) {
       let res: GetRepliesResponse = msg;
@@ -196,7 +198,7 @@ export class Inbox extends Component<any, InboxState> {
       this.setState(this.state);
     } else if (op == UserOperation.CreateComment) {
       // let res: CommentResponse = msg;
-      alert('Reply sent');
+      alert(i18n.t('reply_sent'));
       // this.state.replies.unshift(res.comment); // TODO do this right
       // this.setState(this.state);
     } else if (op == UserOperation.SaveComment) {
index 33cebdd6275a9a346384adb2da48f5d897692cb4..66962accf113520215e0c5487b8e3514124da029 100644 (file)
@@ -4,6 +4,8 @@ import { retryWhen, delay, take } from 'rxjs/operators';
 import { LoginForm, RegisterForm, LoginResponse, UserOperation } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { msgOp } from '../utils';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 interface State {
   loginForm: LoginForm;
@@ -26,6 +28,7 @@ export class Login extends Component<any, State> {
       password: undefined,
       password_verify: undefined,
       admin: false,
+      show_nsfw: false,
     },
     loginLoading: false,
     registerLoading: false,
@@ -50,7 +53,7 @@ export class Login extends Component<any, State> {
   }
 
   componentDidMount() {
-    document.title = "Login - Lemmy";
+    document.title = `${i18n.t('login')} - ${WebSocketService.Instance.site.name}`;
   }
 
   render() {
@@ -74,13 +77,13 @@ export class Login extends Component<any, State> {
         <form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
           <h5>Login</h5>
           <div class="form-group row">
-            <label class="col-sm-2 col-form-label">Email or Username</label>
+            <label class="col-sm-2 col-form-label"><T i18nKey="email_or_username">#</T></label>
             <div class="col-sm-10">
               <input type="text" class="form-control" value={this.state.loginForm.username_or_email} onInput={linkEvent(this, this.handleLoginUsernameChange)} required minLength={3} />
             </div>
           </div>
           <div class="form-group row">
-            <label class="col-sm-2 col-form-label">Password</label>
+            <label class="col-sm-2 col-form-label"><T i18nKey="password">#</T></label>
             <div class="col-sm-10">
               <input type="password" value={this.state.loginForm.password} onInput={linkEvent(this, this.handleLoginPasswordChange)} class="form-control" required />
             </div>
@@ -88,47 +91,53 @@ export class Login extends Component<any, State> {
           <div class="form-group row">
             <div class="col-sm-10">
               <button type="submit" class="btn btn-secondary">{this.state.loginLoading ? 
-              <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 'Login'}</button>
+              <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('login')}</button>
             </div>
           </div>
         </form>
-        {/* Forgot your password or deleted your account? Reset your password. TODO */}
       </div>
     );
   }
   registerForm() {
     return (
       <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
-        <h5>Sign Up</h5>
+        <h5><T i18nKey="sign_up">#</T></h5>
         <div class="form-group row">
-          <label class="col-sm-2 col-form-label">Username</label>
+          <label class="col-sm-2 col-form-label"><T i18nKey="username">#</T></label>
           <div class="col-sm-10">
             <input type="text" class="form-control" value={this.state.registerForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} maxLength={20} pattern="[a-zA-Z0-9_]+" />
           </div>
         </div>
         <div class="form-group row">
-          <label class="col-sm-2 col-form-label">Email</label>
+          <label class="col-sm-2 col-form-label"><T i18nKey="email">#</T></label>
           <div class="col-sm-10">
-            <input type="email" class="form-control" placeholder="Optional" value={this.state.registerForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} />
+            <input type="email" class="form-control" placeholder={i18n.t('optional')} value={this.state.registerForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} />
           </div>
         </div>
         <div class="form-group row">
-          <label class="col-sm-2 col-form-label">Password</label>
+          <label class="col-sm-2 col-form-label"><T i18nKey="password">#</T></label>
           <div class="col-sm-10">
             <input type="password" value={this.state.registerForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required />
           </div>
         </div>
         <div class="form-group row">
-          <label class="col-sm-2 col-form-label">Verify Password</label>
+          <label class="col-sm-2 col-form-label"><T i18nKey="verify_password">#</T></label>
           <div class="col-sm-10">
             <input type="password" value={this.state.registerForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required />
           </div>
         </div>
+        <div class="form-group row">
+          <div class="col-sm-10">
+            <div class="form-check">
+              <input class="form-check-input" type="checkbox" checked={this.state.registerForm.show_nsfw} onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}/>
+              <label class="form-check-label"><T i18nKey="show_nsfw">#</T></label>
+            </div>
+          </div>
+        </div>
         <div class="form-group row">
           <div class="col-sm-10">
             <button type="submit" class="btn btn-secondary">{this.state.registerLoading ? 
-            <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 'Sign Up'}</button>
-
+            <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('sign_up')}</button>
           </div>
         </div>
       </form>
@@ -180,10 +189,15 @@ export class Login extends Component<any, State> {
     i.setState(i.state);
   }
 
+  handleRegisterShowNsfwChange(i: Login, event: any) {
+    i.state.registerForm.show_nsfw = event.target.checked;
+    i.setState(i.state);
+  }
+
   parseMessage(msg: any) {
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
-      alert(msg.error);
+      alert(i18n.t(msg.error));
       this.state = this.emptyState;
       this.setState(this.state);
       return;
index ac43b1d9f88b5f3e41b5a7bfbcc2eb20a21b8c2d..0a7a31e5928128cb141c2c713352966e79c637a4 100644 (file)
@@ -7,6 +7,8 @@ import { WebSocketService, UserService } from '../services';
 import { PostListings } from './post-listings';
 import { SiteForm } from './site-form';
 import { msgOp, repoUrl, mdToHtml, fetchLimit, routeSortTypeToEnum, routeListingTypeToEnum } from '../utils';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 interface MainState {
   subscribedCommunities: Array<CommunityUser>;
@@ -37,6 +39,7 @@ export class Main extends Component<any, MainState> {
         number_of_users: null,
         number_of_posts: null,
         number_of_comments: null,
+        number_of_communities: null,
       },
       admins: [],
       banned: [],
@@ -71,6 +74,7 @@ export class Main extends Component<any, MainState> {
     super(props, context);
 
     this.state = this.emptyState;
+    this.handleEditCancel = this.handleEditCancel.bind(this);
 
     this.subscription = WebSocketService.Instance.subject
     .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
@@ -100,10 +104,6 @@ export class Main extends Component<any, MainState> {
     this.subscription.unsubscribe();
   }
 
-  componentDidMount() {
-    document.title = "Lemmy";
-  }
-
   // Necessary for back button for some reason
   componentWillReceiveProps(nextProps: any) {
     if (nextProps.history.action == 'POP') {
@@ -123,12 +123,26 @@ export class Main extends Component<any, MainState> {
             {this.posts()}
           </div>
           <div class="col-12 col-md-4">
-            {!this.state.loading &&
-              <div>
+            {this.my_sidebar()}
+          </div>
+        </div>
+      </div>
+    )
+  }
+    
+  my_sidebar() {
+    return(
+      <div>
+        {!this.state.loading &&
+          <div>
+            <div class="card border-secondary mb-3">
+              <div class="card-body">
                 {this.trendingCommunities()}
                 {UserService.Instance.user && this.state.subscribedCommunities.length > 0 && 
                   <div>
-                    <h5>Subscribed <Link class="text-white" to="/communities">communities</Link></h5> 
+                    <h5>
+                      <T i18nKey="subscribed_to_communities">#<Link class="text-white" to="/communities">#</Link></T>
+                    </h5> 
                     <ul class="list-inline"> 
                       {this.state.subscribedCommunities.map(community =>
                         <li class="list-inline-item"><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
@@ -136,13 +150,16 @@ export class Main extends Component<any, MainState> {
                     </ul>
                   </div>
                 }
-                <Link class="btn btn-sm btn-secondary btn-block mb-3" 
-                  to="/create_community">Create a Community</Link>
-                {this.sidebar()}
+                <Link class="btn btn-sm btn-secondary btn-block" 
+                  to="/create_community">
+                  <T i18nKey="create_a_community">#</T>
+                </Link>
               </div>
-            }
+            </div>
+            {this.sidebar()}
+            {this.landing()}
           </div>
-        </div>
+        }
       </div>
     )
   }
@@ -150,7 +167,9 @@ export class Main extends Component<any, MainState> {
   trendingCommunities() {
     return (
       <div>
-        <h5>Trending <Link class="text-white" to="/communities">communities</Link></h5> 
+        <h5>
+          <T i18nKey="trending_communities">#<Link class="text-white" to="/communities">#</Link></T>
+        </h5>
         <ul class="list-inline"> 
           {this.state.trendingCommunities.map(community =>
             <li class="list-inline-item"><Link to={`/c/${community.name}`}>{community.name}</Link></li>
@@ -170,7 +189,6 @@ export class Main extends Component<any, MainState> {
             onCancel={this.handleEditCancel} 
           />
         }
-        {this.landing()}
       </div>
     )
   }
@@ -184,50 +202,73 @@ export class Main extends Component<any, MainState> {
   siteInfo() {
     return (
       <div>
-        <h5 class="mb-0">{`${this.state.site.site.name}`}</h5>
-        {this.canAdmin && 
-          <ul class="list-inline mb-1 text-muted small font-weight-bold"> 
-            <li className="list-inline-item">
-              <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
-            </li>
-          </ul>
-        }
-        <ul class="my-2 list-inline">
-          <li className="list-inline-item badge badge-light">{this.state.site.site.number_of_users} Users</li>
-          <li className="list-inline-item badge badge-light">{this.state.site.site.number_of_posts} Posts</li>
-          <li className="list-inline-item badge badge-light">{this.state.site.site.number_of_comments} Comments</li>
-          <li className="list-inline-item"><Link className="badge badge-light" to="/modlog">Modlog</Link></li>
-        </ul>
-        <ul class="my-1 list-inline small"> 
-          <li class="list-inline-item">admins: </li>
-          {this.state.site.admins.map(admin =>
-            <li class="list-inline-item"><Link class="text-info" to={`/u/${admin.name}`}>{admin.name}</Link></li>
-          )}
-        </ul>
-        {this.state.site.site.description && 
-          <div>
-            <hr />
-            <div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.site.site.description)} />
-            <hr />
+        <div class="card border-secondary mb-3">
+          <div class="card-body">
+            <h5 class="mb-0">{`${this.state.site.site.name}`}</h5>
+            {this.canAdmin && 
+              <ul class="list-inline mb-1 text-muted small font-weight-bold"> 
+                <li className="list-inline-item">
+                  <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>
+                    <T i18nKey="edit">#</T>
+                  </span>
+                </li>
+              </ul>
+            }
+            <ul class="my-2 list-inline">
+              <li className="list-inline-item badge badge-secondary">
+                <T i18nKey="number_of_users" interpolation={{count: this.state.site.site.number_of_users}}>#</T>
+              </li>
+              <li className="list-inline-item badge badge-secondary">
+                <T i18nKey="number_of_communities" interpolation={{count: this.state.site.site.number_of_communities}}>#</T>
+              </li>
+              <li className="list-inline-item badge badge-secondary">
+                <T i18nKey="number_of_posts" interpolation={{count: this.state.site.site.number_of_posts}}>#</T>
+              </li>
+              <li className="list-inline-item badge badge-secondary">
+                <T i18nKey="number_of_comments" interpolation={{count: this.state.site.site.number_of_comments}}>#</T>
+              </li>
+              <li className="list-inline-item">
+                <Link className="badge badge-secondary" to="/modlog">
+                  <T i18nKey="modlog">#</T>
+                </Link>
+              </li>
+            </ul>
+            <ul class="mt-1 list-inline small mb-0"> 
+              <li class="list-inline-item">
+                <T i18nKey="admins" class="d-inline">#</T>:
+                </li>
+                {this.state.site.admins.map(admin =>
+                  <li class="list-inline-item"><Link class="text-info" to={`/u/${admin.name}`}>{admin.name}</Link></li>
+                )}
+              </ul>
+            </div>
           </div>
-        }
-      </div>
+          {this.state.site.site.description && 
+            <div class="card border-secondary mb-3">
+              <div class="card-body">
+                <div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.site.site.description)} />
+              </div>
+            </div>
+          }
+        </div>
     )
   }
 
   landing() {
     return (
-      <div>
-        <h5>Powered by  
-          <svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg>
-          <a href={repoUrl}>Lemmy<sup>Beta</sup></a>
-        </h5>
-        <p>Lemmy is a <a href="https://en.wikipedia.org/wiki/Link_aggregation">link aggregator</a> / reddit alternative, intended to work in the <a href="https://en.wikipedia.org/wiki/Fediverse">fediverse</a>.</p>
-        <p>Its self-hostable, has live-updating comment threads, and is tiny (<code>~80kB</code>). Federation into the ActivityPub network is on the roadmap.</p>
-        <p>This is a <b>very early beta version</b>, and a lot of features are currently broken or missing.</p>
-        <p>Suggest new features or report bugs <a href={repoUrl}>here.</a></p>
-        <p>Made with <a href="https://www.rust-lang.org">Rust</a>, <a href="https://actix.rs/">Actix</a>, <a href="https://www.infernojs.org">Inferno</a>, <a href="https://www.typescriptlang.org/">Typescript</a>.</p>
+      <div class="card border-secondary">
+        <div class="card-body">
+          <h5>
+            <T i18nKey="powered_by" class="d-inline">#</T>
+            <svg class="icon mx-2"><use xlinkHref="#icon-mouse">#</use></svg>
+            <a href={repoUrl}>Lemmy<sup>beta</sup></a>
+          </h5>
+          <p class="mb-0">
+            <T i18nKey="landing_0">#<a href="https://en.wikipedia.org/wiki/Link_aggregation">#</a><a href="https://en.wikipedia.org/wiki/Fediverse">#</a><br></br><code>#</code><br></br><b>#</b><br></br><a href={repoUrl}>#</a><br></br><a href="https://www.rust-lang.org">#</a><a href="https://actix.rs/">#</a><a href="https://infernojs.org">#</a><a href="https://www.typescriptlang.org/">#</a>
+          </T>
+        </p>
       </div>
+    </div>
     )
   }
 
@@ -248,7 +289,7 @@ export class Main extends Component<any, MainState> {
 
   selects() {
     return (
-      <div className="mb-2">
+      <div className="mb-3">
         <div class="btn-group btn-group-toggle">
           <label className={`btn btn-sm btn-secondary 
             ${this.state.type_ == ListingType.Subscribed && 'active'}
@@ -260,7 +301,7 @@ export class Main extends Component<any, MainState> {
               onChange={linkEvent(this, this.handleTypeChange)}
               disabled={UserService.Instance.user == undefined}
             />
-            Subscribed
+            {i18n.t('subscribed')}
           </label>
           <label className={`pointer btn btn-sm btn-secondary ${this.state.type_ == ListingType.All && 'active'}`}>
             <input type="radio" 
@@ -268,19 +309,19 @@ export class Main extends Component<any, MainState> {
               checked={this.state.type_ == ListingType.All}
               onChange={linkEvent(this, this.handleTypeChange)}
             /> 
-            All
+            {i18n.t('all')}
           </label>
         </div>
         <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="ml-2 custom-select custom-select-sm w-auto">
-          <option disabled>Sort Type</option>
-          <option value={SortType.Hot}>Hot</option>
-          <option value={SortType.New}>New</option>
-          <option disabled>──────────</option>
-          <option value={SortType.TopDay}>Top Day</option>
-          <option value={SortType.TopWeek}>Week</option>
-          <option value={SortType.TopMonth}>Month</option>
-          <option value={SortType.TopYear}>Year</option>
-          <option value={SortType.TopAll}>All</option>
+          <option disabled><T i18nKey="sort_type">#</T></option>
+          <option value={SortType.Hot}><T i18nKey="hot">#</T></option>
+          <option value={SortType.New}><T i18nKey="new">#</T></option>
+          <option disabled>─────</option>
+          <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
+          <option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
+          <option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
+          <option value={SortType.TopYear}><T i18nKey="year">#</T></option>
+          <option value={SortType.TopAll}><T i18nKey="all">#</T></option>
         </select>
       </div>
     )
@@ -288,11 +329,11 @@ export class Main extends Component<any, MainState> {
 
   paginator() {
     return (
-      <div class="mt-2">
+      <div class="my-2">
         {this.state.page > 1 && 
-          <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
+          <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
         }
-        <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
+        <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
       </div>
     );
   }
@@ -355,7 +396,7 @@ export class Main extends Component<any, MainState> {
     console.log(msg);
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
-      alert(msg.error);
+      alert(i18n.t(msg.error));
       return;
     } else if (op == UserOperation.GetFollowedCommunities) {
       let res: GetFollowedCommunitiesResponse = msg;
@@ -376,6 +417,8 @@ export class Main extends Component<any, MainState> {
       this.state.site.site = res.site;
       this.state.site.banned = res.banned;
       this.setState(this.state);
+      document.title = `${WebSocketService.Instance.site.name}`;
+
     } else if (op == UserOperation.EditSite) {
       let res: SiteResponse = msg;
       this.state.site.site = res.site;
index 894887ae1b2dd95dcd644fbca757fd63ab0661a8..ba1fe5a21d49cb0f3885831664ddc77219203ef5 100644 (file)
@@ -45,7 +45,7 @@ export class Modlog extends Component<any, ModlogState> {
   }
 
   componentDidMount() {
-    document.title = "Modlog - Lemmy";
+    document.title = `Modlog - ${WebSocketService.Instance.site.name}`;
   }
 
   setCombined(res: GetModlogResponse) {
@@ -223,7 +223,7 @@ export class Modlog extends Component<any, ModlogState> {
     console.log(msg);
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
-      alert(msg.error);
+      alert(i18n.t(msg.error));
       return;
     } else if (op == UserOperation.GetModlog) {
       let res: GetModlogResponse = msg;
index c882669530e30103b8055e02a911f34bab776bd3..c6cd981b32581a9f5e402931482124a440487e14 100644 (file)
@@ -1,5 +1,7 @@
 import { Component } from 'inferno';
 import * as moment from 'moment';
+import { getMomentLanguage } from '../utils';
+import { i18n } from '../i18next';
 
 interface MomentTimeProps {
   data: {
@@ -13,12 +15,16 @@ export class MomentTime extends Component<MomentTimeProps, any> {
 
   constructor(props: any, context: any) {
     super(props, context);
+
+    let lang = getMomentLanguage();
+
+    moment.locale(lang);
   }
 
   render() {
     if (this.props.data.updated) {
       return (
-        <span title={this.props.data.updated} className="font-italics">modified {moment.utc(this.props.data.updated).fromNow()}</span>
+        <span title={this.props.data.updated} className="font-italics">{i18n.t('modified')} {moment.utc(this.props.data.updated).fromNow()}</span>
       )
     } else {
       let str = this.props.data.published || this.props.data.when_;
index 68e486c1f195a38c46754cac80fbae067be4c325..593301683b60954586aada1dac53c2e891dad045 100644 (file)
@@ -6,6 +6,8 @@ import { WebSocketService, UserService } from '../services';
 import { UserOperation, GetRepliesForm, GetRepliesResponse, SortType, GetSiteResponse, Comment} from '../interfaces';
 import { msgOp } from '../utils';
 import { version } from '../version';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 interface NavbarState {
   isLoggedIn: boolean;
@@ -74,9 +76,8 @@ export class Navbar extends Component<any, NavbarState> {
   // TODO class active corresponding to current page
   navbar() {
     return (
-      <nav class="container navbar navbar-expand-md navbar-light navbar-bg p-0 px-3">
+      <nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3">
         <Link title={version} class="navbar-brand" to="/">
-          <svg class="icon mr-2 mouse-icon"><use xlinkHref="#icon-mouse"></use></svg>
           {this.state.siteName}
         </Link>
         <button class="navbar-toggler" type="button" onClick={linkEvent(this, this.expandNavbar)}>
@@ -85,16 +86,16 @@ export class Navbar extends Component<any, NavbarState> {
         <div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}>
           <ul class="navbar-nav mr-auto">
             <li class="nav-item">
-              <Link class="nav-link" to="/communities">Communities</Link>
+              <Link class="nav-link" to="/communities"><T i18nKey="communities">#</T></Link>
             </li>
             <li class="nav-item">
-              <Link class="nav-link" to="/search">Search</Link>
+              <Link class="nav-link" to="/search"><T i18nKey="search">#</T></Link>
             </li>
             <li class="nav-item">
-              <Link class="nav-link" to={{pathname: '/create_post', state: { prevPath: this.currentLocation }}}>Create Post</Link>
+              <Link class="nav-link" to={{pathname: '/create_post', state: { prevPath: this.currentLocation }}}><T i18nKey="create_post">#</T></Link>
             </li>
             <li class="nav-item">
-              <Link class="nav-link" to="/create_community">Create Community</Link>
+              <Link class="nav-link" to="/create_community"><T i18nKey="create_community">#</T></Link>
             </li>
           </ul>
           <ul class="navbar-nav ml-auto mr-2">
@@ -102,7 +103,7 @@ export class Navbar extends Component<any, NavbarState> {
             <>
               {
                 <li className="nav-item">
-                  <Link class="inbox nav-link" to="/inbox">
+                  <Link class="nav-link" to="/inbox">
                     <svg class="icon"><use xlinkHref="#icon-mail"></use></svg>
                     {this.state.unreadCount> 0 && <span class="ml-1 badge badge-light">{this.state.unreadCount}</span>}
                   </Link>
@@ -113,13 +114,13 @@ export class Navbar extends Component<any, NavbarState> {
                   {UserService.Instance.user.username}
                 </a>
                 <div className={`dropdown-menu dropdown-menu-right ${this.state.expandUserDropdown && 'show'}`}>
-                  <a role="button" class="dropdown-item pointer" onClick={linkEvent(this, this.handleOverviewClick)}>Overview</a>
-                  <a role="button" class="dropdown-item pointer" onClick={ linkEvent(this, this.handleLogoutClick) }>Logout</a>
+                  <a role="button" class="dropdown-item pointer" onClick={linkEvent(this, this.handleOverviewClick)}><T i18nKey="overview">#</T></a>
+                  <a role="button" class="dropdown-item pointer" onClick={ linkEvent(this, this.handleLogoutClick) }><T i18nKey="logout">#</T></a>
                 </div>
               </li> 
             </>
               : 
-              <Link class="nav-link" to="/login">Login / Sign up</Link>
+              <Link class="nav-link" to="/login"><T i18nKey="login_sign_up">#</T></Link>
             }
           </ul>
         </div>
@@ -153,7 +154,7 @@ export class Navbar extends Component<any, NavbarState> {
   parseMessage(msg: any) {
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
-      if (msg.error == "Not logged in.") {
+      if (msg.error == "not_logged_in") {
         UserService.Instance.logout();
         location.reload();
       }
@@ -209,7 +210,7 @@ export class Navbar extends Component<any, NavbarState> {
     if (UserService.Instance.user) {
     document.addEventListener('DOMContentLoaded', function () {
       if (!Notification) {
-        alert('Desktop notifications not available in your browser. Try Chromium.'); 
+        alert(i18n.t('notifications_error')); 
         return;
       }
 
@@ -224,7 +225,7 @@ export class Navbar extends Component<any, NavbarState> {
     if (Notification.permission !== 'granted')
       Notification.requestPermission();
     else {
-      var notification = new Notification(`${replies.length} Unread Messages`, {
+      var notification = new Notification(`${replies.length} ${i18n.t('unread_messages')}`, {
         icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
         body: `${recentReply.creator_name}: ${recentReply.content}`
       });
index 50b4acd1c5f997d5fa854886e255e178f3f0e258..f502e7f3ef1ee093b883ffcad5218d6077c31236 100644 (file)
@@ -1,14 +1,17 @@
 import { Component, linkEvent } from 'inferno';
+import { PostListings } from './post-listings';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType } from '../interfaces';
+import { PostForm as PostFormI, PostFormParams, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType, SearchForm, SearchType, SearchResponse } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
-import { msgOp, getPageTitle } from '../utils';
+import { msgOp, getPageTitle, debounce, validURL, capitalizeFirstLetter } from '../utils';
 import * as autosize from 'autosize';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 interface PostFormProps {
   post?: Post; // If a post is given, that means this is an edit
-  prevCommunityName?: string;
+  params?: PostFormParams;
   onCancel?(): any;
   onCreate?(id: number): any;
   onEdit?(post: Post): any;
@@ -19,6 +22,8 @@ interface PostFormState {
   communities: Array<Community>;
   loading: boolean;
   suggestedTitle: string;
+  suggestedPosts: Array<Post>;
+  crossPosts: Array<Post>;
 }
 
 export class PostForm extends Component<PostFormProps, PostFormState> {
@@ -27,6 +32,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
   private emptyState: PostFormState = {
     postForm: {
       name: null,
+      nsfw: false,
       auth: null,
       community_id: null,
       creator_id: (UserService.Instance.user) ? UserService.Instance.user.id : null,
@@ -34,6 +40,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     communities: [],
     loading: false,
     suggestedTitle: undefined,
+    suggestedPosts: [],
+    crossPosts: [],
   }
 
   constructor(props: any, context: any) {
@@ -49,24 +57,35 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
         edit_id: this.props.post.id,
         creator_id: this.props.post.creator_id,
         url: this.props.post.url,
+        nsfw: this.props.post.nsfw,
         auth: null
       }
     }
 
+    if (this.props.params) {
+      this.state.postForm.name = this.props.params.name;
+      if (this.props.params.url) {
+        this.state.postForm.url = this.props.params.url;
+      }
+      if (this.props.params.body) {
+        this.state.postForm.body = this.props.params.body;
+      }
+    }
+
     this.subscription = WebSocketService.Instance.subject
-      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
-      .subscribe(
-        (msg) => this.parseMessage(msg),
+    .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+    .subscribe(
+      (msg) => this.parseMessage(msg),
         (err) => console.error(err),
         () => console.log('complete')
-      );
+    );
 
-      let listCommunitiesForm: ListCommunitiesForm = {
-        sort: SortType[SortType.TopAll],
-        limit: 9999,
-      }
+    let listCommunitiesForm: ListCommunitiesForm = {
+      sort: SortType[SortType.TopAll],
+      limit: 9999,
+    }
 
-      WebSocketService.Instance.listCommunities(listCommunitiesForm);
+    WebSocketService.Instance.listCommunities(listCommunitiesForm);
   }
 
   componentDidMount() {
@@ -82,30 +101,41 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       <div>
         <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
           <div class="form-group row">
-            <label class="col-sm-2 col-form-label">URL</label>
+            <label class="col-sm-2 col-form-label"><T i18nKey="url">#</T></label>
             <div class="col-sm-10">
               <input type="url" class="form-control" value={this.state.postForm.url} onInput={linkEvent(this, this.handlePostUrlChange)} />
               {this.state.suggestedTitle && 
-                <span class="text-muted small font-weight-bold pointer" onClick={linkEvent(this, this.copySuggestedTitle)}>copy suggested title: {this.state.suggestedTitle}</span>
+                <div class="mt-1 text-muted small font-weight-bold pointer" onClick={linkEvent(this, this.copySuggestedTitle)}><T i18nKey="copy_suggested_title" interpolation={{title: this.state.suggestedTitle}}>#</T></div>
+              }
+              {this.state.crossPosts.length > 0 && 
+                <>
+                  <div class="my-1 text-muted small font-weight-bold"><T i18nKey="cross_posts">#</T></div>
+                  <PostListings showCommunity posts={this.state.crossPosts} />
+                </>
               }
             </div>
           </div>
           <div class="form-group row">
-            <label class="col-sm-2 col-form-label">Title</label>
+            <label class="col-sm-2 col-form-label"><T i18nKey="title">#</T></label>
             <div class="col-sm-10">
               <textarea value={this.state.postForm.name} onInput={linkEvent(this, this.handlePostNameChange)} class="form-control" required rows={2} minLength={3} maxLength={100} />
+              {this.state.suggestedPosts.length > 0 && 
+                <>
+                  <div class="my-1 text-muted small font-weight-bold"><T i18nKey="related_posts">#</T></div>
+                  <PostListings posts={this.state.suggestedPosts} />
+                </>
+              }
             </div>
           </div>
           <div class="form-group row">
-            <label class="col-sm-2 col-form-label">Body</label>
+            <label class="col-sm-2 col-form-label"><T i18nKey="body">#</T></label>
             <div class="col-sm-10">
               <textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} maxLength={10000} />
             </div>
           </div>
-          {/* Cant change a community from an edit */}
           {!this.props.post &&
             <div class="form-group row">
-            <label class="col-sm-2 col-form-label">Community</label>
+            <label class="col-sm-2 col-form-label"><T i18nKey="community">#</T></label>
             <div class="col-sm-10">
               <select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}>
                 {this.state.communities.map(community =>
@@ -115,13 +145,21 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
             </div>
             </div>
             }
+          <div class="form-group row">
+            <div class="col-sm-10">
+              <div class="form-check">
+                <input class="form-check-input" type="checkbox" checked={this.state.postForm.nsfw} onChange={linkEvent(this, this.handlePostNsfwChange)}/>
+                <label class="form-check-label"><T i18nKey="nsfw">#</T></label>
+              </div>
+            </div>
+          </div>
           <div class="form-group row">
             <div class="col-sm-10">
               <button type="submit" class="btn btn-secondary mr-2">
               {this.state.loading ? 
               <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 
-              this.props.post ? 'Save' : 'Create'}</button>
-              {this.props.post && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>}
+              this.props.post ? capitalizeFirstLetter(i18n.t('save')) : capitalizeFirstLetter(i18n.t('create'))}</button>
+              {this.props.post && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}><T i18nKey="cancel">#</T></button>}
             </div>
           </div>
         </form>
@@ -148,15 +186,48 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
 
   handlePostUrlChange(i: PostForm, event: any) {
     i.state.postForm.url = event.target.value;
-    getPageTitle(i.state.postForm.url).then(d => {
-      i.state.suggestedTitle = d;
-      i.setState(i.state);
-    });
+    if (validURL(i.state.postForm.url)) {
+
+      let form: SearchForm = {
+        q: i.state.postForm.url,
+        type_: SearchType[SearchType.Url],
+        sort: SortType[SortType.TopAll],
+        page: 1,
+        limit: 6,
+      };
+
+      WebSocketService.Instance.search(form);
+
+      // Fetch the page title
+      getPageTitle(i.state.postForm.url).then(d => {
+        i.state.suggestedTitle = d;
+        i.setState(i.state);
+      });
+    } else {
+      i.state.suggestedTitle = undefined;
+      i.state.crossPosts = [];
+    }
+
     i.setState(i.state);
   }
 
   handlePostNameChange(i: PostForm, event: any) {
     i.state.postForm.name = event.target.value;
+    let form: SearchForm = {
+      q: i.state.postForm.name,
+      type_: SearchType[SearchType.Posts],
+      sort: SortType[SortType.TopAll],
+      community_id: i.state.postForm.community_id,
+      page: 1,
+      limit: 6,
+    };
+
+    if (i.state.postForm.name !== '') {
+      WebSocketService.Instance.search(form);
+    } else {
+      i.state.suggestedPosts = [];
+    }
+
     i.setState(i.state);
   }
 
@@ -170,6 +241,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     i.setState(i.state);
   }
 
+  handlePostNsfwChange(i: PostForm, event: any) {
+    i.state.postForm.nsfw = event.target.checked;
+    i.setState(i.state);
+  }
+
   handleCancel(i: PostForm) {
     i.props.onCancel();
   }
@@ -177,7 +253,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
   parseMessage(msg: any) {
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
-      alert(msg.error);
+      alert(i18n.t(msg.error));
       this.state.loading = false;
       this.setState(this.state);
       return;
@@ -186,8 +262,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       this.state.communities = res.communities;
       if (this.props.post) {
         this.state.postForm.community_id = this.props.post.community_id;
-      } else if (this.props.prevCommunityName) {
-        let foundCommunityId = res.communities.find(r => r.name == this.props.prevCommunityName).id;
+      } else if (this.props.params && this.props.params.community) {
+        let foundCommunityId = res.communities.find(r => r.name == this.props.params.community).id;
         this.state.postForm.community_id = foundCommunityId;
       } else {
         this.state.postForm.community_id = res.communities[0].id;
@@ -201,6 +277,15 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       this.state.loading = false;
       let res: PostResponse = msg;
       this.props.onEdit(res.post);
+    } else if (op == UserOperation.Search) {
+      let res: SearchResponse = msg;
+      
+      if (res.type_ == SearchType[SearchType.Posts]) {
+        this.state.suggestedPosts = res.posts;
+      } else if (res.type_ == SearchType[SearchType.Url]) {
+        this.state.crossPosts = res.posts;
+      }
+      this.setState(this.state);
     }
   }
 
index 0fec5ceb39fc788ec85a507b639cae9cc3a80f24..e3b6e76a96df26b3ff06944bcbf530abe06aaa6e 100644 (file)
@@ -5,6 +5,8 @@ import { Post, CreatePostLikeForm, PostForm as PostFormI, SavePostForm, Communit
 import { MomentTime } from './moment-time';
 import { PostForm } from './post-form';
 import { mdToHtml, canMod, isMod, isImage } from '../utils';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 interface PostListingState {
   showEdit: boolean;
@@ -44,7 +46,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   render() {
     return (
-      <div>
+      <div class="row">
         {!this.state.showEdit 
           ? this.listing()
           : <PostForm post={this.props.post} onEdit={this.handleEditPost} onCancel={this.handleEditCancel}/>
@@ -56,25 +58,25 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   listing() {
     let post = this.props.post;
     return (
-      <div class="listing">
-        <div className={`mr-1 float-left small text-center ${this.props.viewOnly && 'no-click'}`}>
-          <div className={`pointer ${post.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostLike)}>
+      <div class="listing col-12">
+        <div className={`vote-bar mr-2 float-left small text-center ${this.props.viewOnly && 'no-click'}`}>
+          <button className={`btn p-0 ${post.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostLike)}>
             <svg class="icon upvote"><use xlinkHref="#icon-arrow-up"></use></svg>
-          </div>
+          </button>
           <div class={`font-weight-bold text-muted`}>{post.score}</div>
-          <div className={`pointer ${post.my_vote == -1 ? 'text-danger' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostDisLike)}>
+          <button className={`btn p-0 ${post.my_vote == -1 ? 'text-danger' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostDisLike)}>
             <svg class="icon downvote"><use xlinkHref="#icon-arrow-down"></use></svg>
-          </div>
+          </button>
         </div>
         {post.url && isImage(post.url) &&
-          <span title="Expand here" class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)}><img class="mx-2 float-left img-fluid thumbnail rounded" src={post.url} /></span>
+          <span title={i18n.t('expand_here')} class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)}><img class="mx-2 mt-1 float-left img-fluid thumbnail rounded" src={post.url} /></span>
         }
         <div className="ml-4">
-          <div>
+          <div className="post-title">
             <h5 className="mb-0 d-inline">
               {post.url ? 
               <a className="text-white" href={post.url} target="_blank" title={post.url}>{post.name}</a> : 
-              <Link className="text-white" to={`/post/${post.id}`} title="Comments">{post.name}</Link>
+              <Link className="text-white" to={`/post/${post.id}`} title={i18n.t('comments')}>{post.name}</Link>
               }
             </h5>
             {post.url && 
@@ -82,22 +84,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 <a className="ml-2 text-muted font-italic" href={post.url} target="_blank" title={post.url}>{(new URL(post.url)).hostname}</a>
               </small>
             }
-            {post.removed &&
-              <small className="ml-2 text-muted font-italic">removed</small>
-            }
-            {post.deleted &&
-              <small className="ml-2 text-muted font-italic">deleted</small>
-            }
-            {post.locked &&
-              <small className="ml-2 text-muted font-italic">locked</small>
-            }
             { post.url && isImage(post.url) && 
               <>
                 { !this.state.imageExpanded
-                  ? <span class="badge badge-light pointer ml-2 text-muted small" title="Expand here" onClick={linkEvent(this, this.handleImageExpandClick)}>+</span>
+                  ? <span class="text-monospace pointer ml-2 text-muted small" title={i18n.t('expand_here')} onClick={linkEvent(this, this.handleImageExpandClick)}>[+]</span>
                   : 
                   <span>
-                    <span class="pointer ml-2 badge badge-light text-muted small" onClick={linkEvent(this, this.handleImageExpandClick)}>-</span>
+                    <span class="text-monospace pointer ml-2 text-muted small" onClick={linkEvent(this, this.handleImageExpandClick)}>[-]</span>
                     <div>
                       <span class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)}><img class="img-fluid" src={post.url} /></span>
                     </div>
@@ -105,22 +98,34 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 }
               </>
             }
+            {post.removed &&
+              <small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
+            }
+            {post.deleted &&
+              <small className="ml-2 text-muted font-italic"><T i18nKey="deleted">#</T></small>
+            }
+            {post.locked &&
+              <small className="ml-2 text-muted font-italic"><T i18nKey="locked">#</T></small>
+            }
+            {post.nsfw &&
+              <small className="ml-2 text-muted font-italic"><T i18nKey="nsfw">#</T></small>
+            }
           </div>
         </div>
-        <div className="details ml-4 mb-1">
+        <div className="details ml-4">
           <ul class="list-inline mb-0 text-muted small">
             <li className="list-inline-item">
-              <span>by </span>
+              <span>{i18n.t('by')} </span>
               <Link className="text-info" to={`/u/${post.creator_name}`}>{post.creator_name}</Link>
               {this.isMod && 
-                <span className="mx-1 badge badge-light">mod</span>
+                <span className="mx-1 badge badge-light"><T i18nKey="mod">#</T></span>
               }
               {this.isAdmin && 
-                <span className="mx-1 badge badge-light">admin</span>
+                <span className="mx-1 badge badge-light"><T i18nKey="admin">#</T></span>
               }
               {this.props.showCommunity && 
                 <span>
-                  <span> to </span>
+                  <span> {i18n.t('to')} </span>
                   <Link to={`/c/${post.community_name}`}>{post.community_name}</Link>
                 </span>
               }
@@ -137,22 +142,25 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               </span>
             </li>
             <li className="list-inline-item">
-              <Link className="text-muted" to={`/post/${post.id}`}>{post.number_of_comments} Comments</Link>
+              <Link className="text-muted" to={`/post/${post.id}`}><T i18nKey="number_of_comments" interpolation={{count: post.number_of_comments}}>#</T></Link>
             </li>
           </ul>
           {UserService.Instance.user && this.props.editable &&
             <ul class="list-inline mb-1 text-muted small font-weight-bold"> 
               <li className="list-inline-item mr-2">
-                <span class="pointer" onClick={linkEvent(this, this.handleSavePostClick)}>{post.saved ? 'unsave' : 'save'}</span>
+                <span class="pointer" onClick={linkEvent(this, this.handleSavePostClick)}>{post.saved ? i18n.t('unsave') : i18n.t('save')}</span>
+              </li>
+              <li className="list-inline-item mr-2">
+                <Link className="text-muted" to={`/create_post${this.crossPostParams}`}><T i18nKey="cross_post">#</T></Link>
               </li>
               {this.myPost && 
                 <>
                   <li className="list-inline-item">
-                    <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
+                    <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
                   </li>
                   <li className="list-inline-item mr-2">
                     <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
-                      {!post.deleted ? 'delete' : 'restore'}
+                      {!post.deleted ? i18n.t('delete') : i18n.t('restore')}
                     </span>
                   </li>
                 </>
@@ -161,12 +169,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 <span>
                   <li className="list-inline-item">
                     {!this.props.post.removed ? 
-                    <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
-                    <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
+                    <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
+                    <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
                     }
                   </li>
                   <li className="list-inline-item">
-                    <span class="pointer" onClick={linkEvent(this, this.handleModLock)}>{this.props.post.locked ? 'unlock' : 'lock'}</span>
+                    <span class="pointer" onClick={linkEvent(this, this.handleModLock)}>{this.props.post.locked ? i18n.t('unlock') : i18n.t('lock')}</span>
                   </li>
                 </span>
               }
@@ -174,8 +182,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           }
           {this.state.showRemoveDialog && 
             <form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
-              <input type="text" class="form-control mr-2" placeholder="Reason" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
-              <button type="submit" class="btn btn-secondary">Remove Post</button>
+              <input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
+              <button type="submit" class="btn btn-secondary"><T i18nKey="remove_post">#</T></button>
             </form>
           }
           {this.props.showBody && this.props.post.body && <div className="md-div" dangerouslySetInnerHTML={mdToHtml(post.body)} />}
@@ -249,6 +257,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       edit_id: i.props.post.id,
       creator_id: i.props.post.creator_id,
       deleted: !i.props.post.deleted,
+      nsfw: i.props.post.nsfw,
       auth: null
     };
     WebSocketService.Instance.editPost(deleteForm);
@@ -264,6 +273,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     WebSocketService.Instance.savePost(form);
   }
 
+  get crossPostParams(): string {
+    let params = `?name=${this.props.post.name}`;
+    if (this.props.post.url) {
+      params += `&url=${this.props.post.url}`;
+    }
+    if (this.props.post.body) {
+      params += `&body=${this.props.post.body}`;
+    }
+    return params;
+  }
+
   handleModRemoveShow(i: PostListing) {
     i.state.showRemoveDialog = true;
     i.setState(i.state);
@@ -283,6 +303,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       creator_id: i.props.post.creator_id,
       removed: !i.props.post.removed,
       reason: i.state.removeReason,
+      nsfw: i.props.post.nsfw,
       auth: null,
     };
     WebSocketService.Instance.editPost(form);
@@ -297,6 +318,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       community_id: i.props.post.community_id,
       edit_id: i.props.post.id,
       creator_id: i.props.post.creator_id,
+      nsfw: i.props.post.nsfw,
       locked: !i.props.post.locked,
       auth: null,
     };
index 93b2f606f9dd1a3cec8e8e473f34a1c406b611e3..5dd6273917e510f12aa541e1e092865d958da357 100644 (file)
@@ -2,6 +2,7 @@ import { Component } from 'inferno';
 import { Link } from 'inferno-router';
 import { Post } from '../interfaces';
 import { PostListing } from './post-listing';
+import { T } from 'inferno-i18next';
 
 interface PostListingsProps {
   posts: Array<Post>;
@@ -18,9 +19,15 @@ export class PostListings extends Component<PostListingsProps, any> {
     return (
       <div>
         {this.props.posts.length > 0 ? this.props.posts.map(post => 
-          <PostListing post={post} showCommunity={this.props.showCommunity} />) : 
-          <div>No posts. {this.props.showCommunity !== undefined  && <span>Subscribe to some <Link to="/communities">communities</Link>.</span>}
-        </div>
+          <>
+            <PostListing post={post} showCommunity={this.props.showCommunity} />
+            <hr class="my-2" />
+          </>
+            ) : 
+          <>
+            <div><T i18nKey="no_posts">#</T></div>
+            {this.props.showCommunity !== undefined  && <div><T i18nKey="subscribe_to_communities">#<Link to="/communities">#</Link></T></div>}
+          </>
         }
       </div>
     )
index 3e2e07b3504338ac7e66595893dcfb26d21a2a46..91f8f4db76b73e90916f9ebefeb5f739e0385610 100644 (file)
@@ -1,14 +1,17 @@
 import { Component, linkEvent } from 'inferno';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, BanUserResponse, AddModToCommunityResponse, AddAdminResponse, UserView } from '../interfaces';
+import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, BanUserResponse, AddModToCommunityResponse, AddAdminResponse, UserView, SearchType, SortType, SearchForm, SearchResponse, GetSiteResponse, GetCommunityResponse } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { msgOp, hotRank } from '../utils';
 import { PostListing } from './post-listing';
+import { PostListings } from './post-listings';
 import { Sidebar } from './sidebar';
 import { CommentForm } from './comment-form';
 import { CommentNodes } from './comment-nodes';
 import * as autosize from 'autosize';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 interface PostState {
   post: PostI;
@@ -20,6 +23,7 @@ interface PostState {
   scrolled?: boolean;
   scrolled_comment_id?: number;
   loading: boolean;
+  crossPosts: Array<PostI>;
 }
 
 export class Post extends Component<any, PostState> {
@@ -33,7 +37,8 @@ export class Post extends Component<any, PostState> {
     moderators: [],
     admins: [],
     scrolled: false, 
-    loading: true
+    loading: true,
+    crossPosts: [],
   }
 
   constructor(props: any, context: any) {
@@ -73,6 +78,19 @@ export class Post extends Component<any, PostState> {
       this.state.scrolled = true;
       this.markScrolledAsRead(this.state.scrolled_comment_id);
     }
+
+    // Necessary if you are on a post and you click another post (same route)
+    if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
+      // Couldnt get a refresh working. This does for now.
+      location.reload();
+
+      // let currentId = this.props.match.params.id;
+      // WebSocketService.Instance.getPost(currentId);
+      // this.context.router.history.push('/sponsors');
+      // this.context.refresh();
+      // this.context.router.history.push(_lastProps.location.pathname);
+
+    }
   }
 
   markScrolledAsRead(commentId: number) {
@@ -101,7 +119,7 @@ export class Post extends Component<any, PostState> {
         {this.state.loading ? 
         <h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : 
         <div class="row">
-            <div class="col-12 col-md-8 col-lg-6 mb-3">
+            <div class="col-12 col-md-8 mb-3">
               <PostListing 
                 post={this.state.post} 
                 showBody 
@@ -110,16 +128,20 @@ export class Post extends Component<any, PostState> {
                 moderators={this.state.moderators} 
                 admins={this.state.admins}
               />
+              {this.state.crossPosts.length > 0 && 
+                <>
+                  <div class="my-1 text-muted small font-weight-bold"><T i18nKey="cross_posts">#</T></div>
+                  <PostListings showCommunity posts={this.state.crossPosts} />
+                </>
+              }
               <div className="mb-2" />
               <CommentForm postId={this.state.post.id} disabled={this.state.post.locked} />
               {this.sortRadios()}
               {this.commentsTree()}
             </div>
-            <div class="col-12 col-md-4 col-lg-3 mb-3 d-none d-md-block px-0">
-              {this.state.comments.length > 0 && this.newComments()}
-            </div>
-            <div class="col-12 col-sm-12 col-lg-3">
+            <div class="col-12 col-sm-12 col-md-4">
               {this.sidebar()}
+              {this.state.comments.length > 0 && this.newComments()}
             </div>
           </div>
         }
@@ -130,17 +152,17 @@ export class Post extends Component<any, PostState> {
   sortRadios() {
     return (
       <div class="btn-group btn-group-toggle mb-3">
-        <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Hot && 'active'}`}>Hot
+        <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Hot && 'active'}`}>{i18n.t('hot')}
           <input type="radio" value={CommentSortType.Hot}
           checked={this.state.commentSort === CommentSortType.Hot} 
           onChange={linkEvent(this, this.handleCommentSortChange)}  />
         </label>
-        <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Top && 'active'}`}>Top
+        <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Top && 'active'}`}>{i18n.t('top')}
           <input type="radio" value={CommentSortType.Top}
           checked={this.state.commentSort === CommentSortType.Top} 
           onChange={linkEvent(this, this.handleCommentSortChange)}  />
         </label>
-        <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.New && 'active'}`}>New
+        <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.New && 'active'}`}>{i18n.t('new')}
           <input type="radio" value={CommentSortType.New}
           checked={this.state.commentSort === CommentSortType.New} 
           onChange={linkEvent(this, this.handleCommentSortChange)}  />
@@ -151,25 +173,26 @@ export class Post extends Component<any, PostState> {
 
   newComments() {
     return (
-      <div class="container-fluid sticky-top new-comments">
-        <h5>Chat</h5>
-        <CommentForm postId={this.state.post.id} disabled={this.state.post.locked} />
-        {this.state.comments.map(comment => 
-          <CommentNodes 
-            nodes={[{comment: comment}]} 
-            noIndent 
-            locked={this.state.post.locked} 
-            moderators={this.state.moderators} 
-            admins={this.state.admins}
-          />
-        )}
+      <div class="d-none d-md-block sticky-top new-comments card border-secondary">
+        <div class="card-body small">
+          <h6><T i18nKey="recent_comments">#</T></h6>
+          {this.state.comments.map(comment => 
+            <CommentNodes 
+              nodes={[{comment: comment}]} 
+              noIndent 
+              locked={this.state.post.locked} 
+              moderators={this.state.moderators} 
+              admins={this.state.admins}
+            />
+          )}
+        </div>
       </div>
     )
   }
 
   sidebar() {
     return ( 
-      <div class="">
+      <div class="mb-3">
         <Sidebar 
           community={this.state.community} 
           moderators={this.state.moderators} 
@@ -242,18 +265,30 @@ export class Post extends Component<any, PostState> {
     console.log(msg);
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
-      alert(msg.error);
+      alert(i18n.t(msg.error));
       return;
     } else if (op == UserOperation.GetPost) {
       let res: GetPostResponse = msg;
       this.state.post = res.post;
-      this.state.post = res.post;
       this.state.comments = res.comments;
       this.state.community = res.community;
       this.state.moderators = res.moderators;
       this.state.admins = res.admins;
       this.state.loading = false;
-      document.title = `${this.state.post.name} - Lemmy`;
+      document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
+
+      // Get cross-posts  
+      if (this.state.post.url) {
+        let form: SearchForm = {
+          q: this.state.post.url,
+          type_: SearchType[SearchType.Url],
+          sort: SortType[SortType.TopAll],
+          page: 1,
+          limit: 6,
+        };
+        WebSocketService.Instance.search(form);
+      }
+      
       this.setState(this.state);
     } else if (op == UserOperation.CreateComment) {
       let res: CommentResponse = msg;
@@ -330,6 +365,21 @@ export class Post extends Component<any, PostState> {
       let res: AddAdminResponse = msg;
       this.state.admins = res.admins;
       this.setState(this.state);
+    } else if (op == UserOperation.Search) {
+      let res: SearchResponse = msg;
+      this.state.crossPosts = res.posts.filter(p => p.id != this.state.post.id);
+      this.setState(this.state);
+    } else if (op == UserOperation.TransferSite) { 
+      let res: GetSiteResponse = msg;
+
+      this.state.admins = res.admins;
+      this.setState(this.state);
+    } else if (op == UserOperation.TransferCommunity) { 
+      let res: GetCommunityResponse = msg;
+      this.state.community = res.community;
+      this.state.moderators = res.moderators;
+      this.state.admins = res.admins;
+      this.setState(this.state);
     }
 
   }
index 7c72939a7d147c8a85b98a417066a986c74c14b8..34a4a3d3178f4e1e6b52c68c1f91b9c5bfb6bd5c 100644 (file)
@@ -1,11 +1,14 @@
 import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { UserOperation, Post, Comment, SortType, SearchForm, SearchResponse, SearchType } from '../interfaces';
+import { UserOperation, Post, Comment, Community, UserView, SortType, SearchForm, SearchResponse, SearchType } from '../interfaces';
 import { WebSocketService } from '../services';
 import { msgOp, fetchLimit } from '../utils';
 import { PostListing } from './post-listing';
 import { CommentNodes } from './comment-nodes';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 interface SearchState {
   q: string,
@@ -21,13 +24,16 @@ export class Search extends Component<any, SearchState> {
   private subscription: Subscription;
   private emptyState: SearchState = {
     q: undefined,
-    type_: SearchType.Both,
+    type_: SearchType.All,
     sort: SortType.TopAll,
     page: 1,
     searchResponse: {
       op: null,
+      type_: null,
       posts: [],
       comments: [],
+      communities: [],
+      users: [],
     },
     loading: false,
   }
@@ -52,7 +58,7 @@ export class Search extends Component<any, SearchState> {
   }
 
   componentDidMount() {
-    document.title = "Search - Lemmy";
+    document.title = `${i18n.t('search')} - ${WebSocketService.Instance.site.name}`;
   }
 
   render() {
@@ -60,11 +66,11 @@ export class Search extends Component<any, SearchState> {
       <div class="container">
         <div class="row">
           <div class="col-12">
-            <h5>Search</h5>
+            <h5><T i18nKey="search">#</T></h5>
             {this.selects()}
             {this.searchForm()}
-            {this.state.type_ == SearchType.Both &&
-              this.both()
+            {this.state.type_ == SearchType.All &&
+              this.all()
             }
             {this.state.type_ == SearchType.Comments &&
               this.comments()
@@ -72,6 +78,12 @@ export class Search extends Component<any, SearchState> {
             {this.state.type_ == SearchType.Posts &&
               this.posts()
             }
+            {this.state.type_ == SearchType.Communities &&
+              this.communities()
+            }
+            {this.state.type_ == SearchType.Users &&
+              this.users()
+            }
             {this.noResults()}
             {this.paginator()}
           </div>
@@ -83,11 +95,11 @@ export class Search extends Component<any, SearchState> {
   searchForm() {
     return (
       <form class="form-inline" onSubmit={linkEvent(this, this.handleSearchSubmit)}>
-        <input type="text" class="form-control mr-2" value={this.state.q} placeholder="Search..." onInput={linkEvent(this, this.handleQChange)} required minLength={3} />
+        <input type="text" class="form-control mr-2" value={this.state.q} placeholder={`${i18n.t('search')}...`} onInput={linkEvent(this, this.handleQChange)} required minLength={3} />
         <button type="submit" class="btn btn-secondary mr-2">
           {this.state.loading ?
           <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
-          <span>Search</span>
+          <span><T i18nKey="search">#</T></span>
           }
         </button>
       </form>
@@ -98,47 +110,72 @@ export class Search extends Component<any, SearchState> {
     return (
       <div className="mb-2">
         <select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="custom-select custom-select-sm w-auto">
-          <option disabled>Type</option>
-          <option value={SearchType.Both}>Both</option>
-          <option value={SearchType.Comments}>Comments</option>
-          <option value={SearchType.Posts}>Posts</option>
+          <option disabled><T i18nKey="type">#</T></option>
+          <option value={SearchType.All}><T i18nKey="all">#</T></option>
+          <option value={SearchType.Comments}><T i18nKey="comments">#</T></option>
+          <option value={SearchType.Posts}><T i18nKey="posts">#</T></option>
+          <option value={SearchType.Communities}><T i18nKey="communities">#</T></option>
+          <option value={SearchType.Users}><T i18nKey="users">#</T></option>
         </select>
         <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
-          <option disabled>Sort Type</option>
-          <option value={SortType.New}>New</option>
-          <option value={SortType.TopDay}>Top Day</option>
-          <option value={SortType.TopWeek}>Week</option>
-          <option value={SortType.TopMonth}>Month</option>
-          <option value={SortType.TopYear}>Year</option>
-          <option value={SortType.TopAll}>All</option>
+          <option disabled><T i18nKey="sort_type">#</T></option>
+          <option value={SortType.New}><T i18nKey="new">#</T></option>
+          <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
+          <option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
+          <option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
+          <option value={SortType.TopYear}><T i18nKey="year">#</T></option>
+          <option value={SortType.TopAll}><T i18nKey="all">#</T></option>
         </select>
       </div>
     )
 
   }
 
-  both() {
-    let combined: Array<{type_: string, data: Comment | Post}> = [];
+  all() {
+    let combined: Array<{type_: string, data: Comment | Post | Community | UserView}> = [];
     let comments = this.state.searchResponse.comments.map(e => {return {type_: "comments", data: e}});
     let posts = this.state.searchResponse.posts.map(e => {return {type_: "posts", data: e}});
+    let communities = this.state.searchResponse.communities.map(e => {return {type_: "communities", data: e}});
+    let users = this.state.searchResponse.users.map(e => {return {type_: "users", data: e}});
 
     combined.push(...comments);
     combined.push(...posts);
+    combined.push(...communities);
+    combined.push(...users);
 
     // Sort it
     if (this.state.sort == SortType.New) {
       combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
     } else {
-      combined.sort((a, b) => b.data.score - a.data.score);
+      combined.sort((a, b) => ((b.data as Comment | Post).score 
+        | (b.data as Community).number_of_subscribers
+          | (b.data as UserView).comment_score) 
+          - ((a.data as Comment | Post).score 
+            | (a.data as Community).number_of_subscribers 
+              | (a.data as UserView).comment_score));
     }
 
     return (
       <div>
         {combined.map(i =>
           <div>
-            {i.type_ == "posts"
-              ? <PostListing post={i.data as Post} showCommunity viewOnly />
-              : <CommentNodes nodes={[{comment: i.data as Comment}]} viewOnly noIndent />
+            {i.type_ == "posts" &&
+              <PostListing post={i.data as Post} showCommunity viewOnly />
+            }
+            {i.type_ == "comments" && 
+              <CommentNodes nodes={[{comment: i.data as Comment}]} viewOnly noIndent />
+            }
+            {i.type_ == "communities" && 
+              <div>
+                <span><Link to={`/c/${(i.data as Community).name}`}>{`/c/${(i.data as Community).name}`}</Link></span>
+                <span>{` - ${(i.data as Community).title} - ${(i.data as Community).number_of_subscribers} subscribers`}</span>
+              </div>
+            }
+            {i.type_ == "users" && 
+              <div>
+                <span><Link className="text-info" to={`/u/${(i.data as UserView).name}`}>{`/u/${(i.data as UserView).name}`}</Link></span>
+                <span>{` - ${(i.data as UserView).comment_score} comment karma`}</span>
+              </div>
             }
           </div>
                      )
@@ -167,13 +204,40 @@ export class Search extends Component<any, SearchState> {
     );
   }
 
+  // Todo possibly create UserListing and CommunityListing
+  communities() {
+    return (
+      <div>
+        {this.state.searchResponse.communities.map(community => 
+          <div>
+            <span><Link to={`/c/${community.name}`}>{`/c/${community.name}`}</Link></span>
+            <span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span>
+          </div>
+        )}
+      </div>
+    );
+  }
+
+  users() {
+    return (
+      <div>
+        {this.state.searchResponse.users.map(user => 
+          <div>
+            <span><Link className="text-info" to={`/u/${user.name}`}>{`/u/${user.name}`}</Link></span>
+            <span>{` - ${user.comment_score} comment karma`}</span>
+          </div>
+        )}
+      </div>
+    );
+  }
+
   paginator() {
     return (
       <div class="mt-2">
         {this.state.page > 1 && 
-          <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
+          <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
         }
-        <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
+        <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
       </div>
     );
   }
@@ -183,7 +247,7 @@ export class Search extends Component<any, SearchState> {
     return (
       <div>
         {res && res.op && res.posts.length == 0 && res.comments.length == 0 && 
-          <span>No Results</span>
+          <span><T i18nKey="no_results">#</T></span>
         }
       </div>
     )
@@ -218,14 +282,12 @@ export class Search extends Component<any, SearchState> {
     i.state.sort = Number(event.target.value);
     i.state.page = 1;
     i.setState(i.state);
-    i.search();
   }
 
   handleTypeChange(i: Search, event: any) {
     i.state.type_ = Number(event.target.value);
     i.state.page = 1;
     i.setState(i.state);
-    i.search();
   }
 
   handleSearchSubmit(i: Search, event: any) {
@@ -244,16 +306,15 @@ export class Search extends Component<any, SearchState> {
     console.log(msg);
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
-      alert(msg.error);
+      alert(i18n.t(msg.error));
       return;
     } else if (op == UserOperation.Search) {
       let res: SearchResponse = msg;
       this.state.searchResponse = res;
       this.state.loading = false;
-      document.title = `Search - ${this.state.q} - Lemmy`;
+      document.title = `${i18n.t('search')} - ${this.state.q} - ${WebSocketService.Instance.site.name}`;
       window.scrollTo(0,0);
       this.setState(this.state);
-      
     }
   }
 }
index edb98260c67f891e1795ab7df202b76712bafd70..24a5f2d68961709a73fd49587e6307693c5c8954 100644 (file)
@@ -5,6 +5,8 @@ import { RegisterForm, LoginResponse, UserOperation } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { msgOp } from '../utils';
 import { SiteForm } from './site-form';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 interface State {
   userForm: RegisterForm;
@@ -21,6 +23,7 @@ export class Setup extends Component<any, State> {
       password: undefined,
       password_verify: undefined,
       admin: true,
+      show_nsfw: true,
     },
     doneRegisteringUser: false,
     userLoading: false,
@@ -46,7 +49,7 @@ export class Setup extends Component<any, State> {
   }
 
   componentDidMount() {
-    document.title = "Setup - Lemmy";
+    document.title = `${i18n.t('setup')} - Lemmy`;
   }
 
   render() {
@@ -54,7 +57,7 @@ export class Setup extends Component<any, State> {
       <div class="container">
         <div class="row">
           <div class="col-12 offset-lg-3 col-lg-6">
-            <h3>Lemmy Instance Setup</h3>
+            <h3><T i18nKey="lemmy_instance_setup">#</T></h3>
             {!this.state.doneRegisteringUser ? this.registerUser() : <SiteForm />}
           </div>
         </div>
@@ -65,27 +68,27 @@ export class Setup extends Component<any, State> {
   registerUser() {
     return (
       <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
-        <h5>Set up Site Administrator</h5>
+        <h5><T i18nKey="setup_admin">#</T></h5>
         <div class="form-group row">
-          <label class="col-sm-2 col-form-label">Username</label>
+          <label class="col-sm-2 col-form-label"><T i18nKey="username">#</T></label>
           <div class="col-sm-10">
             <input type="text" class="form-control" value={this.state.userForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} maxLength={20} pattern="[a-zA-Z0-9_]+" />
           </div>
         </div>
         <div class="form-group row">
-          <label class="col-sm-2 col-form-label">Email</label>
+          <label class="col-sm-2 col-form-label"><T i18nKey="email">#</T></label>
           <div class="col-sm-10">
-            <input type="email" class="form-control" placeholder="Optional" value={this.state.userForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} />
+            <input type="email" class="form-control" placeholder={i18n.t('optional')} value={this.state.userForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} />
           </div>
         </div>
         <div class="form-group row">
-          <label class="col-sm-2 col-form-label">Password</label>
+          <label class="col-sm-2 col-form-label"><T i18nKey="password">#</T></label>
           <div class="col-sm-10">
             <input type="password" value={this.state.userForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required />
           </div>
         </div>
         <div class="form-group row">
-          <label class="col-sm-2 col-form-label">Verify Password</label>
+          <label class="col-sm-2 col-form-label"><T i18nKey="verify_password">#</T></label>
           <div class="col-sm-10">
             <input type="password" value={this.state.userForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required />
           </div>
@@ -93,7 +96,7 @@ export class Setup extends Component<any, State> {
         <div class="form-group row">
           <div class="col-sm-10">
             <button type="submit" class="btn btn-secondary">{this.state.userLoading ? 
-            <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 'Sign Up'}</button>
+            <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('sign_up')}</button>
 
           </div>
         </div>
@@ -133,7 +136,7 @@ export class Setup extends Component<any, State> {
   parseMessage(msg: any) {
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
-      alert(msg.error);
+      alert(i18n.t(msg.error));
       this.state.userLoading = false;
       this.setState(this.state);
       return;
index 6532bc05025c122b58175c276f395deeab88964c..df5ad097c217c29176ca54d6d4013dc5c4ccddae 100644 (file)
@@ -4,6 +4,8 @@ import { Community, CommunityUser, FollowCommunityForm, CommunityForm as Communi
 import { WebSocketService, UserService } from '../services';
 import { mdToHtml, getUnixTime } from '../utils';
 import { CommunityForm } from './community-form';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 interface SidebarProps {
   community: Community;
@@ -52,85 +54,90 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
     let community = this.props.community;
     return (
       <div>
-        <h5 className="mb-0">{community.title}
-        {community.removed &&
-          <small className="ml-2 text-muted font-italic">removed</small>
-        }
-        {community.deleted &&
-          <small className="ml-2 text-muted font-italic">deleted</small>
-        }
-      </h5>
-      <Link className="text-muted" to={`/c/${community.name}`}>/c/{community.name}</Link>
-      <ul class="list-inline mb-1 text-muted small font-weight-bold"> 
-        {this.canMod && 
-          <>
-            <li className="list-inline-item">
-              <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
-            </li>
-            {this.amCreator && 
-              <li className="list-inline-item">
-                <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
-                  {!community.deleted ? 'delete' : 'restore'}
-                </span>
-              </li>
+        <div class="card border-secondary mb-3">
+          <div class="card-body">
+            <h5 className="mb-0">
+              <span>{community.title}</span>
+              {community.removed &&
+                <small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
+              }
+              {community.deleted &&
+                <small className="ml-2 text-muted font-italic"><T i18nKey="deleted">#</T></small>
+              }
+            </h5>
+            <Link className="text-muted" to={`/c/${community.name}`}>/c/{community.name}</Link>
+            <ul class="list-inline mb-1 text-muted small font-weight-bold"> 
+              {this.canMod && 
+                <>
+                  <li className="list-inline-item">
+                    <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
+                  </li>
+                  {this.amCreator && 
+                    <li className="list-inline-item">
+                      <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
+                        {!community.deleted ? i18n.t('delete') : i18n.t('restore')}
+                      </span>
+                    </li>
+                  }
+                </>
+              }
+              {this.canAdmin &&
+                <li className="list-inline-item">
+                  {!this.props.community.removed ? 
+                  <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
+                  <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
+                  }
+                </li>
+
+              }
+            </ul>
+            {this.state.showRemoveDialog && 
+              <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
+                <div class="form-group row">
+                  <label class="col-form-label"><T i18nKey="reason">#</T></label>
+                  <input type="text" class="form-control mr-2" placeholder={i18n.t('optional')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
+                </div>
+                {/* TODO hold off on expires for now */}
+                {/* <div class="form-group row"> */}
+                  {/*   <label class="col-form-label">Expires</label> */}
+                  {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
+                  {/* </div> */}
+                  <div class="form-group row">
+                    <button type="submit" class="btn btn-secondary"><T i18nKey="remove_community">#</T></button>
+                  </div>
+                </form>
             }
-          </>
-        }
-        {this.canAdmin &&
-          <li className="list-inline-item">
-            {!this.props.community.removed ? 
-            <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
-            <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
+            <ul class="my-1 list-inline">
+              <li className="list-inline-item"><Link className="badge badge-secondary" to="/communities">{community.category_name}</Link></li>
+              <li className="list-inline-item badge badge-secondary"><T i18nKey="number_of_subscribers" interpolation={{count: community.number_of_subscribers}}>#</T></li>
+              <li className="list-inline-item badge badge-secondary"><T i18nKey="number_of_posts" interpolation={{count: community.number_of_posts}}>#</T></li>
+              <li className="list-inline-item badge badge-secondary"><T i18nKey="number_of_comments" interpolation={{count: community.number_of_comments}}>#</T></li>
+              <li className="list-inline-item"><Link className="badge badge-secondary" to={`/modlog/community/${this.props.community.id}`}><T i18nKey="modlog">#</T></Link></li>
+            </ul>
+            <ul class="list-inline small"> 
+              <li class="list-inline-item">{i18n.t('mods')}: </li>
+              {this.props.moderators.map(mod =>
+                <li class="list-inline-item"><Link class="text-info" to={`/u/${mod.user_name}`}>{mod.user_name}</Link></li>
+              )}
+            </ul>
+            <Link class={`btn btn-sm btn-secondary btn-block mb-3 ${(community.deleted || community.removed) && 'no-click'}`}
+            to={`/create_post?community=${community.name}`}><T i18nKey="create_a_post">#</T></Link>
+          <div>
+            {community.subscribed 
+              ? <button class="btn btn-sm btn-secondary btn-block" onClick={linkEvent(community.id, this.handleUnsubscribe)}><T i18nKey="unsubscribe">#</T></button>
+              : <button class="btn btn-sm btn-secondary btn-block" onClick={linkEvent(community.id, this.handleSubscribe)}><T i18nKey="subscribe">#</T></button>
             }
-          </li>
-
-        }
-      </ul>
-      {this.state.showRemoveDialog && 
-        <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
-          <div class="form-group row">
-            <label class="col-form-label">Reason</label>
-            <input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
           </div>
-          {/* TODO hold off on expires for now */}
-          {/* <div class="form-group row"> */}
-          {/*   <label class="col-form-label">Expires</label> */}
-          {/*   <input type="date" class="form-control mr-2" placeholder="Expires" value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
-          {/* </div> */}
-          <div class="form-group row">
-            <button type="submit" class="btn btn-secondary">Remove Community</button>
+        </div>
+        </div>
+        {community.description && 
+          <div class="card border-secondary">
+            <div class="card-body">
+              <div className="md-div" dangerouslySetInnerHTML={mdToHtml(community.description)} />
+            </div>
           </div>
-        </form>
-      }
-      <ul class="my-1 list-inline">
-        <li className="list-inline-item"><Link className="badge badge-light" to="/communities">{community.category_name}</Link></li>
-        <li className="list-inline-item badge badge-light">{community.number_of_subscribers} Subscribers</li>
-        <li className="list-inline-item badge badge-light">{community.number_of_posts} Posts</li>
-        <li className="list-inline-item badge badge-light">{community.number_of_comments} Comments</li>
-        <li className="list-inline-item"><Link className="badge badge-light" to={`/modlog/community/${this.props.community.id}`}>Modlog</Link></li>
-      </ul>
-      <ul class="list-inline small"> 
-        <li class="list-inline-item">mods: </li>
-        {this.props.moderators.map(mod =>
-          <li class="list-inline-item"><Link class="text-info" to={`/u/${mod.user_name}`}>{mod.user_name}</Link></li>
-        )}
-      </ul>
-      <Link class="btn btn-sm btn-secondary btn-block mb-3" 
-        to={`/create_post/c/${community.name}`}>Create a Post</Link>
-      <div>
-        {community.subscribed 
-          ? <button class="btn btn-sm btn-secondary btn-block mb-3" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</button>
-          : <button class="btn btn-sm btn-secondary btn-block mb-3" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</button>
         }
-      </div>
-      {community.description && 
-        <div>
-          <hr />
-          <div className="md-div" dangerouslySetInnerHTML={mdToHtml(community.description)} />
-          <hr />
         </div>
-      }
-    </div>
     );
   }
 
@@ -157,6 +164,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
       category_id: i.props.community.category_id,
       edit_id: i.props.community.id,
       deleted: !i.props.community.deleted,
+      nsfw: i.props.community.nsfw,
       auth: null,
     };
     WebSocketService.Instance.editCommunity(deleteForm);
@@ -216,6 +224,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
       removed: !i.props.community.removed,
       reason: i.state.removeReason,
       expires: getUnixTime(i.state.removeExpires),
+      nsfw: i.props.community.nsfw,
       auth: null,
     };
     WebSocketService.Instance.editCommunity(deleteForm);
index 7c51be40358731b8ac0ff35464ed6a383fa990fa..011642158e6cb2bd7ead54f27fb868f881c14109 100644 (file)
@@ -1,7 +1,10 @@
 import { Component, linkEvent } from 'inferno';
 import { Site, SiteForm as SiteFormI } from '../interfaces';
 import { WebSocketService } from '../services';
+import { capitalizeFirstLetter } from '../utils';
 import * as autosize from 'autosize';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 interface SiteFormProps {
   site?: Site; // If a site is given, that means this is an edit
@@ -39,15 +42,15 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
   render() {
     return (
       <form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
-        <h5>{`${this.props.site ? 'Edit' : 'Name'} your Site`}</h5>
+        <h5>{`${this.props.site ? capitalizeFirstLetter(i18n.t('edit')) : capitalizeFirstLetter(i18n.t('name'))} ${i18n.t('your_site')}`}</h5>
         <div class="form-group row">
-          <label class="col-12 col-form-label">Name</label>
+          <label class="col-12 col-form-label"><T i18nKey="name">#</T></label>
           <div class="col-12">
             <input type="text" class="form-control" value={this.state.siteForm.name} onInput={linkEvent(this, this.handleSiteNameChange)} required minLength={3} maxLength={20} />
           </div>
         </div>
         <div class="form-group row">
-          <label class="col-12 col-form-label">Sidebar</label>
+          <label class="col-12 col-form-label"><T i18nKey="sidebar">#</T></label>
           <div class="col-12">
             <textarea value={this.state.siteForm.description} onInput={linkEvent(this, this.handleSiteDescriptionChange)} class="form-control" rows={3} maxLength={10000} />
           </div>
@@ -57,8 +60,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
             <button type="submit" class="btn btn-secondary mr-2">
               {this.state.loading ? 
               <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 
-              this.props.site ? 'Save' : 'Create'}</button>
-              {this.props.site && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>}
+              this.props.site ? capitalizeFirstLetter(i18n.t('save')) : capitalizeFirstLetter(i18n.t('create'))}</button>
+              {this.props.site && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}><T i18nKey="cancel">#</T></button>}
           </div>
         </div>
       </form>
index 4c9d02ffbf0c8161feae5f221b0ff25a7eaeb01d..8f58812d64ddbe7a19ba36930c770da3ba19cf66 100644 (file)
@@ -1,4 +1,7 @@
 import { Component } from 'inferno';
+import { WebSocketService } from '../services';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 let general = 
   [
@@ -17,7 +20,7 @@ export class Sponsors extends Component<any, any> {
   }
 
   componentDidMount() {
-    document.title = "Sponsors - Lemmy";
+    document.title = `${i18n.t('sponsors')} - ${WebSocketService.Instance.site.name}`;
   }
 
   render() {
@@ -35,19 +38,19 @@ export class Sponsors extends Component<any, any> {
   topMessage() {
     return (
       <div>
-        <h5>Sponsors of Lemmy</h5>
+        <h5><T i18nKey="sponsors_of_lemmy">#</T></h5>
         <p>
-          Lemmy is free, <a href="https://github.com/dessalines/lemmy">open-source</a> 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:
+          <T i18nKey="sponsor_message">#<a href="https://github.com/dessalines/lemmy">#</a></T>
         </p>
-        <a class="btn btn-secondary" href="https://www.patreon.com/dessalines">Support on Patreon</a>
+        <a class="btn btn-secondary" href="https://www.patreon.com/dessalines"><T i18nKey="support_on_patreon">#</T></a>
       </div>
     )
   }
   sponsors() {
     return (
       <div class="container">
-        <h5>Sponsors</h5>
-        <p>General Sponsors are those that pledged $10 to $39 to Lemmy.</p>
+        <h5><T i18nKey="sponsors">#</T></h5>
+        <p><T i18nKey="general_sponsors">#</T></p>
         <div class="row card-columns">
           {general.map(s => 
             <div class="card col-12 col-md-2">
@@ -62,18 +65,24 @@ export class Sponsors extends Component<any, any> {
   bitcoin() {
     return (
       <div>
-      <h5>Crypto</h5>
+        <h5><T i18nKey="crypto">#</T></h5>
       <div class="table-responsive">
         <table class="table table-hover text-center">
           <tbody>
           <tr>
-            <td>Bitcoin</td>
+            <td><T i18nKey="bitcoin">#</T></td>
             <td><code>1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK</code></td>
           </tr>
           <tr>
-            <td>Ethereum</td>
+            <td><T i18nKey="ethereum">#</T></td>
             <td><code>0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01</code></td>
           </tr>
+          <tr>
+            <td><T i18nKey="monero">#</T></td>
+            <td>
+              <code>41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV</code>
+            </td>
+          </tr>
           </tbody>
         </table>
       </div>
index 1085a4dafe6d386c56ca0918bb71e4eeafad2e30..e2803fb7d5314ef8835f7cc7d025c9c3e921f815 100644 (file)
@@ -23,55 +23,42 @@ export class Symbols extends Component<any, any> {
             <path d="M28 5h-24c-2.209 0-4 1.792-4 4v13c0 2.209 1.791 4 4 4h24c2.209 0 4-1.791 4-4v-13c0-2.208-1.791-4-4-4zM2 10.25l6.999 5.25-6.999 5.25v-10.5zM30 22c0 1.104-0.898 2-2 2h-24c-1.103 0-2-0.896-2-2l7.832-5.875 4.368 3.277c0.533 0.398 1.166 0.6 1.8 0.6 0.633 0 1.266-0.201 1.799-0.6l4.369-3.277 7.832 5.875zM30 20.75l-7-5.25 7-5.25v10.5zM17.199 18.602c-0.349 0.262-0.763 0.4-1.199 0.4s-0.851-0.139-1.2-0.4l-12.8-9.602c0-1.103 0.897-2 2-2h24c1.102 0 2 0.897 2 2l-12.801 9.602z"></path>
           </symbol>         
           <symbol id="icon-mouse" version="1.1" x="0px" y="0px"
-            viewBox="0 0 512 512">
-            <g>
-              <g>
-                <path d="M499.059,323.505l-7.52-32.532l-70.047,16.19c1.513-11.983,2.297-24.042,2.297-36.037c0-18.334-1.801-35.785-5.316-52.19
-                  c29.365-12.101,55.143-28.885,69.372-45.529c17.524-20.498,25.985-46.568,23.822-73.406
-                  c-2.163-26.862-14.706-51.268-35.316-68.724C433.879-4.694,369.917,0.439,333.774,42.718
-                  c-9.546,11.168-18.318,27.381-25.379,46.649c-16.512-5.419-34.132-8.243-52.395-8.243s-35.885,2.824-52.395,8.243
-                  c-7.06-19.267-15.832-35.481-25.379-46.649C142.082,0.44,78.123-4.695,35.648,31.277C15.038,48.733,2.494,73.141,0.332,100.001
-                  c-2.161,26.838,6.297,52.907,23.822,73.406c14.229,16.644,40.006,33.427,69.372,45.529c-3.515,16.405-5.316,33.856-5.316,52.189
-                  c0,11.995,0.785,24.053,2.297,36.037l-70.047-16.19l-7.52,32.532l84.337,19.492c4.349,17.217,10.201,33.953,17.421,49.752
-                  L12.941,416.27l7.52,32.532l110.634-25.57c1.38,2.197,2.779,4.373,4.218,6.509c32.548,48.323,75.409,74.934,120.687,74.934
-                  c45.278,0,88.138-26.612,120.687-74.934c1.439-2.136,2.839-4.313,4.218-6.509l110.634,25.57l7.52-32.532l-101.758-23.519
-                  c7.221-15.799,13.072-32.535,17.421-49.752L499.059,323.505z M183.578,220.372c0-11.41,9.189-20.65,20.482-20.65
-                  c11.306,0,20.494,9.24,20.494,20.65c0,11.408-9.188,20.656-20.494,20.656C192.768,241.028,183.578,231.78,183.578,220.372z
-                  M256,413.29c-29.895,0-54.216-19.471-54.216-43.403c0-23.932,24.322-43.403,54.216-43.403s54.216,19.471,54.216,43.403
-                  C310.216,393.819,285.895,413.29,256,413.29z M307.785,241.183c-11.402,0-20.65-9.317-20.65-20.81
-                  c0-11.494,9.248-20.81,20.65-20.81c11.387,0,20.635,9.317,20.635,20.81C328.422,231.866,319.173,241.183,307.785,241.183z"/>
-                </g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
-              <g>
-              </g>
+            viewBox="0 0 1024 1024">
+  <g
+     id="layer1"
+     transform="translate(0,-26.066658)"
+     style="display:inline">
+    <path
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="m 167.03908,270.78735 c -0.94784,-0.002 -1.8939,0.004 -2.83789,0.0215 -4.31538,0.0778 -8.58934,0.3593 -12.8125,0.8457 -33.78522,3.89116 -64.215716,21.86394 -82.871086,53.27344 -18.27982,30.77718 -22.77749,64.66635 -13.46094,96.06837 9.31655,31.40203 31.88488,59.93174 65.296886,82.5332 0.20163,0.13618 0.40678,0.26709 0.61523,0.39258 28.65434,17.27768 57.18167,28.93179 87.74218,34.95508 -0.74566,12.61339 -0.72532,25.5717 0.082,38.84375 2.43989,40.10943 16.60718,77.03742 38.0957,109.67187 l -77.00781,31.4375 c -8.30605,3.25932 -12.34178,12.68234 -8.96967,20.94324 3.37211,8.2609 12.84919,12.16798 21.06342,8.68371 l 84.69727,-34.57617 c 15.70675,18.72702 33.75346,35.68305 53.12109,50.57032 0.74013,0.56891 1.4904,1.12236 2.23437,1.68554 l -49.61132,65.69141 c -5.45446,7.0474 -4.10058,17.19288 3.01098,22.5634 7.11156,5.37052 17.24028,3.89649 22.52612,-3.27824 l 50.38672,-66.71876 c 27.68572,17.53469 57.07524,31.20388 86.07227,40.25196 14.88153,27.28008 43.96965,44.64648 77.58789,44.64648 33.93762,0 63.04252,-18.68693 77.80082,-45.4375 28.7072,-9.21295 57.7527,-22.93196 85.1484,-40.40234 l 51.0977,67.66016 c 5.2858,7.17473 15.4145,8.64876 22.5261,3.27824 7.1115,-5.37052 8.4654,-15.516 3.011,-22.5634 l -50.3614,-66.68555 c 0.334,-0.25394 0.6727,-0.50077 1.0059,-0.75586 19.1376,-14.64919 37.0259,-31.28581 52.7031,-49.63476 l 82.5625,33.70507 c 8.2143,3.48427 17.6913,-0.42281 21.0634,-8.68371 3.3722,-8.2609 -0.6636,-17.68392 -8.9696,-20.94324 l -74.5391,-30.42773 c 22.1722,-32.82971 37.0383,-70.03397 40.1426,-110.46094 1.0253,-13.35251 1.2292,-26.42535 0.6387,-39.17578 30.3557,-6.05408 58.7164,-17.66833 87.2011,-34.84375 0.2085,-0.12549 0.4136,-0.2564 0.6153,-0.39258 33.412,-22.60147 55.9803,-51.13117 65.2968,-82.5332 9.3166,-31.40202 4.8189,-65.29118 -13.4609,-96.06837 -18.6553,-31.40951 -49.0859,-49.38228 -82.8711,-53.27344 -4.2231,-0.4864 -8.4971,-0.76791 -12.8125,-0.8457 -30.2077,-0.54448 -62.4407,8.82427 -93.4316,26.71484 -22.7976,13.16063 -43.3521,33.31423 -59.4375,55.30469 -44.9968,-25.75094 -103.5444,-40.25065 -175.4785,-41.43945 -6.4522,-0.10663 -13.0125,-0.10696 -19.67974,0.002 -80.18875,1.30929 -144.38284,16.5086 -192.87109,43.9922 -0.11914,-0.19111 -0.24287,-0.37932 -0.37109,-0.56446 -16.29,-22.764 -37.41085,-43.73706 -60.89649,-57.29493 -30.02247,-17.33149 -61.21051,-26.66489 -90.59375,-26.73633 z"
+       id="path817-3"
+        />
+    <path
+       id="path1087"
+       style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 716.85595,362.96478 c 15.29075,-21.36763 35.36198,-41.10921 56.50979,-53.31749 66.66377,-38.48393 137.02617,-33.22172 170.08018,22.43043 33.09493,55.72093 14.98656,117.48866 -47.64399,159.85496 -31.95554,19.26819 -62.93318,30.92309 -97.22892,35.54473 M 307.14407,362.96478 C 291.85332,341.59715 271.78209,321.85557 250.63429,309.64729 183.97051,271.16336 113.60811,276.42557 80.554051,332.07772 47.459131,387.79865 65.56752,449.56638 128.19809,491.93268 c 31.95554,19.26819 62.93319,30.92309 97.22893,35.54473"
+        />
+    <path
+       style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 801.23205,576.8699 C 812.73478,427.06971 720.58431,321.98291 511.99999,325.38859 303.41568,328.79426 213.71393,428.0311 222.76794,576.8699 c 8.64289,142.08048 176.80223,246.40388 288.12038,246.40388 111.31815,0 279.45076,-104.5447 290.34373,-246.40388 z"
+       id="path969"
+        />
+    <path
+       id="path1084"
+       style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 610.4991,644.28932 c 0,23.11198 18.70595,41.84795 41.78091,41.84795 23.07495,0 41.7809,-18.73597 41.7809,-41.84795 0,-23.112 -18.70594,-41.84796 -41.7809,-41.84796 -23.07496,0 -41.78091,18.73596 -41.78091,41.84796 z m -280.56002,0 c 0,23.32492 18.87829,42.23352 42.16586,42.23352 23.28755,0 42.16585,-18.9086 42.16585,-42.23352 0,-23.32494 -18.87829,-42.23353 -42.16585,-42.23353 -23.28757,0 -42.16586,18.90859 -42.16586,42.23353 z"
+        />
+    <path
+       id="path1008"
+       style="display:inline;opacity:1;fill:none;stroke:#000000;stroke-width:32;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 339.72919,769.2467 -54.54422,72.22481 m 399.08582,-72.22481 54.54423,72.22481 M 263.68341,697.82002 175.92752,733.64353 m 579.85765,-35.82351 87.7559,35.82351"
+        />
+    <path
+       style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 512.00082,713.08977 c -45.86417,0 -75.13006,31.84485 -74.14159,71.10084 1.07048,42.51275 32.46865,71.10323 74.14159,71.10323 41.67296,0 74.05118,-32.99608 74.14161,-71.10323 0.0932,-39.26839 -28.27742,-71.10084 -74.14161,-71.10084 z"
+       id="path1115"
+        />
+  </g>
             </symbol>
             <symbol id="icon-search" viewBox="0 0 32 32">
               <title>search</title>
index 4cd88abc8ea2cd9c9940585165c5185c0a4c5614..8b78917ed89250f87db507d9228b5c5c81fb0d67 100644 (file)
@@ -2,12 +2,14 @@ import { Component, linkEvent } from 'inferno';
 import { Link } from 'inferno-router';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse } from '../interfaces';
-import { WebSocketService } from '../services';
+import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse, UserSettingsForm, LoginResponse } from '../interfaces';
+import { WebSocketService, UserService } from '../services';
 import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter } from '../utils';
 import { PostListing } from './post-listing';
 import { CommentNodes } from './comment-nodes';
 import { MomentTime } from './moment-time';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
 
 enum View {
   Overview, Comments, Posts, Saved
@@ -26,6 +28,8 @@ interface UserState {
   sort: SortType;
   page: number;
   loading: boolean;
+  userSettingsForm: UserSettingsForm;
+  userSettingsLoading: boolean;
 }
 
 export class User extends Component<any, UserState> {
@@ -52,6 +56,11 @@ export class User extends Component<any, UserState> {
     view: this.getViewFromProps(this.props),
     sort: this.getSortTypeFromProps(this.props),
     page: this.getPageFromProps(this.props),
+    userSettingsForm: {
+      show_nsfw: null,
+      auth: null,
+    },
+    userSettingsLoading: null,
   }
 
   constructor(props: any, context: any) {
@@ -73,6 +82,10 @@ export class User extends Component<any, UserState> {
     this.refetch();
   }
 
+  get isCurrentUser() {
+    return UserService.Instance.user && UserService.Instance.user.id == this.state.user.id;
+  }
+
   getViewFromProps(props: any): View {
     return (props.match.params.view) ? 
       View[capitalizeFirstLetter(props.match.params.view)] : 
@@ -110,7 +123,7 @@ export class User extends Component<any, UserState> {
         {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-9">
+          <div class="col-12 col-md-8">
             <h5>/u/{this.state.user.name}</h5>
             {this.selects()}
             {this.state.view == View.Overview &&
@@ -127,8 +140,11 @@ export class User extends Component<any, UserState> {
             }
             {this.paginator()}
           </div>
-          <div class="col-12 col-md-3">
+          <div class="col-12 col-md-4">
             {this.userInfo()}
+            {this.isCurrentUser &&
+              this.userSettings()
+            }
             {this.moderates()}
             {this.follows()}
           </div>
@@ -142,20 +158,20 @@ export class User extends Component<any, UserState> {
     return (
       <div className="mb-2">
         <select value={this.state.view} onChange={linkEvent(this, this.handleViewChange)} class="custom-select custom-select-sm w-auto">
-          <option disabled>View</option>
-          <option value={View.Overview}>Overview</option>
-          <option value={View.Comments}>Comments</option>
-          <option value={View.Posts}>Posts</option>
-          <option value={View.Saved}>Saved</option>
+          <option disabled><T i18nKey="view">#</T></option>
+          <option value={View.Overview}><T i18nKey="overview">#</T></option>
+          <option value={View.Comments}><T i18nKey="comments">#</T></option>
+          <option value={View.Posts}><T i18nKey="posts">#</T></option>
+          <option value={View.Saved}><T i18nKey="saved">#</T></option>
         </select>
         <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
-          <option disabled>Sort Type</option>
-          <option value={SortType.New}>New</option>
-          <option value={SortType.TopDay}>Top Day</option>
-          <option value={SortType.TopWeek}>Week</option>
-          <option value={SortType.TopMonth}>Month</option>
-          <option value={SortType.TopYear}>Year</option>
-          <option value={SortType.TopAll}>All</option>
+          <option disabled><T i18nKey="sort_type">#</T></option>
+          <option value={SortType.New}><T i18nKey="new">#</T></option>
+          <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
+          <option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
+          <option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
+          <option value={SortType.TopYear}><T i18nKey="year">#</T></option>
+          <option value={SortType.TopAll}><T i18nKey="all">#</T></option>
         </select>
       </div>
     )
@@ -216,19 +232,52 @@ export class User extends Component<any, UserState> {
     let user = this.state.user;
     return (
       <div>
-        <h5>{user.name}</h5>
-        <div>Joined <MomentTime data={user} /></div>
-        <table class="table table-bordered table-sm mt-2">
-          <tr>
-            <td>{user.post_score} points</td>
-            <td>{user.number_of_posts} posts</td>
-          </tr>
-          <tr>
-            <td>{user.comment_score} points</td>
-            <td>{user.number_of_comments} comments</td>
-          </tr>
-        </table>
-        <hr />
+        <div class="card border-secondary mb-3">
+          <div class="card-body">
+            <h5>{user.name}</h5>
+            <div>{i18n.t('joined')} <MomentTime data={user} /></div>
+            <div class="table-responsive">
+              <table class="table table-bordered table-sm mt-2 mb-0">
+                <tr>
+                  <td><T i18nKey="number_of_points" interpolation={{count: user.post_score}}>#</T></td>
+                  <td><T i18nKey="number_of_posts" interpolation={{count: user.number_of_posts}}>#</T></td>
+                </tr>
+                <tr>
+                  <td><T i18nKey="number_of_points" interpolation={{count: user.comment_score}}>#</T></td>
+                  <td><T i18nKey="number_of_comments" interpolation={{count: user.number_of_comments}}>#</T></td>
+                </tr>
+              </table>
+            </div>
+          </div>
+        </div>
+      </div>
+    )
+  }
+
+  userSettings() {  
+    return (
+      <div>
+        <div class="card border-secondary mb-3">
+          <div class="card-body">
+            <h5><T i18nKey="settings">#</T></h5>
+            <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
+              <div class="form-group row">
+                <div class="col-12">
+                  <div class="form-check">
+                    <input class="form-check-input" type="checkbox" checked={this.state.userSettingsForm.show_nsfw} onChange={linkEvent(this, this.handleUserSettingsShowNsfwChange)}/>
+                    <label class="form-check-label"><T i18nKey="show_nsfw">#</T></label>
+                  </div>
+                </div>
+              </div>
+              <div class="form-group row mb-0">
+                <div class="col-12">
+                  <button type="submit" class="btn btn-secondary">{this.state.userSettingsLoading ? 
+                  <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : capitalizeFirstLetter(i18n.t('save'))}</button>
+                </div>
+              </div>
+            </form>
+          </div>
+        </div>
       </div>
     )
   }
@@ -236,16 +285,20 @@ export class User extends Component<any, UserState> {
   moderates() {
     return (
       <div>
-        {this.state.moderates.length > 0 &&
-          <div>
-            <h5>Moderates</h5>
-            <ul class="list-unstyled"> 
-              {this.state.moderates.map(community =>
-                <li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
-              )}
-            </ul>
+        <div class="card border-secondary mb-3">
+          <div class="card-body">
+            {this.state.moderates.length > 0 &&
+              <div>
+                <h5><T i18nKey="moderates">#</T></h5>
+                <ul class="list-unstyled mb-0"> 
+                  {this.state.moderates.map(community =>
+                    <li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
+                  )}
+                </ul>
+              </div>
+            }
           </div>
-        }
+        </div>
       </div>
     )
   }
@@ -254,14 +307,15 @@ export class User extends Component<any, UserState> {
     return (
       <div>
         {this.state.follows.length > 0 &&
-          <div>
-            <hr />
-            <h5>Subscribed</h5>
-            <ul class="list-unstyled"> 
-              {this.state.follows.map(community =>
-                <li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
-              )}
-            </ul>
+          <div class="card border-secondary mb-3">
+            <div class="card-body">
+              <h5><T i18nKey="subscribed">#</T></h5>
+              <ul class="list-unstyled mb-0"> 
+                {this.state.follows.map(community =>
+                  <li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
+                )}
+              </ul>
+            </div>
           </div>
         }
       </div>
@@ -270,11 +324,11 @@ export class User extends Component<any, UserState> {
 
   paginator() {
     return (
-      <div class="mt-2">
+      <div class="my-2">
         {this.state.page > 1 && 
-          <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
+          <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
         }
-        <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
+        <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
       </div>
     );
   }
@@ -327,11 +381,24 @@ export class User extends Component<any, UserState> {
     i.refetch();
   }
 
+  handleUserSettingsShowNsfwChange(i: User, event: any) {
+    i.state.userSettingsForm.show_nsfw = event.target.checked;
+    i.setState(i.state);
+  }
+
+  handleUserSettingsSubmit(i: User, event: any) {
+    event.preventDefault();
+    i.state.userSettingsLoading = true;
+    i.setState(i.state);
+
+    WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
+  }
+
   parseMessage(msg: any) {
     console.log(msg);
     let op: UserOperation = msgOp(msg);
     if (msg.error) {
-      alert(msg.error);
+      alert(i18n.t(msg.error));
       return;
     } else if (op == UserOperation.GetUserDetails) {
       let res: UserDetailsResponse = msg;
@@ -341,7 +408,10 @@ export class User extends Component<any, UserState> {
       this.state.moderates = res.moderates;
       this.state.posts = res.posts;
       this.state.loading = false;
-      document.title = `/u/${this.state.user.name} - Lemmy`;
+      if (this.isCurrentUser) {
+        this.state.userSettingsForm.show_nsfw = UserService.Instance.user.show_nsfw;
+      }
+      document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
       window.scrollTo(0,0);
       this.setState(this.state);
     } else if (op == UserOperation.EditComment) {
@@ -359,7 +429,7 @@ export class User extends Component<any, UserState> {
       this.setState(this.state);
     } else if (op == UserOperation.CreateComment) {
       // let res: CommentResponse = msg;
-      alert('Reply sent');
+      alert(i18n.t('reply_sent'));
       // this.state.comments.unshift(res.comment); // TODO do this right
       // this.setState(this.state);
     } else if (op == UserOperation.SaveComment) {
@@ -376,6 +446,12 @@ export class User extends Component<any, UserState> {
       if (res.comment.my_vote !== null) 
         found.my_vote = res.comment.my_vote;
       this.setState(this.state);
+    } else if (op == UserOperation.SaveUserSettings) {
+        this.state = this.emptyState;
+        this.state.userSettingsLoading = false;
+        this.setState(this.state);
+        let res: LoginResponse = msg;
+        UserService.Instance.login(res);
     }
   }
 }
index 2a65ca19f545efb2c5cb4631f5e081bd9aa50f66..9a6e2067e59d085cd516dc3de59cad0103fd4b5c 100644 (file)
@@ -2,6 +2,10 @@ body, .text-white, .navbar-brand, .badge-light, .btn-secondary {
   color: #dedede !important;
 }
 
+.navbar-toggler {
+  border: 0px;
+}
+
 .pointer {
   cursor: pointer;
 }
@@ -15,10 +19,18 @@ body, .text-white, .navbar-brand, .badge-light, .btn-secondary {
  color: var(--info);
 }
 
+.upvote {
+  margin-bottom: -5px;
+}
+
 .downvote:hover {
   color: var(--danger);
 }
 
+.downvote {
+  margin-top: -10px;
+}
+
 .form-control, .form-control:focus {
   background-color: var(--secondary);
   color: #fff;
@@ -42,7 +54,7 @@ body, .text-white, .navbar-brand, .badge-light, .btn-secondary {
   background-color: #444 !important;
 }
 
-.md-div p {
+.md-div p:last-child {
   margin-bottom: 0px;
 }
 
@@ -51,8 +63,16 @@ body, .text-white, .navbar-brand, .badge-light, .btn-secondary {
   height: auto;
 }
 
-.listing {
-  min-height: 61px;
+.comment-node {
+  margin-bottom: 10px;
+}
+
+.vote-bar {
+  margin-top: -6.5px;
+}
+
+.post-title {
+  line-height: 1.0;
 }
 
 .icon {
@@ -80,20 +100,12 @@ body, .text-white, .navbar-brand, .badge-light, .btn-secondary {
   z-index: 2000;
 }
 
-.navbar-bg {
-  background-color: #222;
-}
-
 blockquote {
   border-left: 3px solid #ccc;
   margin: 0.5em 5px;
   padding: 0.1em 5px;
 }
 
-.inbox {
-  margin-top: 6px;
-}
-
 .mouse-icon {
   margin-top: -4px;
 }
@@ -108,8 +120,8 @@ blockquote {
 }
 
 .thumbnail {
-  max-height: 50px;
-  max-width: 50px;
+  max-height: 62px;
+  max-width: 400px;
 }
 
 .no-s-hows {
@@ -117,3 +129,7 @@ blockquote {
   top: -9999px !important;
   left: -9999px !important;
 }
+
+hr {
+  border-top: 1px solid var(--secondary);
+}
diff --git a/ui/src/i18next.ts b/ui/src/i18next.ts
new file mode 100644 (file)
index 0000000..069c820
--- /dev/null
@@ -0,0 +1,47 @@
+import * as i18n from 'i18next';
+import { getLanguage } from './utils';
+import { en } from './translations/en';
+import { eo } from './translations/eo';
+import { es } from './translations/es';
+import { de } from './translations/de';
+import { fr } from './translations/fr';
+import { sv } from './translations/sv';
+import { ru } from './translations/ru';
+import { zh } from './translations/zh';
+import { nl } from './translations/nl';
+
+// https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66
+// TODO don't forget to add moment locales for new languages.
+const resources = {
+  en,
+  eo,
+  es,
+  de,
+  zh,
+  fr,
+  sv,
+  ru,
+  nl,
+}
+
+function format(value: any, format: any, lng: any) {
+       if (format === 'uppercase') return value.toUpperCase();
+       return value;
+}
+
+i18n
+.init({
+  debug: true,
+  // load: 'languageOnly',
+
+  // initImmediate: false,
+  lng: getLanguage(),
+  fallbackLng: 'en',
+       resources,
+       interpolation: {
+    format: format
+    
+  }
+});
+
+export { i18n, resources };
index 9e091211d0fdda8fe35bbff8b308b40f5fd19dcb..4dbde8b0788e0c5dbed345afd4ed151b9925ecb4 100644 (file)
@@ -1,15 +1,15 @@
+<!DOCTYPE html>
 <html lang="en">
 
 <head>
        <!-- Required meta tags -->
+  <meta name="Description" content="Lemmy">
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="shortcut icon" type="image/svg+xml" href="/static/assets/favicon.svg" />
        <link rel="apple-touch-icon" href="/static/assets/apple-touch-icon.png" />
-       <title>Lemmy</title>
-       <link rel="stylesheet" href="/static/assets/libs/balloon-css/balloon.min.css" type="text/css">
-    <script src="/static/assets/libs/sortable/sortable.min.js" type="text/javascript"></script>
-    <script src="/static/assets/libs/markdown-it-emoji/markdown-it-emoji.min.js" type="text/javascript"></script>
+  <script async src="/static/assets/libs/sortable/sortable.min.js"></script>
+  <script src="/static/assets/libs/markdown-it-emoji/markdown-it-emoji.min.js" type="text/javascript"></script>
 </head>
 
 <body>
index 04160a9fabb98544f666e73651c435f2ece2917d..d620a5e22e22ea1a6471268e372b3c199be07cfe 100644 (file)
@@ -1,5 +1,6 @@
 import { render, Component } from 'inferno';
-import { HashRouter, BrowserRouter, Route, Switch } from 'inferno-router';
+import { BrowserRouter, Route, Switch } from 'inferno-router';
+import { Provider } from 'inferno-i18next';
 import { Main } from './components/main';
 import { Navbar } from './components/navbar';
 import { Footer } from './components/footer';
@@ -16,6 +17,7 @@ import { Inbox } from './components/inbox';
 import { Search } from './components/search';
 import { Sponsors } from './components/sponsors';
 import { Symbols } from './components/symbols';
+import { i18n } from './i18next';
 
 import './css/bootstrap.min.css';
 import './css/main.css';
@@ -34,37 +36,38 @@ class Index extends Component<any, any> {
 
   render() {
     return (
-      <HashRouter>
-        <Navbar />
-        <div class="mt-1 p-0">
-          <Switch>
-            <Route path={`/home/type/:type/sort/:sort/page/:page`} component={Main} />
-            <Route exact path={`/`} component={Main} />
-            <Route path={`/login`} component={Login} />
-            <Route path={`/create_post/c/:name`} component={CreatePost} />
-            <Route path={`/create_post`} component={CreatePost} />
-            <Route path={`/create_community`} component={CreateCommunity} />
-            <Route path={`/communities/page/:page`} component={Communities} />
-            <Route path={`/communities`} component={Communities} />
-            <Route path={`/post/:id/comment/:comment_id`} component={Post} />
-            <Route path={`/post/:id`} component={Post} />
-            <Route path={`/c/:name/sort/:sort/page/:page`} component={Community} />
-            <Route path={`/community/:id`} component={Community} />
-            <Route path={`/c/:name`} component={Community} />
-            <Route path={`/u/:username/view/:view/sort/:sort/page/:page`} component={User} />
-            <Route path={`/user/:id`} component={User} />
-            <Route path={`/u/:username`} component={User} />
-            <Route path={`/inbox`} component={Inbox} />
-            <Route path={`/modlog/community/:community_id`} component={Modlog} />
-            <Route path={`/modlog`} component={Modlog} />
-            <Route path={`/setup`} component={Setup} />
-            <Route path={`/search`} component={Search} />
-            <Route path={`/sponsors`} component={Sponsors} />
-          </Switch>
-          <Symbols />
-        </div>
-        <Footer />
-      </HashRouter>
+      <Provider i18next={i18n}>
+        <BrowserRouter>
+          <Navbar />
+          <div class="mt-4 p-0">
+            <Switch>
+              <Route path={`/home/type/:type/sort/:sort/page/:page`} component={Main} />
+              <Route exact path={`/`} component={Main} />
+              <Route path={`/login`} component={Login} />
+              <Route path={`/create_post`} component={CreatePost} />
+              <Route path={`/create_community`} component={CreateCommunity} />
+              <Route path={`/communities/page/:page`} component={Communities} />
+              <Route path={`/communities`} component={Communities} />
+              <Route path={`/post/:id/comment/:comment_id`} component={Post} />
+              <Route path={`/post/:id`} component={Post} />
+              <Route path={`/c/:name/sort/:sort/page/:page`} component={Community} />
+              <Route path={`/community/:id`} component={Community} />
+              <Route path={`/c/:name`} component={Community} />
+              <Route path={`/u/:username/view/:view/sort/:sort/page/:page`} component={User} />
+              <Route path={`/user/:id`} component={User} />
+              <Route path={`/u/:username`} component={User} />
+              <Route path={`/inbox`} component={Inbox} />
+              <Route path={`/modlog/community/:community_id`} component={Modlog} />
+              <Route path={`/modlog`} component={Modlog} />
+              <Route path={`/setup`} component={Setup} />
+              <Route path={`/search`} component={Search} />
+              <Route path={`/sponsors`} component={Sponsors} />
+            </Switch>
+            <Symbols />
+          </div>
+          <Footer />
+        </BrowserRouter>
+      </Provider>
     );
   }
 
index 50cf2c71777557293b218dd9f6eecedcf14b8e41..c9a647d61c3e72a686bdf6a1633f9784398ef564 100644 (file)
@@ -1,5 +1,5 @@
 export enum UserOperation {
-  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead
+  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead, SaveUserSettings, TransferCommunity, TransferSite
 }
 
 export enum CommentSortType {
@@ -15,13 +15,14 @@ export enum SortType {
 }
 
 export enum SearchType {
-  Both, Comments, Posts
+  All, Comments, Posts, Communities, Users, Url
 }
 
 export interface User {
   id: number;
   iss: string;
   username: string;
+  show_nsfw: boolean;
 }
 
 export interface UserView {
@@ -53,6 +54,7 @@ export interface Community {
   creator_id: number;
   removed: boolean;
   deleted: boolean;
+  nsfw: boolean;
   published: string;
   updated?: string;
   creator_name: string;
@@ -74,11 +76,14 @@ export interface Post {
   removed: boolean;
   deleted: boolean;
   locked: boolean;
+  nsfw: boolean;
   published: string;
   updated?: string;
   creator_name: string;
   community_name: string;
   community_removed: boolean;
+  community_deleted: boolean;
+  community_nsfw: boolean;
   number_of_comments: number;
   score: number;
   upvotes: number;
@@ -130,6 +135,7 @@ export interface Site {
   number_of_users: number;
   number_of_posts: number;
   number_of_comments: number;
+  number_of_communities: number;
 }
 
 export interface FollowCommunityForm {
@@ -197,6 +203,17 @@ export interface AddModToCommunityForm {
   auth?: string;
 }
 
+export interface TransferCommunityForm {
+  community_id: number;
+  user_id: number;
+  auth?: string;
+}
+
+export interface TransferSiteForm {
+  user_id: number;
+  auth?: string;
+}
+
 export interface AddModToCommunityResponse {
   op: string;
   moderators: Array<CommunityUser>;
@@ -222,16 +239,16 @@ export interface GetModlogResponse {
 }
 
 export interface ModRemovePost {
-    id: number;
-    mod_user_id: number;
-    post_id: number;
-    reason?: string;
-    removed?: boolean;
-    when_: string
-    mod_user_name: string;
-    post_name: string;
-    community_id: number;
-    community_name: string;
+  id: number;
+  mod_user_id: number;
+  post_id: number;
+  reason?: string;
+  removed?: boolean;
+  when_: string
+  mod_user_name: string;
+  post_name: string;
+  community_id: number;
+  community_name: string;
 }
 
 export interface ModLockPost {
@@ -334,6 +351,7 @@ export interface RegisterForm {
   password: string;
   password_verify: string;
   admin: boolean;
+  show_nsfw: boolean;
 }
 
 export interface LoginResponse {
@@ -341,7 +359,10 @@ export interface LoginResponse {
   jwt: string;
 }
 
-
+export interface UserSettingsForm {
+  show_nsfw: boolean;
+  auth: string;
+}
 
 export interface CommunityForm {
   name: string;
@@ -351,6 +372,7 @@ export interface CommunityForm {
   edit_id?: number;
   removed?: boolean;
   deleted?: boolean;
+  nsfw: boolean;
   reason?: string;
   expires?: number;
   auth?: string;
@@ -396,11 +418,19 @@ export interface PostForm {
   creator_id: number;
   removed?: boolean;
   deleted?: boolean;
+  nsfw: boolean;
   locked?: boolean;
   reason?: string;
   auth: string;
 }
 
+export interface PostFormParams {
+  name: string;
+  url?: string;
+  body?: string;
+  community?: string;
+}
+
 export interface GetPostResponse {
   op: string;
   post: Post;
@@ -540,6 +570,9 @@ export interface SearchForm {
 
 export interface SearchResponse {
   op: string;
+  type_: string;
   posts?: Array<Post>;
   comments?: Array<Comment>;
+  communities: Array<Community>;  
+  users: Array<UserView>;
 }
index d3259adb46718142bbd633f170eaee5e08036030..4a75b85a2cc587503b357caacc2eed0273466630 100644 (file)
@@ -16,12 +16,11 @@ export class UserService {
     } else {
       console.log('No JWT cookie found.');
     }
-
   }
 
   public login(res: LoginResponse) {
     this.setUser(res.jwt);
-    Cookies.set("jwt", res.jwt);
+    Cookies.set("jwt", res.jwt, { expires: 365 });
     console.log("jwt cookie set");
   }
 
index 2b30f7d8f763dd2e0483fd1c5d5874abbacc82ff..f67dbf6d94b82f11a4fcd2090a5d012b312b92f4 100644 (file)
@@ -1,14 +1,14 @@
 import { wsUri } from '../env';
-import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, SavePostForm, CommentForm, SaveCommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, AddAdminForm, BanUserForm, SiteForm, Site, UserView, GetRepliesForm, SearchForm } from '../interfaces';
+import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, SavePostForm, CommentForm, SaveCommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, TransferCommunityForm, AddAdminForm, TransferSiteForm, BanUserForm, SiteForm, Site, UserView, GetRepliesForm, SearchForm, UserSettingsForm } from '../interfaces';
 import { webSocket } from 'rxjs/webSocket';
 import { Subject } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import { UserService } from './';
+import { i18n } from '../i18next';
 
 export class WebSocketService {
   private static _instance: WebSocketService;
   public subject: Subject<any>;
-  public instanceName: string;
 
   public site: Site;
   public admins: Array<UserView>;
@@ -136,6 +136,16 @@ export class WebSocketService {
     this.subject.next(this.wsSendWrapper(UserOperation.AddModToCommunity, form));
   }
 
+  public transferCommunity(form: TransferCommunityForm) {
+    this.setAuth(form);
+    this.subject.next(this.wsSendWrapper(UserOperation.TransferCommunity, form));
+  }
+
+  public transferSite(form: TransferSiteForm) {
+    this.setAuth(form);
+    this.subject.next(this.wsSendWrapper(UserOperation.TransferSite, form));
+  }
+  
   public banUser(form: BanUserForm) {
     this.setAuth(form);
     this.subject.next(this.wsSendWrapper(UserOperation.BanUser, form));
@@ -171,7 +181,7 @@ export class WebSocketService {
   }
 
   public getSite() {
-    this.subject.next(this.wsSendWrapper(UserOperation.GetSite, {}));
+    this.subject.next(this.wsSendWrapper(UserOperation.GetSite, undefined));
   }
 
   public search(form: SearchForm) {
@@ -184,6 +194,11 @@ export class WebSocketService {
     this.subject.next(this.wsSendWrapper(UserOperation.MarkAllAsRead, form));
   }
 
+  public saveUserSettings(userSettingsForm: UserSettingsForm) {
+    this.setAuth(userSettingsForm);
+    this.subject.next(this.wsSendWrapper(UserOperation.SaveUserSettings, userSettingsForm));
+  }
+
   private wsSendWrapper(op: UserOperation, data: any) {
     let send = { op: UserOperation[op], data: data };
     console.log(send);
@@ -193,7 +208,7 @@ export class WebSocketService {
   private setAuth(obj: any, throwErr: boolean = true) {
     obj.auth = UserService.Instance.auth;
     if (obj.auth == null && throwErr) {
-      alert("Not logged in.");
+      alert(i18n.t('not_logged_in'));
       throw "Not logged in";
     }
   }
diff --git a/ui/src/translations/de.ts b/ui/src/translations/de.ts
new file mode 100644 (file)
index 0000000..8430d3a
--- /dev/null
@@ -0,0 +1,157 @@
+export const de = {
+  translation: {
+    post: 'post',
+    remove_post: 'Beitrag löschen',
+    no_posts: 'Keine Beiträge.',
+    create_a_post: 'Einen Beitrag anlegen',
+    create_post: 'Beitrag anlegen',
+    number_of_posts:'{{count}} Beiträge',
+    posts: 'Beiträge',
+    related_posts: 'Diese Beiträge könnten verwandt sein',
+    comments: 'Kommentare',
+    number_of_comments:'{{count}} Kommentare',
+    remove_comment: 'Kommentar löschen',
+    communities: 'Communities',
+    create_a_community: 'Eine community anlegen',
+    create_community: 'Community anlegen',
+    remove_community: 'Community entfernen',
+    subscribed_to_communities:'Abonnierte <1>communities</1>',
+    trending_communities:'Trending <1>communities</1>',
+    list_of_communities: 'Liste von communities',
+    community_reqs: 'Kleinbuchstaben, Großbuchstaben und keine Leerzeichen.',
+    edit: 'editieren',
+    reply: 'antworten',
+    cancel: 'Abbrechen',
+    unlock: 'entsperren',
+    lock: 'sperren',
+    link: 'link',
+    mod: 'mod',
+    mods: 'mods',
+    moderates: 'Moderiert',
+    remove_as_mod: 'Als mod entfernen',
+    appoint_as_mod: 'Zum mod ernennen',
+    modlog: 'Modlog',
+    admin: 'admin',
+    admins: 'admins',
+    remove_as_admin: 'Als admin entfernen',
+    appoint_as_admin: 'Zum admin ernennen',
+    remove: 'entfernen',
+    removed: 'entfernt',
+    locked: 'gesperrt',
+    reason: 'Grund',
+    mark_as_read: 'als gelesen markieren',
+    mark_as_unread: 'als ungelesen markieren',
+    delete: 'löschen',
+    deleted: 'gelöscht',
+    restore: 'wiederherstellen',
+    ban: 'bannen',
+    ban_from_site: 'Von der Seite bannen',
+    unban: 'entbannen',
+    unban_from_site: 'Von der Seite entbannen',
+    save: 'speichern',
+    unsave: 'unsave',
+    create: 'anlegen',
+    username: 'Username',
+    email_or_username: 'Email oder Username',
+    number_of_users:'{{count}} Benutzer',
+    number_of_subscribers:'{{count}} Abonnenten',
+    number_of_points:'{{count}} Punkte',
+    name: 'Name',
+    title: 'Titel',
+    category: 'Kategorie',
+    subscribers: 'Abonnenten',
+    both: 'Beide',
+    saved: 'Gespeichert',
+    unsubscribe: 'Abbestellen',
+    subscribe: 'Abonnieren',
+    prev: 'Zurück',
+    next: 'Weiter',
+    sidebar: 'Sidebar',
+    sort_type: 'Sortieren nach',
+    hot: 'Hot',
+    new: 'Neu',
+    top_day: 'Top täglich',
+    week: 'Woche',
+    month: 'Monat',
+    year: 'Jahr',
+    all: 'Alle',
+    top: 'Top',
+    api: 'API',
+    inbox: 'Posteingang',
+    inbox_for: 'Posteingang für <1>{{user}}</1>',
+    mark_all_as_read: 'Alle als gelesen markieren',
+    type: 'Typ',
+    unread: 'Ungelesen',
+    reply_sent: 'Antwort gesendet',
+    search: 'Suchen',
+    overview: 'Übersicht',
+    view: 'Ansicht',
+    logout: 'Ausloggen',
+    login_sign_up: 'Einloggen / Registrieren',
+    notifications_error: 'Desktop-Benachrichtigungen sind in deinem browser nicht verfügbar. Versuche Firefox oder Chrome.',
+    unread_messages: 'Ungelesene Nachrichten',
+    password: 'Passwort',
+    verify_password: 'Passwort überprüfen',
+    login: 'Einloggen',
+    sign_up: 'Registrieren',
+    email: 'Email',
+    optional: 'Optional',
+    url: 'URL',
+    body: 'Text',
+    copy_suggested_title: 'Vorgeschlagenen Titel übernehmen: {{title}}',
+    community: 'Community',
+    expand_here: 'Expand here',
+    subscribe_to_communities: 'Abonniere ein paar <1>communities</1>.',
+    chat: 'Chat',
+    no_results: 'Keine Ergebnisse.',
+    setup: 'Setup',
+    lemmy_instance_setup: 'Lemmy Instanz Setup',
+    setup_admin: 'Seiten Administrator konfigurieren',
+    your_site: 'deine Seite',
+    modified: 'verändert',
+    sponsors: 'Sponsoren',
+    sponsors_of_lemmy: 'Sponsoren von Lemmy',
+    sponsor_message: 'Lemmy ist freie <1>Open-Source</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:',
+    support_on_patreon: 'Auf Patreon unterstützen',
+    general_sponsors:'Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.',
+    bitcoin: 'Bitcoin',
+    ethereum: 'Ethereum',
+    code: 'Code',
+    powered_by: 'Bereitgestellt durch',
+    landing_0: 'Lemmy ist ein <1>Link Aggregator</1> / Reddit Alternative im <2>Fediverse</2>.<3></3>Es ist selbst-hostbar, hat live-updates von Kommentar-threads und ist winzig (<4>~80kB</4>). Federation in das ActivityPub Netzwerk ist geplant. <5></5>Dies ist eine <6>sehr frühe Beta Version</6>, und viele Features funktionieren zurzeit nicht richtig oder fehlen. <7></7>Schlage neue Features vor oder melde Bugs <8>hier.</8><9></9>Gebaut mit <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
+    not_logged_in: 'Nicht eingeloggt.',
+    community_ban: 'Du wurdest von dieser Community gebannt.',
+    site_ban: 'Du wurdest von dieser Seite gebannt',
+    couldnt_create_comment: 'Konnte Kommentar nicht anlegen.',
+    couldnt_like_comment: 'Konnte nicht liken.',
+    couldnt_update_comment: 'Konnte Kommentar nicht aktualisieren.',
+    couldnt_save_comment: 'Konnte Kommentar nicht speichern.',
+    no_comment_edit_allowed: 'Keine Erlaubnis Kommentar zu editieren.',
+    no_post_edit_allowed: 'Keine Erlaubnis Beitrag zu editieren.',
+    no_community_edit_allowed: 'Keine Erlaubnis Community zu editieren.',
+    couldnt_find_community: 'Konnte Community nicht finden.',
+    couldnt_update_community: 'Konnte Community nicht aktualisieren.',
+    community_already_exists: 'Community existiert bereits.',
+    community_moderator_already_exists: 'Community Moderator existiert bereits.',
+    community_follower_already_exists: 'Community Follower existiert bereits.',
+    community_user_already_banned: 'Community Nutzer schon gebannt.',
+    couldnt_create_post: 'Konnte Beitrag nicht anlegen.',
+    couldnt_like_post: 'Konnte Beitrag nicht liken.',
+    couldnt_find_post: 'Konnte Beitrag nicht finden.',
+    couldnt_get_posts: 'Konnte Beiträge nicht holen.',
+    couldnt_update_post: 'Konnte Beitrag nicht aktualisieren.',
+    couldnt_save_post: 'Konnte Beitrag nicht speichern.',
+    no_slurs: 'Keine Beleidigungen.',
+    not_an_admin: 'Kein Administrator.',
+    site_already_exists: 'Seite existiert bereits.',
+    couldnt_update_site: 'Konnte Seite nicht aktualisieren.',
+    couldnt_find_that_username_or_email: 'Konnte Username oder E-Mail nicht finden.',
+    password_incorrect: 'Passwort falsch.',
+    passwords_dont_match: 'Passwörter stimmen nicht überein.',
+    admin_already_created: 'Entschuldigung, es gibt schon einen Administrator.',
+    user_already_exists: 'Nutzer existiert bereits.',
+    couldnt_update_user: 'Konnte Nutzer nicht aktualisieren',
+    system_err_login: 'Systemfehler. Versuche dich aus- und wieder einzuloggen.',
+  },
+}
+
diff --git a/ui/src/translations/en.ts b/ui/src/translations/en.ts
new file mode 100644 (file)
index 0000000..10c1540
--- /dev/null
@@ -0,0 +1,177 @@
+export const en = {
+  translation: {
+    post: 'post',
+    remove_post: 'Remove Post',
+    no_posts: 'No Posts.',
+    create_a_post: 'Create a post',
+    create_post: 'Create Post',
+    number_of_posts:'{{count}} Posts',
+    posts: 'Posts',
+    related_posts: 'These posts might be related',
+    cross_posts: 'This link has also been posted to:',
+    cross_post: 'cross-post',
+    comments: 'Comments',
+    number_of_comments:'{{count}} Comments',
+    remove_comment: 'Remove Comment',
+    communities: 'Communities',
+    users: 'Users',
+    create_a_community: 'Create a community',
+    create_community: 'Create Community',
+    remove_community: 'Remove Community',
+    subscribed_to_communities:'Subscribed to <1>communities</1>',
+    trending_communities:'Trending <1>communities</1>',
+    list_of_communities: 'List of communities',
+    number_of_communities:'{{count}} Communities',
+    community_reqs: 'lowercase, underscores, and no spaces.',
+    edit: 'edit',
+    reply: 'reply',
+    cancel: 'Cancel',
+    unlock: 'unlock',
+    lock: 'lock',
+    link: 'link',
+    mod: 'mod',
+    mods: 'mods',
+    moderates: 'Moderates',
+    settings: 'Settings',
+    remove_as_mod: 'remove as mod',
+    appoint_as_mod: 'appoint as mod',
+    modlog: 'Modlog',
+    admin: 'admin',
+    admins: 'admins',
+    remove_as_admin: 'remove as admin',
+    appoint_as_admin: 'appoint as admin',
+    remove: 'remove',
+    removed: 'removed',
+    locked: 'locked',
+    reason: 'Reason',
+    mark_as_read: 'mark as read',
+    mark_as_unread: 'mark as unread',
+    delete: 'delete',
+    deleted: 'deleted',
+    restore: 'restore',
+    ban: 'ban',
+    ban_from_site: 'ban from site',
+    unban: 'unban',
+    unban_from_site: 'unban from site',
+    save: 'save',
+    unsave: 'unsave',
+    create: 'create',
+    username: 'Username',
+    email_or_username: 'Email or Username',
+    number_of_users:'{{count}} Users',
+    number_of_subscribers:'{{count}} Subscribers',
+    number_of_points:'{{count}} Points',
+    name: 'Name',
+    title: 'Title',
+    category: 'Category',
+    subscribers: 'Subscribers',
+    both: 'Both',
+    saved: 'Saved',
+    unsubscribe: 'Unsubscribe',
+    subscribe: 'Subscribe',
+    subscribed: 'Subscribed',
+    prev: 'Prev',
+    next: 'Next',
+    sidebar: 'Sidebar',
+    sort_type: 'Sort type',
+    hot: 'Hot',
+    new: 'New',
+    top_day: 'Top day',
+    week: 'Week',
+    month: 'Month',
+    year: 'Year',
+    all: 'All',
+    top: 'Top',
+    api: 'API',
+    inbox: 'Inbox',
+    inbox_for: 'Inbox for <1>{{user}}</1>',
+    mark_all_as_read: 'mark all as read',
+    type: 'Type',
+    unread: 'Unread',
+    reply_sent: 'Reply sent',
+    search: 'Search',
+    overview: 'Overview',
+    view: 'View',
+    logout: 'Logout',
+    login_sign_up: 'Login / Sign up',
+    login: 'Login',
+    sign_up: 'Sign Up',
+    notifications_error: 'Desktop notifications not available in your browser. Try Firefox or Chrome.',
+    unread_messages: 'Unread Messages',
+    password: 'Password',
+    verify_password: 'Verify Password',
+    email: 'Email',
+    optional: 'Optional',
+    expires: 'Expires',
+    url: 'URL',
+    body: 'Body',
+    copy_suggested_title: 'copy suggested title: {{title}}',
+    community: 'Community',
+    expand_here: 'Expand here',
+    subscribe_to_communities: 'Subscribe to some <1>communities</1>.',
+    chat: 'Chat',
+    recent_comments: 'Recent Comments',
+    no_results: 'No results.',
+    setup: 'Setup',
+    lemmy_instance_setup: 'Lemmy Instance Setup',
+    setup_admin: 'Set Up Site Administrator',
+    your_site: 'your site',
+    modified: 'modified',
+    nsfw: 'NSFW',
+    show_nsfw: 'Show NSFW content',
+    sponsors: 'Sponsors',
+    sponsors_of_lemmy: 'Sponsors of Lemmy',
+    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',
+    general_sponsors:'General Sponsors are those that pledged $10 to $39 to Lemmy.',
+    crypto: 'Crypto',
+    bitcoin: 'Bitcoin',
+    ethereum: 'Ethereum',
+    monero: 'Monero',
+    code: 'Code',
+    joined: 'Joined',
+    by: 'by',
+    to: 'to',
+    transfer_community: 'transfer community',
+    transfer_site: 'transfer site',
+    are_you_sure: 'are you sure?',
+    yes: 'yes',
+    no: 'no',
+    powered_by: 'Powered by',
+    landing_0: '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.',
+    community_ban: 'You have been banned from this community.',
+    site_ban: 'You have been banned from the site',
+    couldnt_create_comment: 'Couldn\'t create comment.',
+    couldnt_like_comment: 'Couldn\'t like comment.',
+    couldnt_update_comment: 'Couldn\'t update comment.',
+    couldnt_save_comment: 'Couldn\'t save comment.',
+    no_comment_edit_allowed: 'Not allowed to edit comment.',
+    no_post_edit_allowed: 'Not allowed to edit post.',
+    no_community_edit_allowed: 'Not allowed to edit community.',
+    couldnt_find_community: 'Couldn\'t find community.',
+    couldnt_update_community: 'Couldn\'t update Community.',
+    community_already_exists: 'Community already exists.',
+    community_moderator_already_exists: 'Community moderator already exists.',
+    community_follower_already_exists: 'Community follower already exists.',
+    community_user_already_banned: 'Community user already banned.',
+    couldnt_create_post: 'Couldn\'t create post.',
+    couldnt_like_post: 'Couldn\'t like post.',
+    couldnt_find_post: 'Couldn\'t find post.',
+    couldnt_get_posts: 'Couldn\'t get posts',
+    couldnt_update_post: 'Couldn\'t update post',
+    couldnt_save_post: 'Couldn\'t save post.',
+    no_slurs: 'No slurs.',
+    not_an_admin: 'Not an admin.',
+    site_already_exists: 'Site already exists.',
+    couldnt_update_site: 'Couldn\'t update site.',
+    couldnt_find_that_username_or_email: 'Couldn\'t find that username or email.',
+    password_incorrect: 'Password incorrect.',
+    passwords_dont_match: 'Passwords do not match.',
+    admin_already_created: 'Sorry, there\'s already an admin.',
+    user_already_exists: 'User already exists.',
+    couldnt_update_user: 'Couldn\'t update user.',
+    system_err_login: 'System error. Try logging out and back in.',
+  },
+}
+
diff --git a/ui/src/translations/eo.ts b/ui/src/translations/eo.ts
new file mode 100644 (file)
index 0000000..07ad7fb
--- /dev/null
@@ -0,0 +1,173 @@
+export const eo = {
+  translation: {
+    post: 'Poŝti',
+    remove_post: 'Fortiri Poŝton',
+    no_posts: 'Ne Poŝtoj.',
+    create_a_post: 'Verki Poŝton',
+    create_post: 'Verki Poŝton',
+    number_of_posts:'{{count}} Poŝtoj',
+    posts: 'Poŝtoj',
+    related_posts: 'Tiuj poŝtoj eble rilatas',
+    cross_posts: 'Tiuj ligilo ankaŭ estas poŝtinta al:',
+    cross_post: 'laŭapoŝto',
+    comments: 'Komentoj',
+    number_of_comments:'{{count}} Komentoj',
+    remove_comment: 'Fortiri Komentojn',
+    communities: 'Komunumoj',
+    users: 'Uzantoj',
+    create_a_community: 'Krei komunumon',
+    create_community: 'Krei Komunumon',
+    remove_community: 'Forigi Komunumon',
+    subscribed_to_communities:'Abonita al <1>komunumoj</1>',
+    trending_communities:'Furora <1>komunumoj</1>',
+    list_of_communities: 'Listo de komunumoj',
+    community_reqs: 'minusklaj leteroj, substrekoj, kaj ne spacetoj.',
+    edit: 'redakti',
+    reply: 'repliki',
+    cancel: 'nuligi',
+    unlock: 'malŝlosi',
+    lock: 'ŝlosi',
+    link: 'ligi',
+    mod: 'moderanto',
+    mods: 'moderantoj',
+    moderates: 'Moderigas',
+    settings: 'Agordoj',
+    remove_as_mod: 'forigi per moderanto',
+    appoint_as_mod: 'nomumi per moderanto',
+    modlog: 'Moderlogo',
+    admin: 'administranto',
+    admins: 'administrantoj',
+    remove_as_admin: 'forigi per administranto',
+    appoint_as_admin: 'nomumi per administranto',
+    remove: 'fortiri',
+    removed: 'fortirita',
+    locked: 'ŝlosita',
+    reason: 'Kialo',
+    mark_as_read: 'marki kiel legita',
+    mark_as_unread: 'marki kiel nelegita',
+    delete: 'forigi',
+    deleted: 'forigita',
+    restore: 'restaŭri',
+    ban: 'forbari',
+    ban_from_site: 'forbari de retejo',
+    unban: 'malforbari',
+    unban_from_site: 'malforbari de retejo',
+    save: 'konservi',
+    unsave: 'malkonservi',
+    create: 'krei',
+    username: 'Uzantnomo',
+    email_or_username: 'Retadreso aŭ Uzantnomo',
+    number_of_users:'{{count}} Uzantoj',
+    number_of_subscribers:'{{count}} Abonantoj',
+    number_of_points:'{{count}} Voĉdonoj',
+    name: 'Nomo',
+    title: 'Titolo',
+    category: 'Kategorio',
+    subscribers: 'Abonantoj',
+    both: 'Ambaŭ',
+    saved: 'Konservita',
+    unsubscribe: 'Malaboni',
+    subscribe: 'Aboni',
+    subscribed: 'Abonita',
+    prev: 'Antaŭe',
+    next: 'Poste',
+    sidebar: 'Flankstango',
+    sort_type: 'Klasi per kia',
+    hot: 'Varmaj',
+    new: 'Novaj',
+    top_day: 'Supraj tagaj',
+    week: 'Semajno',
+    month: 'Monato',
+    year: 'Jaro',
+    all: 'Ĉiam',
+    top: 'Supraj',
+    api: 'API',
+    inbox: 'Ricevujo',
+    inbox_for: 'Ricevujo de <1>{{user}}</1>',
+    mark_all_as_read: 'marki ĉiujn kiel legitaj',
+    type: 'Tipo',
+    unread: 'Nelegitaj',
+    reply_sent: 'Repliko sendis',
+    search: 'Serĉi',
+    overview: 'Resumo',
+    view: 'Rigardi',
+    logout: 'Elsaluti',
+    login_sign_up: 'Ensaluti / Registriĝi',
+    login: 'Ensaluti',
+    sign_up: 'Registriĝi',
+    notifications_error: 'Labortablaj avizoj estas nehavebla en via retumilo. Provu Firefox-on aŭ Chrome-on.',
+    unread_messages: 'Nelegitaj Mesaĝoj',
+    password: 'Pasvorto',
+    verify_password: 'Konfirmu Vian Pasvorton',
+    email: 'Retadreso',
+    optional: 'Fakultativa',
+    expires: 'Finiĝos',
+    url: 'URL',
+    body: 'Ĉefparto',
+    copy_suggested_title: 'kopii la sugestiitan titolon: {{title}}',
+    community: 'Komunumo',
+    expand_here: 'Ekspansii ĉi tie',
+    subscribe_to_communities: 'Aboni al iuj <1>komunumoj</1>.',
+    chat: 'Babilo',
+    recent_comments: 'Freŝaj Komentoj',
+    no_results: 'Ne rezultoj.',
+    setup: 'Agordi',
+    lemmy_instance_setup: 'Agordi Instancon de Lemmy',
+    setup_admin: 'Agordi Retejan Administranton',
+    your_site: 'via retejo',
+    modified: 'modifita',
+    nsfw: 'NSFW',
+    show_nsfw: 'Vidigi NSFW-an enhavon',
+    sponsors: 'Subtenantoj',
+    sponsors_of_lemmy: 'Subtenantoj de Lemmy',
+    sponsor_message: 'Lemmy estas senpaga, <1>liberkoda</1> programaro. Tio signifas ne reklami, pagigi, aŭ riska kapitalo, ĉiam. Viaj donacoj rekte subtenas plentempan evoluon de la projekto. Dankon al tiuj homoj:',
+    support_on_patreon: 'Subteni per Patreon',
+    general_sponsors:'Ĝeneralaj Subtenantoj estas tiuj ke donacis inter $10 kaj $39 al Lemmy.',
+    crypto: 'Crypto',
+    bitcoin: 'Bitcoin',
+    ethereum: 'Ethereum',
+    monero: 'Monero',
+    code: 'Kodo',
+    joined: 'Unuiĝis',
+    by: 'de',
+    to: 'al',
+    transfer_community: 'transdoni la komunumon',
+    transfer_site: 'transdoni la retejon',
+    powered_by: 'Konstruis per',
+    landing_0: 'Lemmy estas <1>ligila agregatilo</1> / Reddit anstataŭo ke intenciĝas funkci en la <2>federacio-universo</2>.<3></3>ĝi estas mem-gastigebla, havas nuna-ĝisdatigajn komentarojn, kaj estas malgrandega (<4>~80kB</4>). Federacio en la ActivityPub-an reton estas planizita. <5></5>Estas <6>fruega beta versio</6>, kaj multaj trajtoj estas nune difektaj aŭ mankaj. <7></7>Sugestias novajn trajtojn aŭ raportas cimojn <8>ĉi tie.</8><9></9>Faris per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
+    not_logged_in: 'Ne estas ensalutinta.',
+    community_ban: 'Vi estas forbarita de la komunumo.',
+    site_ban: 'Vi estas forbarita de la retejo',
+    couldnt_create_comment: 'Ne povis krei la komenton.',
+    couldnt_like_comment: 'Ne povis ŝati la komenton.',
+    couldnt_update_comment: 'Ne povis ĝisdatigi komenton.',
+    couldnt_save_comment: 'Ne povis konservi komenton.',
+    no_comment_edit_allowed: 'Ne rajtas redakti la komenton.',
+    no_post_edit_allowed: 'Ne rajtas redakti la poŝton.',
+    no_community_edit_allowed: 'Ne rajtas redakti la komunumon.',
+    couldnt_find_community: 'Ne povis trovi la komunumon.',
+    couldnt_update_community: 'Ne povis ĝisdatigi la komunumon.',
+    community_already_exists: 'Komunumo jam ekzistas.',
+    community_moderator_already_exists: 'Komunuma moderanto jam ekzistas.',
+    community_follower_already_exists: 'Komunuma sekvanto.',
+    community_user_already_banned: 'Komunuma uzanto jam estas forbarita.',
+    couldnt_create_post: 'Ne povis krei la poŝton.',
+    couldnt_like_post: 'Ne povis ŝati la poŝton.',
+    couldnt_find_post: 'Ne povis trovi la poŝton.',
+    couldnt_get_posts: 'Ne povis irpreni poŝtojn',
+    couldnt_update_post: 'Ne povis ĝisdatigi la poŝton',
+    couldnt_save_post: 'Ne povis konservi la poŝton.',
+    no_slurs: 'Ne bigotaj vortoj.',
+    not_an_admin: 'Ne estas administranto.',
+    site_already_exists: 'Retejo jam ekzistas.',
+    couldnt_update_site: 'Ne povis ĝisdatigi la retejon.',
+    couldnt_find_that_username_or_email: 'Ne povis trovi tiun uzantnomon aŭ retadreson.',
+    password_incorrect: 'Pasvorto malĝustas.',
+    passwords_dont_match: 'Pasvortoj ne samas.',
+    admin_already_created: 'Pardonu, jam estas administranto.',
+    user_already_exists: 'Uzanto jam ekzistas.',
+    couldnt_update_user: 'Ne povis ĝisdatigi la uzanton.',
+    system_err_login: 'Sistema eraro. Provu elsaluti kaj ensaluti.',
+  },
+}
+
diff --git a/ui/src/translations/es.ts b/ui/src/translations/es.ts
new file mode 100644 (file)
index 0000000..25e9d26
--- /dev/null
@@ -0,0 +1,173 @@
+export const es = {
+  translation: {
+    post: 'Publicar',
+    remove_post: 'Remover publicación',
+    no_posts: 'Sin publicaciones.',
+    create_a_post: 'Crear una publicación',
+    create_post: 'Crear Publicación',
+    number_of_posts:'{{count}} Publicaciones',
+    posts: 'Publicaciones',
+    related_posts: 'Estas publicaciones podrían estar relacionadas',
+    cross_posts: 'Este link también ha sido publicado en:',
+    cross_post: 'cross-post',
+    comments: 'Comentarios',
+    number_of_comments:'{{count}} Comentarios',
+    remove_comment: 'Remover Comentarios',
+    communities: 'Comunidades',
+    users: 'Usuarios',
+    create_a_community: 'Crear una comunidad',
+    create_community: 'Crear Comunidad',
+    remove_community: 'Remover Comunidad',
+    subscribed_to_communities:'Suscrito a <1>comunidades</1>',
+    trending_communities:'<1>Comunidades</1> en tendencia',
+    list_of_communities: 'Lista de comunidades',
+    community_reqs: 'minúsculas, guión bajo, y sin espacios.',
+    edit: 'editar',
+    reply: 'responder',
+    cancel: 'Cancelar',
+    unlock: 'desbloquear',
+    lock: 'bloquear',
+    link: 'link',
+    mod: 'Moderador',
+    mods: 'Moderadores',
+    moderates: 'Modera',
+    settings: 'Configuración',
+    remove_as_mod: 'remover como moderador',
+    appoint_as_mod: 'designar como moderador',
+    modlog: 'Historial de moderación',
+    admin: 'administrador',
+    admins: 'administradores',
+    remove_as_admin: 'remover como administrador',
+    appoint_as_admin: 'designar como administrador',
+    remove: 'remover',
+    removed: 'removido',
+    locked: 'bloqueado',
+    reason: 'Razón',
+    mark_as_read: 'marcar como leído',
+    mark_as_unread: 'marcar como no leído',
+    delete: 'eliminar',
+    deleted: 'eliminado',
+    restore: 'restaurar',
+    ban: 'expulsar',
+    ban_from_site: 'expulsión del sitio',
+    unban: 'admitir',
+    unban_from_site: 'admitir al sitio',
+    save: 'guardar',
+    unsave: 'descartar',
+    create: 'crear',
+    username: 'Nombre de Usuario',
+    email_or_username: 'Correo electrónico o Nombre de Usuario',
+    number_of_users:'{{count}} Usuarios',
+    number_of_subscribers:'{{count}} Suscriptores',
+    number_of_points:'{{count}} Puntos',
+    name: 'Nombre',
+    title: 'Titulo',
+    category: 'Categoría',
+    subscribers: 'Suscriptores',
+    both: 'Ambos',
+    saved: 'Guardado',
+    unsubscribe: 'Abandonar comunidad',
+    subscribe: 'Suscribir',
+    subscribed: 'Suscrito',
+    prev: 'Anterior',
+    next: 'Siguiente',
+    sidebar: 'Descripción de la comunidad',
+    sort_type: 'Tipo de orden',
+    hot: 'Popular',
+    new: 'Nuevo',
+    top_day: 'Lo mejor del día',
+    week: 'Semana',
+    month: 'Mes',
+    year: 'Año',
+    all: 'Todo',
+    top: 'Mejor',
+    api: 'API',
+    inbox: 'Buzón de entrada',
+    inbox_for: 'Buzón de entrada para <1>{{user}}</1>',
+    mark_all_as_read: 'marcar todo como leído',
+    type: 'Tipo',
+    unread: 'No leído',
+    reply_sent: 'Respuesta enviada',
+    search: 'Buscar',
+    overview: 'Resumen',
+    view: 'Vista',
+    logout: 'Cerrar sesión',
+    login_sign_up: 'Iniciar sesión / Crear cuenta',
+    login: 'Iniciar sesión',
+    sign_up: 'Crear cuenta',
+    notifications_error: 'Notificaciones de escritorio no disponibles en tu navegador. Prueba Firefox o Chrome.',
+    unread_messages: 'Mensajes no leídos',
+    password: 'Contraseña',
+    verify_password: 'Verificar contraseña',
+    email: 'Correo electrónico',
+    optional: 'Opcional',
+    expires: 'Expira',
+    url: 'URL',
+    body: 'Descripción',
+    copy_suggested_title: 'Copiar el título sugerido: {{title}}',
+    community: 'Comunidad',
+    expand_here: 'Expandir aquí',
+    subscribe_to_communities: 'Suscribirse a algunas <1>comunidades</1>.',
+    chat: 'Chat',
+    recent_comments: 'Comentarios recientes',
+    no_results: 'Sin resultados.',
+    setup: 'Configurar',
+    lemmy_instance_setup: 'Configuración de instancia de Lemmy',
+    setup_admin: 'Configurar administrador del Sitio',
+    your_site: 'tu sitio',
+    modified: 'modificado',
+    nsfw: 'NSFW',
+    show_nsfw: 'Mostrar contenido NSFW',
+    sponsors: 'Patrocinadores',
+    sponsors_of_lemmy: 'Patrocinadores of Lemmy',
+    sponsor_message: 'Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:',
+    support_on_patreon: 'Apoyo en Patreon',
+    general_sponsors:'Patrocinadores Generales son aquellos que señaron entre $10 y $39 a Lemmy.',
+    crypto: 'Crypto',
+    bitcoin: 'Bitcoin',
+    ethereum: 'Ethereum',
+    monero: 'Monero',
+    code: 'Código',
+    joined: 'Se unió',
+    by: 'por',
+    to: 'en',
+    transfer_community: 'transferir comunidad',
+    transfer_site: 'transferir sitio',
+    powered_by: 'Impulsado por',
+    landing_0: 'Lemmy es un <1>agregador de links</1> / alternativa a reddit, con la intención de funcionar en el <2>fediverso</2>.<3></3>Es alojable por uno mismo (sin necesidad de grandes compañías), tiene actualización en vivo de cadenas de comentarios, y es pequeño (<4>~80kB</4>). Federar con el sistema de redes ActivityPub forma parte de los objetivos del proyecto. <5></5>Esta es una <6>version beta muy prematura</6>, y actualmente muchas de las características están rotas o faltan. <7></7>Sugiere nuevas características o reporta errores <8>aquí</8>.<9></9>Hecho con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
+    not_logged_in: 'No has iniciado sesión.',
+    community_ban: 'Has sido expulsado de esta comunidad.',
+    site_ban: 'Has sido expulsado del sitio',
+    couldnt_create_comment: 'No se pudo crear el comentario.',
+    couldnt_like_comment: 'No se pudo gustar el comentario.',
+    couldnt_update_comment: 'No se pudo actualizar el comentario.',
+    couldnt_save_comment: 'No se pudo guardar el comentario.',
+    no_comment_edit_allowed: 'No tiene permitido editar el comentario.',
+    no_post_edit_allowed: 'No tiene permitido editar la publicación.',
+    no_community_edit_allowed: 'No tiene permitido editar la comunidad.',
+    couldnt_find_community: 'No se pudo encontrar la comunidad.',
+    couldnt_update_community: 'No se pudo actualizar la comunidad.',
+    community_already_exists: 'Esta comunidad ya existe.',
+    community_moderator_already_exists: 'Este moderador de la comunidad ya existe.',
+    community_follower_already_exists: 'Este seguidor de la comunidad ya existe.',
+    community_user_already_banned: 'Este usuario de la comunidad ya fue expulsado.',
+    couldnt_create_post: 'No se pudo crear la publicación.',
+    couldnt_like_post: 'No se pudo gustar la publicación.',
+    couldnt_find_post: 'No se pudo encontrar la publicación.',
+    couldnt_get_posts: 'No se pudo obtener las publicaciones',
+    couldnt_update_post: 'No se pudo actualizar la publicación',
+    couldnt_save_post: 'No se pudo guardar la publicación.',
+    no_slurs: 'Prohibido insultar.',
+    not_an_admin: 'No es un administrador.',
+    site_already_exists: 'El sitio ya existe.',
+    couldnt_update_site: 'No se pudo actualizar el sitio.',
+    couldnt_find_that_username_or_email: 'No se pudo encontrar ese nombre de usuario o correo electrónico.',
+    password_incorrect: 'Contraseña incorrecta.',
+    passwords_dont_match: 'Las contraseñas no coinciden.',
+    admin_already_created: 'Lo sentimos, ya hay un adminisitrador.',
+    user_already_exists: 'El usuario ya existe.',
+    couldnt_update_user: 'No se pudo actualizar el usuario.',
+    system_err_login: 'Error del sistema. Intente cerrar sesión e ingresar de nuevo.',
+  },
+}
+
diff --git a/ui/src/translations/fr.ts b/ui/src/translations/fr.ts
new file mode 100644 (file)
index 0000000..44f527a
--- /dev/null
@@ -0,0 +1,160 @@
+export const fr = {
+  translation: {
+    post: 'sujet',
+    remove_post: 'Supprimer le sujet',
+    no_posts: 'Pas de sujets.',
+    create_a_post: 'Créer un sujet',
+    create_post: 'Créer le sujet',
+    number_of_posts:'{{count}} Sujets',
+    posts: 'Sujets',
+    related_posts: 'Ces sujets peuvent être corrélés',
+    comments: 'Commentaires',
+    number_of_comments:'{{count}} Commentaires',
+    remove_comment: 'Supprimer le commentaire',
+    communities: 'Communautés',
+    create_a_community: 'Créer une communauté',
+    create_community: 'Créer la communauté',
+    remove_community: 'Supprimer la Communauté',
+    subscribed_to_communities:'Abonné à ces <1>communautés</1>',
+    trending_communities:'<1>Communauté</1> en vogue',
+    list_of_communities: 'Liste des communautés',
+    community_reqs: 'en minuscule, sans espace et avec tiret du bas.',
+    edit: 'éditer',
+    reply: 'répondre',
+    cancel: 'Annuler',
+    unlock: 'débloquer',
+    lock: 'bloquer',
+    link: 'lien',
+    mod: 'modérateur',
+    mods: 'modérateurs',
+    moderates: 'Modération',
+    remove_as_mod: 'Supprimer comme modérateur',
+    appoint_as_mod: 'Nommer comme modérateur',
+    modlog: 'Historique de modération',
+    admin: 'admin',
+    admins: 'admins',
+    remove_as_admin: 'Supprimer comme admin',
+    appoint_as_admin: 'Nommer comme admin',
+    remove: 'retirer',
+    removed: 'retiré',
+    locked: 'bloqué',
+    reason: 'Raison',
+    mark_as_read: 'marquer comme lu',
+    mark_as_unread: 'marquer comme non-lu',
+    delete: 'supprimer',
+    deleted: 'supprimé',
+    restore: 'restaurer',
+    ban: 'bannir',
+    ban_from_site: 'bannir du site',
+    unban: 'débannir',
+    unban_from_site: 'débannir du site',
+    save: 'sauvegarder',
+    unsave: 'retirer',
+    create: 'créer',
+    username: 'Nom d\'utilisateur',
+    email_or_username: 'Email ou Nom d\'utilisateur',
+    number_of_users:'{{count}} Utilisateurs',
+    number_of_subscribers:'{{count}} Abonnés',
+    number_of_points:'{{count}} Points',
+    name: 'Nom',
+    title: 'Titre',
+    category: 'Catégorie',
+    subscribers: 'Abonnés',
+    both: 'Les deux',
+    saved: 'Sauvegardé',
+    unsubscribe: 'Se désincrire',
+    subscribe: 'S\'inscrire',
+    subscribed: 'Inscris',
+    prev: 'Précédent',
+    next: 'Suivant',
+    sidebar: 'Texte latéral',
+    sort_type: 'Trier',
+    hot: 'Chaud',
+    new: 'Nouveau',
+    top_day: 'Top jour',
+    week: 'Semaine',
+    month: 'Mois',
+    year: 'Année',
+    all: 'Tout',
+    top: 'Top',
+    api: 'API',
+    inbox: 'Boîte de réception',
+    inbox_for: 'Boîte de réception de <1>{{user}}</1>',
+    mark_all_as_read: 'Tout marquer comme lu',
+    type: 'Type',
+    unread: 'Non-lu',
+    reply_sent: 'Réponse envoyée',
+    search: 'Rechercher',
+    overview: 'Général',
+    view: 'Voir',
+    logout: 'Se déconnecter',
+    login_sign_up: 'Se connecter / S\'inscrire',
+    login: 'Se connecter',
+    sign_up: 'S\'inscrire',
+    notifications_error: 'Les notifications de bureau ne sont pas discponibles sur votre navigateur. Essayez Firefox ou Chrome.',
+    unread_messages: 'Messages non-lu',
+    password: 'Mot de passe',
+    verify_password: 'Vérifiez le mot de passe',
+    email: 'Email',
+    optional: 'Optionnel',
+    expires: 'Expire',
+    url: 'URL',
+    body: 'Texte',
+    copy_suggested_title: 'Ajouter le titre suggéré: {{title}}',
+    community: 'Communauté',
+    expand_here: 'Développer ici',
+    subscribe_to_communities: 'S\'abonner à quelques <1>communautés</1>.',
+    chat: 'Chat',
+    no_results: 'Pas de résultats.',
+    setup: 'Installation',
+    lemmy_instance_setup: 'Installation d\'une instance Lemmy',
+    setup_admin: 'Créer un administrateur',
+    your_site: 'votre site',
+    modified: 'modifié',
+    sponsors: 'Sponsors',
+    sponsors_of_lemmy: 'Sponsors de Lemmy',
+    sponsor_message: 'Lemmy est gratuit et <1>open-source</1>, c\'est à dire sans publicité et sans monétisation. Pour toujours. Vos dons soutiennent directement le développement du projet. Merci à nos soutiens.',
+    support_on_patreon: 'Soutenir sur Patreon',
+    general_sponsors:'General Sponsors are those that pledged $10 to $39 to Lemmy.',
+    crypto: 'Crypto',
+    bitcoin: 'Bitcoin',
+    ethereum: 'Ethereum',
+    code: 'Code',
+    joined: 'Membre depuis',
+    powered_by: 'Propulsé par',
+    landing_0: 'Lemmy est un <1>aggrégateur de lien</1>, similaire à reddit et conçu pour fonctionner sur le <2>fédiverse</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via Activitypub est prévue sur la feuille de route. <5></5>Lemmy est une <6>version beta très précoce</6>, et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez rapporter des bugs et suggérez de nouvelles fonctionnalités <8>ici.</8><9></9>Crée avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
+    not_logged_in: 'Vous n\'êtes pas connecté.',
+    community_ban: 'Vous avez été banni de cette communauté.',
+    site_ban: 'Vous avez été banni du site',
+    couldnt_create_comment: 'Impossible de poster le commentaire.',
+    couldnt_like_comment: 'Impossible d\'aimer le commentaire.',
+    couldnt_update_comment: 'Impossible de mettre à jour le commentaire.',
+    couldnt_save_comment: 'Impossible de sauvegarder le commentaire.',
+    no_comment_edit_allowed: 'Vous n\'êtes pas autorisé à éditer ce commentaire.',
+    no_post_edit_allowed: 'ous n\'êtes pas autorisé à éditer sujet.',
+    no_community_edit_allowed: 'ous n\'êtes pas autorisé à éditer cette communauté.',
+    couldnt_find_community: 'Impossible de trouver cette communauté.',
+    couldnt_update_community: 'Impossible d\'éditer cette communauté.',
+    community_already_exists: 'Cette communauté existe déjà.',
+    community_moderator_already_exists: 'Ce membre est déjà modérateur.',
+    community_follower_already_exists: 'Ce membre est déjà abonné.',
+    community_user_already_banned: 'Ce membre est déjà banni.',
+    couldnt_create_post: 'Impossible dae créer le sujet.',
+    couldnt_like_post: 'Impossible d\'aimer le sujet.',
+    couldnt_find_post: 'Impossible de trouver le sujet.',
+    couldnt_get_posts: 'Impossible d\'obtenir les sujets',
+    couldnt_update_post: 'Impossible de mettre à jour le sujet',
+    couldnt_save_post: 'Impossible de sauvegarder le sujet.',
+    no_slurs: 'Pas d\'insultes.',
+    not_an_admin: 'Pas administrateur.',
+    site_already_exists: 'Le site existe déjà.',
+    couldnt_update_site: 'Impossible de mettre à jour le site.',
+    couldnt_find_that_username_or_email: 'Impossible de trouver cet utilisateur ou cet email.',
+    password_incorrect: 'Mot de passe incorrect.',
+    passwords_dont_match: 'Les mots de passes ne correspondent pas..',
+    admin_already_created: 'Désolé, il y a déjà un admin.',
+    user_already_exists: 'L\'utilisateur existe déjà.',
+    couldnt_update_user: 'Impossible de mettre à jour l\'utilisateur.',
+    system_err_login: 'Erreur système. Essayez de vous déconneter puis de vous reconnecter.',
+  },
+}
diff --git a/ui/src/translations/nl.ts b/ui/src/translations/nl.ts
new file mode 100644 (file)
index 0000000..33fc24e
--- /dev/null
@@ -0,0 +1,176 @@
+export const nl = {
+  translation: {
+    post: 'post',
+    remove_post: 'Verwijder post',
+    no_posts: 'Geen posts.',
+    create_a_post: 'Plaats een post',
+    create_post: 'Plaats post',
+    number_of_posts:'{{count}} posts',
+    posts: 'posts',
+    related_posts: 'Deze posts kunnen gerelateerd zijn',
+    cross_posts: 'Deze link is ook geplaatst in:',
+    cross_post: 'cross-post',
+    comments: 'Reacties',
+    number_of_comments:'{{count}} reacties',
+    remove_comment: 'Verwijder reactie',
+    communities: 'Communities',
+    users: 'Gebruikers',
+    create_a_community: 'Maak een community',
+    create_community: 'Maak community',
+    remove_community: 'Verwijder community',
+    subscribed_to_communities:'Geabonneerd op <1>communities</1>',
+    trending_communities:'Populaire <1>communities</1>',
+    list_of_communities: 'Lijst van communities',
+    number_of_communities:'{{count}} communities',
+    community_reqs: 'kleine letters, onderstrepingsteken en geen spaties',
+    edit: 'bewerk',
+    reply: 'reageer',
+    cancel: 'Annuleer',
+    unlock: 'ontsluiten',
+    lock: 'sluiten',
+    link: 'link',
+    mod: 'moderator',
+    mods: 'moderators',
+    moderates: 'Modereert',
+    settings: 'Instellingen',
+    remove_as_mod: 'Verwijder als moderator',
+    appoint_as_mod: 'Benoemen tot moderator',
+    modlog: 'Moderatorlog',
+    admin: 'beheerder',
+    admins: 'beheerders',
+    remove_as_admin: 'verwijder als beheerder',
+    appoint_as_admin: 'benoemen tot beheerder',
+    remove: 'weghalen',
+    removed: 'weggehaald',
+    locked: 'gesloten',
+    reason: 'Reden',
+    mark_as_read: 'markeer als gelezen',
+    mark_as_unread: 'markeer als ongelezen',
+    delete: 'verwijder',
+    deleted: 'verwijderd',
+    restore: 'herstellen',
+    ban: 'verban',
+    ban_from_site: 'verban van site',
+    unban: 'verbanning opzeggen',
+    unban_from_site: 'verbanning van site opzeggen',
+    save: 'opslaan',
+    unsave: 'unsave',
+    create: 'maak',
+    username: 'Gebruikersnaam',
+    email_or_username: 'E-mail of gebruikersnaam',
+    number_of_users:'{{count}} gebruikers',
+    number_of_subscribers:'{{count}} abonnees',
+    number_of_points:'{{count}} punten',
+    name: 'Naam',
+    title: 'Titel',
+    category: 'Categorie',
+    subscribers: 'Abonnees',
+    both: 'Beide',
+    saved: 'Opgeslagen',
+    unsubscribe: 'Afmelden',
+    subscribe: 'Abonneren',
+    subscribed: 'Geabonneerd',
+    prev: 'Vorige',
+    next: 'Volgende',
+    sidebar: 'Zijbalk',
+    sort_type: 'Sorteertype',
+    hot: 'Populair',
+    new: 'Nieuw',
+    top_day: 'Dagelijkse top',
+    week: 'Week',
+    month: 'Maand',
+    year: 'Jaar',
+    all: 'Alle',
+    top: 'Top',
+    api: 'API',
+    inbox: 'Postvak-in',
+    inbox_for: 'Postvak-in voor <1>{{user}}</1>',
+    mark_all_as_read: 'markeer alle als gelezen',
+    type: 'Type',
+    unread: 'Ongelezen',
+    reply_sent: 'Reactie gestuurd',
+    search: 'Zoek',
+    overview: 'Overzicht',
+    view: 'Beeld',
+    logout: 'Log uit',
+    login_sign_up: 'Log in / Aanmelden',
+    login: 'Log in',
+    sign_up: 'Aanmelden',
+    notifications_error: 'Bureabladberichten niet beschikbaar in je browser. Probeer Firefox of Chrome.',
+    unread_messages: 'Ongelezen berichten',
+    password: 'Wachtwoord',
+    verify_password: 'Herhaal wachtwoord',
+    email: 'E-mail',
+    optional: 'Optioneel',
+    expires: 'Verloopt',
+    url: 'url',
+    body: 'Tekst',
+    copy_suggested_title: 'neem voorgestelde titel over: {{title}}',
+    community: 'Community',
+    expand_here: 'Breid hier uit',
+    subscribe_to_communities: 'Abonneer je op een paar <1>communities</1>.',
+    chat: 'Praat',
+    recent_comments: 'Recente reacties',
+    no_results: 'Geen resultaten',
+    setup: 'Installatie',
+    lemmy_instance_setup: 'Installatie van Lemmy-instantie',
+    setup_admin: 'Maak een administrator',
+    your_site: 'jouw site',
+    modified: 'bewerkt',
+    nsfw: 'NSFW',
+    show_nsfw: 'Laat NSFW-inhoud zien',
+    sponsors: 'Sponsoren',
+    sponsors_of_lemmy: 'Sponsoren van Lemmy',
+    sponsor_message: 'Lemmy is vrije, <1>open-source</1> software, dus zonder reclame, winstoogmerk en durfkapitaal, punt. Jouw donaties gaan direct naar de full-time-ontwikkeling van het project. Met veel dank aan de volgende mensen:',
+    support_on_patreon: 'Ondersteun op Patreon',
+    general_sponsors:'Algemene sponsors zijn sponsors die tussen de $10 en $39 hebben gegeven aan Lemmy.',
+    crypto: 'Cryptovaluta',
+    bitcoin: 'Bitcoin',
+    ethereum: 'Ethereum',
+    monero: 'Monero',
+    code: 'Code',
+    joined: 'toegetreden',
+    by: 'door',
+    to: 'aan',
+    transfer_community: 'community overplaatsen',
+    transfer_site: 'site overplaatsen',
+    are_you_sure: 'weet je het zeker?',
+    yes: 'ja',
+    no: 'nee',
+    powered_by: 'Mogelijk gemaakt door',
+    landing_0: 'Lemmy is een <1>linkverzameler</1> / reddit-alternatief, bedoeld om in de <2>fediverse</2> te werken.<3></3>Lemmy kan door om het even wie gehost worden, heeft live-bijgewerkte reacties en is superklein (<4>ca. 80 kB</4>). Federatie in hte ActivityPub-netwerk is gepland. <5></5>Dit is een <6>erg vroege bèta-versie</6>, en een hoop functies zijn stuk of afwezig. <7></7>Stel nieuwe functies voor of meldt fouten <8>hier</8>.<9></9>Gemaakt met <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> en <13>Typescript</13>.',
+    not_logged_in: 'Niet ingelogd.',
+    community_ban: 'Je bent verbannen uit deze community.',
+    site_ban: 'Je bent verbannen van deze site.',
+    couldnt_create_comment: 'Kon reactie niet maken.',
+    couldnt_like_comment: 'Kon reactie niet leuk vinden.',
+    couldnt_update_comment: 'Kon reactie niet bijwerken.',
+    couldnt_save_comment: 'Kon reactie niet opslaan.',
+    no_comment_edit_allowed: 'Niet toegestaan om reactie te bewerken.',
+    no_post_edit_allowed: 'Niet toegestaan om posts te bewerken.',
+    no_community_edit_allowed: 'Niet toegestaan om community te bewerken.',
+    couldnt_find_community: 'Kon community niet vinden.',
+    couldnt_update_community: 'Kon community niet bijwerken.',
+    community_already_exists: 'Community bestaat al.',
+    community_moderator_already_exists: 'Community-moderator bestaat al.',
+    community_follower_already_exists: 'Community-volger bestaat al.',
+    community_user_already_banned: 'Community-gebruiker reeds verbannen.',
+    couldnt_create_post: 'Kon post niet maken.',
+    couldnt_like_post: 'Kon post niet leuk vinden.',
+    couldnt_find_post: 'Kon post niet vinden.',
+    couldnt_get_posts: 'Kon posts niet ophalen.',
+    couldnt_update_post: 'Kon post niet bijwerken.',
+    couldnt_save_post: 'Kon post niet opslaan.',
+    no_slurs: 'Geen beledigingen.',
+    not_an_admin: 'Niet een beheerder.',
+    site_already_exists: 'Site bestaat al.',
+    couldnt_update_site: 'Kon site niet bijwerken.',
+    couldnt_find_that_username_or_email: 'Kon gebruikersnaam of e-mailadres niet vinden.',
+    password_incorrect: 'Wachtwoord incorrect.',
+    passwords_dont_match: 'Wachtwoorden zijn niet gelijk.',
+    admin_already_created: 'Sorry, er is al een beheerder.',
+    user_already_exists: 'Gebruiker bestaat al.',
+    couldnt_update_user: 'Kon gebruiker niet bijwerken.',
+    system_err_login: 'Systeemfout. Probeer uit te loggen en weer in te loggen.',
+  },
+}
diff --git a/ui/src/translations/ru.ts b/ui/src/translations/ru.ts
new file mode 100644 (file)
index 0000000..71e2126
--- /dev/null
@@ -0,0 +1,165 @@
+export const ru = {
+  translation: {
+    post: 'запись',
+    remove_post: 'Удалить запись',
+    no_posts: 'Нет записей.',
+    create_a_post: 'Создать запись',
+    create_post: 'Создать запись',
+    number_of_posts:'{{count}} записей',
+    posts: 'Записи',
+    related_posts: 'Эти записи могут быть связаны',
+    comments: 'Комментарии',
+    number_of_comments:'{{count}} комментариев',
+    remove_comment: 'Удалить комментарий',
+    communities: 'Сообщества',
+    users: 'Пользователи',
+    create_a_community: 'Создать сообщество',
+    create_community: 'Создать сообщество',
+    remove_community: 'Удалить сообщество',
+    subscribed_to_communities:'Подписаны на <1>сообщества</1>',
+    trending_communities:'<1>Сообщества</1> в тренде',
+    list_of_communities: 'Список сообществ',
+    community_reqs: 'строчными буквами, подчеркиваниями и без пробелов.',
+    edit: 'редактировать',
+    reply: 'ответить',
+    cancel: 'Отмена',
+    unlock: 'разблокировать',
+    lock: 'заблокировать',
+    link: 'ссылка',
+    mod: 'модератор',
+    mods: 'модераторы',
+    moderates: 'Модерация',
+    settings: 'Настройки',
+    remove_as_mod: 'снять из модераторов',
+    appoint_as_mod: 'назначить модератором',
+    modlog: 'Модлог',
+    admin: 'администратор',
+    admins: 'администраторы',
+    remove_as_admin: 'снять из администраторов',
+    appoint_as_admin: 'назначить администратором',
+    remove: 'убрать',
+    removed: 'убрано',
+    locked: 'заблокировано',
+    reason: 'Причина',
+    mark_as_read: 'пометить как прочитанное',
+    mark_as_unread: 'пометить как непрочитанное',
+    delete: 'удалить',
+    deleted: 'удалено',
+    restore: 'восстановить',
+    ban: 'заблокировать',
+    ban_from_site: 'заблокировать на сайте',
+    unban: 'разблокировать',
+    unban_from_site: 'разблокировать на сайте',
+    save: 'сохранить',
+    unsave: 'не сохранять',
+    create: 'создать',
+    username: 'Имя пользователя',
+    email_or_username: 'Электронная почта или имя пользователя',
+    number_of_users:'{{count}} пользователей',
+    number_of_subscribers:'{{count}} подписчиков',
+    number_of_points:'{{count}} баллов',
+    name: 'Имя',
+    title: 'Название',
+    category: 'Категория',
+    subscribers: 'Подписчики',
+    both: 'Оба',
+    saved: 'Сохранено',
+    unsubscribe: 'Отписаться',
+    subscribe: 'Подписаться',
+    subscribed: 'Подписаны',
+    prev: 'Назад',
+    next: 'Далее',
+    sidebar: 'Боковая панель',
+    sort_type: 'Тип сортировки',
+    hot: 'Популярно',
+    new: 'Новое',
+    top_day: 'Лучшее за день',
+    week: 'Неделя',
+    month: 'Месяц',
+    year: 'Год',
+    all: 'Всё',
+    top: 'Лучшее',
+    api: 'API',
+    inbox: 'Входящие',
+    inbox_for: 'Входящие сообщения для <1>{{user}}</1>',
+    mark_all_as_read: 'пометить все как прочитанные',
+    type: 'Тип',
+    unread: 'Не прочитано',
+    reply_sent: 'Ответ отправлен',
+    search: 'Поиск',
+    overview: 'Обзор',
+    view: 'Просмотр',
+    logout: 'Выйти',
+    login_sign_up: 'Войти / Регистрация',
+    login: 'Авторизация',
+    sign_up: 'Регистрация',
+    notifications_error: 'Уведомления на рабочем столе недоступны в вашем браузере. Попробуйте Firefox или Chrome.',
+    unread_messages: 'Непрочитанные сообщения',
+    password: 'Пароль',
+    verify_password: 'Повторите пароль',
+    email: 'Электронная почта',
+    optional: 'Необязательно',
+    expires: 'Истёк',
+    url: 'URL',
+    body: 'Тело',
+    copy_suggested_title: 'предложенное название: {{title}}',
+    community: 'Сообщество',
+    expand_here: 'Расширить здесь',
+    subscribe_to_communities: 'Подпишитесь на некоторые <1>сообщества</1>.',
+    chat: 'Чат',
+    no_results: 'Нет результатов.',
+    setup: 'Установка',
+    lemmy_instance_setup: 'Установка инстанции Lemmy',
+    setup_admin: 'Настройка администратора сайта',
+    your_site: 'ваш сайт',
+    modified: 'изменено',
+    nsfw: 'NSFW',
+    show_nsfw: 'Показывать NSFW-контент',
+    sponsors: 'Спонсоры',
+    sponsors_of_lemmy: 'Спонсоры Lemmy',
+    sponsor_message: 'Lemmy это бесплатное, <1>открытое</1> программное обеспечение, что означает отсутствие рекламы, монетизации или венчурного капитала, никогда. Ваши пожертвования напрямую поддерживают развитие проекта. Спасибо нижеуказанным людям:',
+    support_on_patreon: 'Поддержать на Patreon',
+    general_sponsors:'Генеральные спонсоры - это те, кто пообещал Lemmy от $10 до $39.',
+    crypto: 'Крипто',
+    bitcoin: 'Bitcoin',
+    ethereum: 'Ethereum',
+    code: 'Код',
+    joined: 'Присоединился',
+    powered_by: 'Работает на',
+    landing_0: 'Lemmy - это <1>агрегатор ссылок</1> / альтернатива reddit, предназначенный для работы в <2>федиверсе</2>.<3></3>Это самодостаточная система, с обновляемыми комментариями, и эта система крошечная (<4>~80 Кб</4>). Федерация в сети ActivityPub находится в разработке. <5></5>Это <6>очень ранняя бета-версия</6>, и многие функции в настоящее время сломаны или отсутствуют. <7></7>Предлагать новые функции или сообщать об ошибках можно <8>здесь.</8><9></9>Сделано на <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
+    not_logged_in: 'Не авторизованы.',
+    community_ban: 'Вы были заблокированы на данном сообществе.',
+    site_ban: 'Вы были заблокированы на данном сайте',
+    couldnt_create_comment: 'Не получилось создать комментарий.',
+    couldnt_like_comment: 'Не получилось лайкнуть комментарий.',
+    couldnt_update_comment: 'Не получилось обновить комментарий.',
+    couldnt_save_comment: 'Не получилось сохранить комментарий.',
+    no_comment_edit_allowed: 'Невозможно отредактировать комментарий.',
+    no_post_edit_allowed: 'Невозможно отредактировать запись.',
+    no_community_edit_allowed: 'Невозможно отредактировать сообщество.',
+    couldnt_find_community: 'Не получилось найти сообщество.',
+    couldnt_update_community: 'Не получилось обновить сообщество.',
+    community_already_exists: 'Сообщество уже существует.',
+    community_moderator_already_exists: 'Модератор сообщества уже существует.',
+    community_follower_already_exists: 'Подписчик сообщества уже существует.',
+    community_user_already_banned: 'Пользователь сообщества уже заблокирован.',
+    couldnt_create_post: 'Не получилось создать запись.',
+    couldnt_like_post: 'Не получилось лайкнуть запись.',
+    couldnt_find_post: 'Не получилось найти запись.',
+    couldnt_get_posts: 'Не получилось найти записи',
+    couldnt_update_post: 'Не получилось обновить запись',
+    couldnt_save_post: 'Не получилось сохранить запись.',
+    no_slurs: 'Без оскорблений.',
+    not_an_admin: 'Не администратор.',
+    site_already_exists: 'Сайт уже существует.',
+    couldnt_update_site: 'Не получилось обновить сайт.',
+    couldnt_find_that_username_or_email: 'Не получилось найти данное имя пользователя или электронную почту.',
+    password_incorrect: 'Неверный пароль.',
+    passwords_dont_match: 'Пароли не совпадают.',
+    admin_already_created: 'Извините, уже есть администратор.',
+    user_already_exists: 'Пользователь уже существует.',
+    couldnt_update_user: 'Не получилось обновить пользователя.',
+    system_err_login: 'Системная ошибка. Попробуйте выйти из системы и вернуться обратно.',
+  },
+}
+
diff --git a/ui/src/translations/sv.ts b/ui/src/translations/sv.ts
new file mode 100644 (file)
index 0000000..ecfb094
--- /dev/null
@@ -0,0 +1,161 @@
+export const sv = {
+  translation: {
+    post: 'inlägg',
+    remove_post: 'Radera inlägg',
+    no_posts: 'Inga inlägg.',
+    create_a_post: 'Skriv ett inlägg',
+    create_post: 'Skapa inlägg',
+    number_of_posts:'{{count}} inlägg',
+    posts: 'Inlägg',
+    related_posts: 'Dessa inlägg kan vara relaterade',
+    comments: 'Kommentarer',
+    number_of_comments:'{{count}} kommentarer',
+    remove_comment: 'Radera kommentar',
+    communities: 'Gemenskaper',
+    users: 'Användare',
+    create_a_community: 'Skapa en gemenskap',
+    create_community: 'Skapa gemenskap',
+    remove_community: 'Radera gemenskap',
+    subscribed_to_communities:'Prenumererar på <1>gemenskaper</1>',
+    trending_communities:'Populära <1>gemenskaper</1>',
+    list_of_communities: 'Lista över gemenskaper',
+    community_reqs: 'gemener, understreck och inga blanksteg.',
+    edit: 'redigera',
+    reply: 'svara',
+    cancel: 'Avbryt',
+    unlock: 'lås upp',
+    lock: 'lås',
+    link: 'länk',
+    mod: 'moderator',
+    mods: 'moderatorer',
+    moderates: 'Modererar',
+    remove_as_mod: 'tag bort som moderator',
+    appoint_as_mod: 'lägg till som moderator',
+    modlog: 'Moderationslogg',
+    admin: 'administratör',
+    admins: 'administratörer',
+    remove_as_admin: 'tag bort som administratör',
+    appoint_as_admin: 'lägg till som administratör',
+    remove: 'ta bort',
+    removed: 'borttagen',
+    locked: 'låst',
+    reason: 'Anledning',
+    mark_as_read: 'markera som läst',
+    mark_as_unread: 'markera som oläst',
+    delete: 'radera',
+    deleted: 'raderad',
+    restore: 'återställ',
+    ban: 'blockera',
+    ban_from_site: 'blockera från webbplats',
+    unban: 'ta bort blockering',
+    unban_from_site: 'ta bort blockering från webbplats',
+    save: 'spara',
+    unsave: 'förkasta', // Is perhaps 'ångra' more appropriate?
+    create: 'skapa',
+    username: 'Användarnamn',
+    email_or_username: 'E-postadress eller användarnamn',
+    number_of_users:'{{count}} användare',
+    number_of_subscribers:'{{count}} prenumeranter',
+    number_of_points:'{{count}} poäng',
+    name: 'Namn',
+    title: 'Titel',
+    category: 'Kategori',
+    subscribers: 'Prenumeranter',
+    both: 'Båda',
+    saved: 'Sparade',
+    unsubscribe: 'Avbryt prenumeration',
+    subscribe: 'Prenumerera',
+    subscribed: 'Prenumererar',
+    prev: 'Föregående',
+    next: 'Nästa',
+    sidebar: 'Sidlist',
+    sort_type: 'Sorteringstyp',
+    hot: 'Hett',
+    new: 'Nytt',
+    top_day: 'Dagstoppen',
+    week: 'Vecka',
+    month: 'Månad',
+    year: 'År',
+    all: 'Samtliga',
+    top: 'Topp',
+    api: 'API',
+    inbox: 'Inkorg',
+    inbox_for: 'Inkorg tillhörande <1>{{user}}</1>',
+    mark_all_as_read: 'markera alla som lästa',
+    type: 'Typ',
+    unread: 'Oläst',
+    reply_sent: 'Svar skickat',
+    search: 'Sök',
+    overview: 'Översikt',
+    view: 'Vy',
+    logout: 'Logga ut',
+    login_sign_up: 'Logga in eller skapa konto',
+    login: 'Logga in',
+    sign_up: 'Skapa konto',
+    notifications_error: 'Din webbläsare har inte stöd för skrivbordsaviseringar. Testa Firefox eller Chrome.',
+    unread_messages: 'Olästa meddelanden',
+    password: 'Lösenord',
+    verify_password: 'Bekräfta lösenord',
+    email: 'E-postadress',
+    optional: 'Valfritt',
+    expires: 'Går ut',
+    url: 'URL',
+    body: 'Brödtext', // Probably not the best in context.
+    copy_suggested_title: 'kopiera föreslagen titel: {{title}}',
+    community: 'Gemenskap',
+    expand_here: 'Utvidga här',
+    subscribe_to_communities: 'Prenumerera på några <1>gemenskaper</1>.',
+    chat: 'Chatta',
+    no_results: 'Inga resultat.',
+    setup: 'Installering',
+    lemmy_instance_setup: 'Installering av Lemmy-instans',
+    setup_admin: 'Skapa en administratör',
+    your_site: 'din webbplats',
+    modified: 'ändrades',
+    sponsors: 'Sponsorer',
+    sponsors_of_lemmy: 'Lemmys sponsorer',
+    sponsor_message: 'Lemmy är fri mjukvara med <1>öppen källkod</1>, vilket innebär att ingen reklam, vinstindrivning eller venturekapital förekommer, någonsin. Dina donationer går direkt till att stöda utvecklingen av projektet. Stort tack till följande personer:',
+    support_on_patreon: 'Stöd på Patreon',
+    general_sponsors:'Allmänna sponsorer är dem som givit mellan 10 och 39\u00a0dollar till Lemmy.',
+    crypto: 'Kryptovaluta',
+    bitcoin: 'Bitcoin',
+    ethereum: 'Ethereum',
+    code: 'Kod',
+    joined: 'Gick med',
+    powered_by: 'Drivs av',
+    landing_0: 'Lemmy är en <1>länksamlare</1> och alternativ till reddit, ämnad att fungera i <2>Fediversumet</2>.<3></3>Lemmy kan drivas av vem som helst, har kommentarstrådar som updateras i realid och är mycket liten (<4>ca 80\u00a0kB</4>). Federering med ActivityPub-nätverket är planerat. <5></5>Detta är en <6>väldigt tidig betaversion</6> och många funktioner saknas därför eller är trasiga.<7></7>Föreslå nya funktioner eller anmäl buggar <8>här</8>.<9></9>Skapad i <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> och <13>Typescript</13>.',
+    not_logged_in: 'Inte inloggad.',
+    community_ban: 'Du har blockerats från den här gemenskapen.',
+    site_ban: 'Du har blockerats från webbplatsen.',
+    couldnt_create_comment: 'Kunde inte skapa kommentar.',
+    couldnt_like_comment: 'Kunde inte gilla kommentar.',
+    couldnt_update_comment: 'Kunde inte uppdatera kommentar.',
+    couldnt_save_comment: 'Kunde inte spara kommentar.',
+    no_comment_edit_allowed: 'Har inte behörighet att redigera komentar.',
+    no_post_edit_allowed: 'Har inte behörighet att redigera inlägg.',
+    no_community_edit_allowed: 'Har inte behörighet att redigera gemenskap.',
+    couldnt_find_community: 'Kunde inte hitta gemenskap.',
+    couldnt_update_community: 'Kunde inte uppdatera gemenskap.',
+    community_already_exists: 'Gemenskapen finns redan.',
+    community_moderator_already_exists: 'Gemenskapsmoderatorn finns redan.',
+    community_follower_already_exists: 'Gemenskapsföljaren finns redan.',
+    community_user_already_banned: 'Gemenskapsanvändaren redan blockerad.',
+    couldnt_create_post: 'Kunde inte skapa inlägg.',
+    couldnt_like_post: 'Kunde inte gilla inlägg.',
+    couldnt_find_post: 'Kunde inte hitta inlägg.',
+    couldnt_get_posts: 'Kunde inte hämta inlägg.',
+    couldnt_update_post: 'Kunde inte uppdatera inlägg.',
+    couldnt_save_post: 'Kunde inte spara inlägg.',
+    no_slurs: 'Inga förolämpningar.',
+    not_an_admin: 'Inte en administratör.',
+    site_already_exists: 'Webbplatsen finns redan.',
+    couldnt_update_site: 'Kunde inte uppdatera webbplats.',
+    couldnt_find_that_username_or_email: 'Kunde inte hitta det användarnamnet eller e-postadressen.',
+    password_incorrect: 'Ogiltigt lösenord.',
+    passwords_dont_match: 'Lösenorden stämmer inte överens.',
+    admin_already_created: 'Beklagar, men det finns redan en administratör.',
+    user_already_exists: 'Användaren finns redan.',
+    couldnt_update_user: 'Kunde inte uppdatera användare.',
+    system_err_login: 'Systemfel. Försök att logga ut och sedan in igen.',
+  },
+}
diff --git a/ui/src/translations/zh.ts b/ui/src/translations/zh.ts
new file mode 100644 (file)
index 0000000..2a40767
--- /dev/null
@@ -0,0 +1,161 @@
+export const zh = {
+  translation: {
+    post: '帖子',
+    remove_post: '移除帖子',
+    no_posts: '没有帖子.',
+    create_a_post: '创建新帖子',
+    create_post: '创建帖子',
+    number_of_posts:'{{count}} 帖子',
+    posts: '帖子',
+    related_posts: '相关的帖子',
+    comments: '评论',
+    number_of_comments:'{{count}} 评论',
+    remove_comment: '移除评论',
+    communities: '节点',
+    create_a_community: '创建新节点',
+    create_community: '创建节点',
+    remove_community: '移除节点',
+    subscribed_to_communities:'订阅新 <1>节点</1>',
+    trending_communities:'<1>节点</1>趋势',
+    list_of_communities: '节点列表',
+    community_reqs: '包含小写与下划线且没有空格的字符串.',
+    edit: '编辑',
+    reply: '回应',
+    cancel: '取消',
+    unlock: '解锁',
+    lock: '加锁',
+    link: '链接',
+    mod: 'mod',
+    mods: 'mods',
+    moderates: 'Moderates',
+    remove_as_mod: 'remove as mod',
+    appoint_as_mod: 'appoint as mod',
+    modlog: 'Modlog',
+    admin: 'admin',
+    admins: 'admins',
+    remove_as_admin: '移除管理权限',
+    appoint_as_admin: '添加管理权限',
+    remove: '移除',
+    removed: '已移除',
+    locked: '已加锁',
+    reason: '原因',
+    mark_as_read: '标记未读',
+    mark_as_unread: '标记已读',
+    delete: '删除',
+    deleted: '已删除',
+    restore: '恢复',
+    ban: '禁止',
+    ban_from_site: '禁止此站点',
+    unban: '取消',
+    unban_from_site: '取消禁止',
+    save: '保存',
+    unsave: '取消保存',
+    create: '创建',
+    username: '用户名',
+    email_or_username: '邮箱或用户名',
+    number_of_users:'{{count}} 用户',
+    number_of_subscribers:'{{count}} 订阅',
+    number_of_points:'{{count}} 分',
+    name: '名字',
+    title: '标题',
+    category: '分类',
+    subscribers: '订阅',
+    both: '全部',
+    saved: '保存',
+    unsubscribe: '取消订阅',
+    subscribe: '订阅',
+    subscribed: '已订阅',
+    prev: '上一页',
+    next: '下一页',
+    sidebar: '侧边栏',
+    sort_type: '排序方式',
+    hot: '最热',
+    new: '最新',
+    top_day: '今日',
+    week: '周',
+    month: '月',
+    year: '年',
+    all: '所有',
+    top: '最热',
+    api: 'API',
+    inbox: '收件箱',
+    inbox_for: '<1>{{user}}</1> 收件箱',
+    mark_all_as_read: '标记所有已读',
+    type: '类型',
+    unread: '未读',
+    reply_sent: '回复发送',
+    search: '搜索',
+    overview: '个人中心',
+    view: '查看',
+    logout: '注销',
+    login_sign_up: '登录/注册',
+    login: '登录',
+    sign_up: '注册',
+    notifications_error: '你的浏览器不支持桌面通知,尝试 Firefox 或 Chrome',
+    unread_messages: '未读消息',
+    password: '密码',
+    verify_password: '确认密码',
+    email: '邮箱',
+    optional: '选项',
+    expires: '过期',
+    url: 'URL',
+    body: '内容',
+    copy_suggested_title: '复制建议的标题: {{title}}',
+    community: '节点',
+    expand_here: '展开',
+    subscribe_to_communities: '订阅一些 <1>节点</1>.',
+    chat: '聊天',
+    no_results: '没有结果.',
+    setup: '设置',
+    lemmy_instance_setup: 'Lemmy Instance Setup',
+    setup_admin: '设置管理员',
+    your_site: '你的站点',
+    modified: '修改',
+    sponsors: 'Sponsors',
+    sponsors_of_lemmy: 'Sponsors of Lemmy',
+    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',
+    general_sponsors:'General Sponsors are those that pledged $10 to $39 to Lemmy.',
+    crypto: '加密',
+    bitcoin: '比特币',
+    ethereum: '以太币',
+    code: '代码',
+    joined: '已加入',
+    powered_by: '保留所有权利',
+    landing_0: '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: '未登录.',
+    community_ban: '你被此节点禁止.',
+    site_ban: '你被此站点禁止',
+    couldnt_create_comment: '不能创建评论.',
+    couldnt_like_comment: '不能收藏评论.',
+    couldnt_update_comment: '不能更新评论.',
+    couldnt_save_comment: '不能保存评论.',
+    no_comment_edit_allowed: '不允许编辑评论.',
+    no_post_edit_allowed: '不运行编辑帖子.',
+    no_community_edit_allowed: '不允许编辑节点.',
+    couldnt_find_community: '不能找到节点.',
+    couldnt_update_community: '不能更新节点.',
+    community_already_exists: '节点已存在.',
+    community_moderator_already_exists: '节点 moderator 已存在.',
+    community_follower_already_exists: '节点 follower 已存在.',
+    community_user_already_banned: '节点用户已禁止.',
+    couldnt_create_post: '不能创建帖子.',
+    couldnt_like_post: '不能收藏帖子.',
+    couldnt_find_post: '不能搜寻帖子.',
+    couldnt_get_posts: '不能获取帖子',
+    couldnt_update_post: '不能更新帖子',
+    couldnt_save_post: '不能保持帖子.',
+    no_slurs: '和谐.',
+    not_an_admin: '不是管理员.',
+    site_already_exists: '站点已存在.',
+    couldnt_update_site: '不能更新站点.',
+    couldnt_find_that_username_or_email: '用户名/邮箱不存在.',
+    password_incorrect: '密码不正确.',
+    passwords_dont_match: '密码不匹配.',
+    admin_already_created: '抱歉,管理员已存在.',
+    user_already_exists: '用户已存在.',
+    couldnt_update_user: '不可以更新用户.',
+    system_err_login: '系统错误. 尝试注销再登录',
+  },
+}
+
index 53d630ce8bc057ad8b980a822555a0a9aace53e8..89be9e2bfb781c828a2f74a7e820c1815975906b 100644 (file)
@@ -1,3 +1,12 @@
+import 'moment/locale/es';
+import 'moment/locale/eo';
+import 'moment/locale/de';
+import 'moment/locale/zh-cn';
+import 'moment/locale/fr';
+import 'moment/locale/sv';
+import 'moment/locale/ru';
+import 'moment/locale/nl';
+
 import { UserOperation, Comment, User, SortType, ListingType } from './interfaces';
 import * as markdown_it from 'markdown-it';
 declare var markdownitEmoji: any;
@@ -11,7 +20,7 @@ export function msgOp(msg: any): UserOperation {
 }
 
 var md = new markdown_it({
-  html: true,
+  html: false,
   linkify: true,
   typographer: true
 }).use(markdown_it_container, 'spoiler', {
@@ -85,6 +94,16 @@ export function isImage(url: string) {
   return imageRegex.test(url);
 }
 
+export function validURL(str: string) {
+  var pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
+    '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
+    '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
+    '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
+    '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
+    '(\\#[-a-z\\d_]*)?$','i'); // fragment locator
+  return !!pattern.test(str);
+}
+
 export let fetchLimit: number = 20;
 
 export function capitalizeFirstLetter(str: string): string {
@@ -118,3 +137,73 @@ export async function getPageTitle(url: string) {
   return data;
 }
 
+export function debounce(func: any, wait: number = 500, immediate: boolean = false) {
+  // 'private' variable for instance
+  // The returned function will be able to reference this due to closure.
+  // Each call to the returned function will share this common timer.
+  let timeout: number;
+
+  // Calling debounce returns a new anonymous function
+  return function() {
+    // reference the context and args for the setTimeout function
+    var context = this,
+    args = arguments;
+
+  // Should the function be called now? If immediate is true
+  //   and not already in a timeout then the answer is: Yes
+  var callNow = immediate && !timeout;
+
+  // This is the basic debounce behaviour where you can call this 
+  //   function several times, but it will only execute once 
+  //   [before or after imposing a delay]. 
+  //   Each time the returned function is called, the timer starts over.
+  clearTimeout(timeout);
+
+  // Set the new timeout
+  timeout = setTimeout(function() {
+
+    // Inside the timeout function, clear the timeout variable
+    // which will let the next execution run when in 'immediate' mode
+    timeout = null;
+
+    // Check if the function already ran with the immediate flag
+    if (!immediate) {
+      // Call the original function with apply
+      // apply lets you define the 'this' object as well as the arguments 
+      //    (both captured before setTimeout)
+      func.apply(context, args);
+    }
+  }, wait);
+
+  // Immediate mode and no wait timer? Execute the function..
+  if (callNow) func.apply(context, args);
+  }
+}
+
+export function getLanguage(): string {
+  return (navigator.language || navigator.userLanguage);
+}
+
+export function getMomentLanguage(): string {
+  let lang = getLanguage();
+  if (lang.startsWith('zh')) {
+    lang = 'zh-cn';
+  } else if (lang.startsWith('sv')) {
+    lang = 'sv';
+  } else if (lang.startsWith('fr')) {
+    lang = 'fr';
+  } else if (lang.startsWith('de')) {
+    lang = 'de';
+  } else if (lang.startsWith('ru')) {
+    lang = 'ru';
+  } else if (lang.startsWith('es')) {
+    lang = 'es';
+  } else if (lang.startsWith('eo')) {
+    lang = 'eo';
+  } else if (lang.startsWith('nl')) {
+    lang = 'nl';
+  } else {
+    lang = 'en';
+  }
+  return lang;
+}
index 45ea6ab9efcaa11b74bfe52d373c986035054812..030b4a0e5e02ad768fdd7d2370d25302bc063db9 100644 (file)
@@ -1 +1 @@
-export let version: string = "v0.0.5-16-gc7d8d5f";
\ No newline at end of file
+export let version: string = "v0.0.8.7-0-g614c4f8";
\ No newline at end of file
diff --git a/ui/stack.dev.yaml b/ui/stack.dev.yaml
deleted file mode 100644 (file)
index bb7c62e..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: lemmy-ui--dev
-spec:
-  selector:
-    matchLabels:
-      app: lemmy-ui--dev
-  template:
-    metadata:
-      labels:
-        app: lemmy-ui--dev
-    spec:
-      containers:
-        - name: lemmy-ui--dev
-          image: registry.gitlab.com/pojntfx/lemmy/ui.dev
-          resources:
-            limits:
-              memory: 1024Mi
-              cpu: 512m
-          ports:
-            - containerPort: 4444
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: lemmy-ui--dev
-spec:
-  type: NodePort
-  selector:
-    app: lemmy-ui--dev
-  ports:
-    - port: 4444
-      nodePort: 30002
diff --git a/ui/stack.prod.yaml b/ui/stack.prod.yaml
deleted file mode 100644 (file)
index ca4e1a7..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: lemmy-ui--prod
-spec:
-  selector:
-    matchLabels:
-      app: lemmy-ui--prod
-  template:
-    metadata:
-      labels:
-        app: lemmy-ui--prod
-    spec:
-      containers:
-        - name: lemmy-ui--prod
-          image: registry.gitlab.com/pojntfx/lemmy/ui.prod
-          resources:
-            limits:
-              memory: 1024Mi
-              cpu: 512m
-          ports:
-            - containerPort: 4444
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: lemmy-ui--prod
-spec:
-  selector:
-    app: lemmy-ui--prod
-  ports:
-    - port: 5000
-      targetPort: 5000
----
-apiVersion: extensions/v1beta1
-kind: Ingress
-metadata:
-  name: lemmy-server--prod
-  annotations:
-    traefik.ingress.kubernetes.io/request-modifier: "ReplacePathRegex: ^/static/(.*) /$1"
-spec:
-  rules:
-    - host: dev.lemmy.local
-      http:
-        paths:
-          - path: /
-            backend:
-              serviceName: lemmy-ui--prod
-              servicePort: 5000
-          - path: /api/v1/ws
-            backend:
-              serviceName: lemmy-server--prod
-              servicePort: 8536
diff --git a/ui/translation_report.ts b/ui/translation_report.ts
new file mode 100644 (file)
index 0000000..8e579f2
--- /dev/null
@@ -0,0 +1,37 @@
+import { en } from './src/translations/en';
+import { eo } from './src/translations/eo';
+import { es } from './src/translations/es';
+import { de } from './src/translations/de';
+import { zh } from './src/translations/zh';
+import { fr } from './src/translations/fr';
+import { sv } from './src/translations/sv';
+import { ru } from './src/translations/ru';
+import { nl } from './src/translations/nl';
+
+let files = [
+  {t: de, n: 'de'}, 
+  {t: eo, n: 'eo'}, 
+  {t: es, n: 'es'}, 
+  {t: fr, n: 'fr'}, 
+  {t: nl, n: 'nl'}, 
+  {t: ru, n: 'ru'}, 
+  {t: sv, n: 'sv'}, 
+  {t: zh, n: 'zh'}, 
+];
+let masterKeys = Object.keys(en.translation);
+
+let report = 'lang | done | missing\n';
+report += '--- | --- | ---\n';
+
+for (let file of files) {
+  let keys = Object.keys(file.t.translation);
+  let pct: number = (keys.length / masterKeys.length * 100);
+  let missing = difference(masterKeys, keys);
+  report += `${file.n} | ${pct.toFixed(0)}% | ${missing} \n`;
+}
+
+console.log(report);
+
+function difference(a: Array<string>, b: Array<string>): Array<string> {
+  return a.filter(x => !b.includes(x));
+}
index d3e7a8a97242ade6a3f24cb1b53df2426dcb72a0..938502e473c519ba0903f21928b0fc46be64589b 100644 (file)
@@ -2,7 +2,7 @@
     "extends": "tslint:recommended",
     "rules": {
                        "forin": false,
-                       "indent": [ true, "tabs" ],
+                       "indent": [ true, "spaces" ],
                        "interface-name": false,
                        "ban-types": true,
                        "max-classes-per-file": true,
index 5c22b16f1ddc44b7d377b71403f76d3345b2ee80..79fe71665d4c835ac4bf2c092be696ffdac9e258 100644 (file)
@@ -2,10 +2,10 @@
 # yarn lockfile v1
 
 
-"@babel/runtime@^7.1.2":
-  version "7.4.5"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
-  integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1":
+  version "7.5.5"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
+  integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
   dependencies:
     regenerator-runtime "^0.13.2"
 
   dependencies:
     "@types/jquery" "*"
 
+"@types/i18next@^12.1.0":
+  version "12.1.0"
+  resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-12.1.0.tgz#7c3fd3dbe03f9531147033773bbd0ca4f474a180"
+  integrity sha512-qLyqTkp3ZKHsSoX8CNVYcTyTkxlm0aRCUpaUVetgkSlSpiNCdWryOgaYwgbO04tJIfLgBXPcy0tJ3Nl/RagllA==
+
 "@types/jquery@*":
-  version "3.3.29"
-  resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.29.tgz#680a2219ce3c9250483722fccf5570d1e2d08abd"
-  integrity sha512-FhJvBninYD36v3k6c+bVk1DSZwh7B5Dpb/Pyk3HKVsiohn0nhbefZZ+3JXbWQhFyt0MxSl2jRDdGQPHeOHFXrQ==
+  version "3.3.31"
+  resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.31.tgz#27c706e4bf488474e1cb54a71d8303f37c93451b"
+  integrity sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==
   dependencies:
     "@types/sizzle" "*"
 
   dependencies:
     "@types/markdown-it" "*"
 
-"@types/markdown-it@*", "@types/markdown-it@^0.0.7":
+"@types/markdown-it@*":
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.8.tgz#9af8704acde87fec70475369ba0413d50717bd8d"
+  integrity sha512-ouaTOi5kAdkTPl97u6uDkth9od4pQffPF9STcjYVZKFrEwLYf15s7Z772WxWE3IOcYBJglaT0XqdyNEiEfGgYg==
+  dependencies:
+    "@types/linkify-it" "*"
+
+"@types/markdown-it@^0.0.7":
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.7.tgz#75070485a3d8ad11e7deb8287f4430be15bf4d39"
   integrity sha512-WyL6pa76ollQFQNEaLVa41ZUUvDvPY+qAUmlsphnrpL6I9p1m868b26FyeoOmo7X3/Ta/S9WKXcEYXUSHnxoVQ==
@@ -91,9 +103,9 @@ ajax-request@^1.2.0:
     utils-extend "^1.0.7"
 
 ajv@^6.5.5:
-  version "6.10.0"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
-  integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
+  version "6.10.2"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52"
+  integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==
   dependencies:
     fast-deep-equal "^2.0.1"
     fast-json-stable-stringify "^2.0.0"
@@ -224,6 +236,11 @@ async-each@^1.0.0:
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
   integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
 
+async-limiter@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
+  integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+
 asynckit@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -263,9 +280,9 @@ base64-img@^1.0.3:
     file-system "^2.1.0"
 
 base64-js@^1.2.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
-  integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
+  integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
 
 base@^0.11.1:
   version "0.11.2"
@@ -309,9 +326,9 @@ body-parser@1.19.0:
     type-is "~1.6.17"
 
 bowser@^2.0.0-beta.3:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.4.0.tgz#fcfbca3d7659ba88afabbb8a45b33d2e4876a90c"
-  integrity sha512-DA9Opnb8S8TBLPPszrHDtCCATbAMkrxF+AxPs/d95r99frBioGpNwL1cbG3AHeV3FnoZW655vEvEryBHFeGrMg==
+  version "2.5.3"
+  resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.5.3.tgz#811b0a24219c566c9a6ab3402bc8a13f35a18a96"
+  integrity sha512-aWCA+CKfKNL/WGzNgjmK+Whp57JMzboZMwJ5gy2jDj2bEIjbMCb3ImGX+V++5wsJftyFiDIbOjRXl60ycniVqg==
 
 brace-expansion@^1.1.7:
   version "1.1.11"
@@ -417,9 +434,9 @@ chokidar@^1.6.1:
     fsevents "^1.0.0"
 
 chownr@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
-  integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6"
+  integrity sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==
 
 class-utils@^0.3.5:
   version "0.3.6"
@@ -661,9 +678,9 @@ escape-string-regexp@^1.0.5:
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
 escodegen@^1.8.1:
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510"
-  integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==
+  version "1.12.0"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541"
+  integrity sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==
   dependencies:
     esprima "^3.1.3"
     estraverse "^4.2.0"
@@ -678,14 +695,14 @@ esprima@^3.1.3:
   integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
 
 estraverse@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
-  integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+  integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
 
 esutils@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
-  integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
 etag@~1.8.1:
   version "1.8.1"
@@ -1043,9 +1060,9 @@ get-value@^2.0.3, get-value@^2.0.6:
   integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
 
 getopts@^2.1.1:
-  version "2.2.4"
-  resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.4.tgz#3137fe8a5fddf304904059a851bdc1c22f0f54fb"
-  integrity sha512-Rz7DGyomZjrenu9Jx4qmzdlvJgvrEFHXHvjK0FcZtcTC1U5FmES7OdZHUwMuSnEE6QvBvwse1JODKj7TgbSEjQ==
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b"
+  integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA==
 
 getpass@^0.1.1:
   version "0.1.7"
@@ -1082,9 +1099,9 @@ glob@^7.1.1, glob@^7.1.3:
     path-is-absolute "^1.0.0"
 
 graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
-  version "4.1.15"
-  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
-  integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02"
+  integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==
 
 har-schema@^2.0.0:
   version "2.0.0"
@@ -1157,7 +1174,14 @@ hoist-non-inferno-statics@^1.1.3:
   resolved "https://registry.yarnpkg.com/hoist-non-inferno-statics/-/hoist-non-inferno-statics-1.1.3.tgz#7d870f4160bfb6a59269b45c343c027f0e30ab35"
   integrity sha1-fYcPQWC/tqWSabRcNDwCfw4wqzU=
 
-http-errors@1.7.2, http-errors@~1.7.2:
+html-parse-stringify2@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a"
+  integrity sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=
+  dependencies:
+    void-elements "^2.0.1"
+
+http-errors@1.7.2:
   version "1.7.2"
   resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
   integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
@@ -1168,6 +1192,17 @@ http-errors@1.7.2, http-errors@~1.7.2:
     statuses ">= 1.5.0 < 2"
     toidentifier "1.0.0"
 
+http-errors@~1.7.2:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
+  integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.4"
+    setprototypeof "1.1.1"
+    statuses ">= 1.5.0 < 2"
+    toidentifier "1.0.0"
+
 http-signature@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
@@ -1177,6 +1212,13 @@ http-signature@~1.2.0:
     jsprim "^1.2.2"
     sshpk "^1.7.0"
 
+i18next@^17.0.9:
+  version "17.0.13"
+  resolved "https://registry.yarnpkg.com/i18next/-/i18next-17.0.13.tgz#3c639e15de86e0523f8f286f6cf07db355ee0a4f"
+  integrity sha512-tCBpekVs95IsN3kdi/6HhnfzHDlpXerOmOsf2ZMWtct9YbMYKI54HVdQ6XxsHGXBxY+UgjbQJwqghKCd2sYQWw==
+  dependencies:
+    "@babel/runtime" "^7.3.1"
+
 iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.4:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -1196,33 +1238,58 @@ ignore-walk@^3.0.1:
   dependencies:
     minimatch "^3.0.4"
 
+inferno-clone-vnode@^7.1.12:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/inferno-clone-vnode/-/inferno-clone-vnode-7.3.1.tgz#7dc75d58ce818188beb77acb49f672568cd817a8"
+  integrity sha512-Nu+jrwOVXlaMsxtxRbPiB51SySgI9FrGKt0wR9NNWSOoaW9fXbQewcPdI5Jn9MYin/oQrpJ8+BI9pZ9Bu7Z6bQ==
+  dependencies:
+    inferno "7.3.1"
+
+inferno-create-element@^7.1.12:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/inferno-create-element/-/inferno-create-element-7.3.1.tgz#b85f8ede233b19653662d6ac3bd88892319b09e4"
+  integrity sha512-HyC4UTmSB+3+erVp/0Y5BYioBxKLZpKrIBYEHTzeDXfsJv9wEsJ5KbZ5vsmQDh+jj6NRd72cCPMBXmN9pyyMCA==
+  dependencies:
+    inferno "7.3.1"
+
+inferno-i18next@nimbusec-oss/inferno-i18next:
+  version "7.1.12"
+  resolved "https://codeload.github.com/nimbusec-oss/inferno-i18next/tar.gz/f8c1403e60be70141c558e36f12f22c106cb7463"
+  dependencies:
+    html-parse-stringify2 "^2.0.1"
+    inferno "^7.1.12"
+    inferno-clone-vnode "^7.1.12"
+    inferno-create-element "^7.1.12"
+    inferno-shared "^7.1.12"
+    inferno-vnode-flags "^7.1.12"
+
 inferno-router@^7.0.1:
-  version "7.1.13"
-  resolved "https://registry.yarnpkg.com/inferno-router/-/inferno-router-7.1.13.tgz#7f72ca8deaa5bf8c2f49bcb3db253e294c134f77"
-  integrity sha512-y97fF0IG70+nAlHRtxaH/8QsMUsSWeS375MC416NomP5tohSxmBLB0WQGTYFHvnMpqb/QSq2ojsifBAWqSHnuw==
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/inferno-router/-/inferno-router-7.3.1.tgz#cdb81349dd1b51fa03c65bdd938e6b050006e44b"
+  integrity sha512-Hav1iCti9u6oc8ZIGmUhDHQOBRK/uJpDdbk0naEmMZ4zfu/6hbk7q7mObRVtO5QVBL2Y343SKb43LAJWv3C/rw==
   dependencies:
     history "^4.9.0"
     hoist-non-inferno-statics "^1.1.3"
-    inferno "7.1.13"
+    inferno "7.3.1"
     path-to-regexp-es6 "1.7.0"
 
-inferno-shared@7.1.13:
-  version "7.1.13"
-  resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-7.1.13.tgz#2c70af637873b58fbfc3897813076dec4850a6b0"
-  integrity sha512-HNWpvCFO9vw5I9XNvsZamhe7UcSMBN62AOCyyGHlBGPB996/f3xwwgBWi0uHQ2rBtiM4Hp3rn65PQ5wSVd6/hg==
+inferno-shared@7.3.1, inferno-shared@^7.1.12:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-7.3.1.tgz#93e269cb46838780b68fa5113c9a29b6109882de"
+  integrity sha512-7I1ZJG+MFcGtlXjuyvkwFSGcT46Vs9NTzA0Sr1EkF9EMqVskVflP1r5f+quASMhby2OY7AGXnAaSsM7AbxsOlA==
 
-inferno-vnode-flags@7.1.13:
-  version "7.1.13"
-  resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-7.1.13.tgz#8c98e69079fe85feffe0ded79e6d7c05ae0d59b1"
-  integrity sha512-RELi78Y2bs81hSxgVOY2oZ+E6mHqtXFFdsuslWYwmaYbEDYV2qZQp4ayu5MyHQU+Ip8dkjKvIkN/yrYoBAvJgw==
+inferno-vnode-flags@7.3.1, inferno-vnode-flags@^7.1.12:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-7.3.1.tgz#27f767a03d8f0775f8bc5ad03c9ea07ec66aa2c7"
+  integrity sha512-7LKuUGfFVSiFdRH6NVLTetrDbft4BxUePUIjXm4f+g9hginYuBwPbFJbbHK700Ysy4rapTqYnFxSk0k5JBZr+A==
 
-inferno@7.1.13, inferno@^7.0.1:
-  version "7.1.13"
-  resolved "https://registry.yarnpkg.com/inferno/-/inferno-7.1.13.tgz#d8ce8384a07de5138897f9f79c25d04141228b93"
-  integrity sha512-CrdzQRHMRkhrRTAB2tYBxnqe1umYkuKparHOHd7R37okd1TGp/Q8bQa58002Qh4pY+BU272mz3FuQC5LDQOLRg==
+inferno@7.3.1, inferno@^7.0.1, inferno@^7.1.12:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/inferno/-/inferno-7.3.1.tgz#5e356d9a5a218809eb4da49c333d324126515683"
+  integrity sha512-9t4G/YjhbiC65d8GHxzfn41qALVf2fvykMdH92ySUiSLr3EQak8gs6rno52KuW7puDZcmJgmMHVA0vTVegtfsQ==
   dependencies:
-    inferno-shared "7.1.13"
-    inferno-vnode-flags "7.1.13"
+    inferno-shared "7.3.1"
+    inferno-vnode-flags "7.3.1"
     opencollective-postinstall "^2.0.2"
 
 inflight@^1.0.4:
@@ -1233,7 +1300,12 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@~2.0.1, inherits@~2.0.3:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+inherits@2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
   integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
@@ -1393,7 +1465,7 @@ is-number@^4.0.0:
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
   integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==
 
-is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+is-plain-object@^2.0.3, is-plain-object@^2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
   integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
@@ -1453,9 +1525,9 @@ isstream@~0.1.2:
   integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
 
 js-cookie@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.0.tgz#1b2c279a6eece380a12168b92485265b35b1effb"
-  integrity sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s=
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
+  integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
 
 "js-tokens@^3.0.0 || ^4.0.0":
   version "4.0.0"
@@ -1549,16 +1621,16 @@ levn@~0.3.0:
     type-check "~0.3.2"
 
 linkify-it@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.1.0.tgz#c4caf38a6cd7ac2212ef3c7d2bde30a91561f9db"
-  integrity sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf"
+  integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==
   dependencies:
     uc.micro "^1.0.1"
 
 lodash@^4.3.0:
-  version "4.17.11"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
-  integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
+  version "4.17.15"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+  integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
 
 loose-envify@^1.2.0:
   version "1.4.0"
@@ -1708,9 +1780,9 @@ minimist@^1.2.0:
   integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
 
 minipass@^2.2.1, minipass@^2.3.5:
-  version "2.3.5"
-  resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
-  integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.5.0.tgz#dddb1d001976978158a05badfcbef4a771612857"
+  integrity sha512-9FwMVYhn6ERvMR8XFdOavRz4QK/VJV8elU1x50vYexf9lslDcWe/f4HBRxCPd185ekRSjU6CfYyJCECa/CQy7Q==
   dependencies:
     safe-buffer "^5.1.2"
     yallist "^3.0.0"
@@ -1723,9 +1795,9 @@ minizlib@^1.2.1:
     minipass "^2.2.1"
 
 mixin-deep@^1.2.0:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
-  integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
+  integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
   dependencies:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
@@ -1747,11 +1819,16 @@ ms@2.0.0:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
   integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
 
-ms@2.1.1, ms@^2.1.1:
+ms@2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
   integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
 
+ms@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
 mustache@^2.3.0:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5"
@@ -1840,9 +1917,9 @@ npm-bundled@^1.0.1:
   integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==
 
 npm-packlist@^1.1.6:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc"
-  integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44"
+  integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw==
   dependencies:
     ignore-walk "^3.0.1"
     npm-bundled "^1.0.1"
@@ -2051,9 +2128,9 @@ prettysize@0.0.3:
   integrity sha1-FK//amReWRpN3xxykZwjtBRhgaE=
 
 process-nextick-args@~2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
-  integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
 
 proxy-addr@~2.0.5:
   version "2.0.5"
@@ -2064,9 +2141,9 @@ proxy-addr@~2.0.5:
     ipaddr.js "1.9.0"
 
 psl@^1.1.24:
-  version "1.1.32"
-  resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.32.tgz#3f132717cf2f9c169724b2b6caf373cf694198db"
-  integrity sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.3.0.tgz#e1ebf6a3b5564fa8376f3da2275da76d875ca1bd"
+  integrity sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag==
 
 punycode@^1.4.1:
   version "1.4.1"
@@ -2152,7 +2229,7 @@ realm-utils@^1.0.9:
     app-root-path "^1.3.0"
     mkdirp "^0.5.1"
 
-regenerate-unicode-properties@^8.0.2:
+regenerate-unicode-properties@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
   integrity sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==
@@ -2165,9 +2242,9 @@ regenerate@^1.4.0:
   integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
 
 regenerator-runtime@^0.13.2:
-  version "0.13.2"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
-  integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==
+  version "0.13.3"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
+  integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==
 
 regex-cache@^0.4.2:
   version "0.4.4"
@@ -2185,12 +2262,12 @@ regex-not@^1.0.0, regex-not@^1.0.2:
     safe-regex "^1.1.0"
 
 regexpu-core@^4.1.3:
-  version "4.5.4"
-  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.4.tgz#080d9d02289aa87fe1667a4f5136bc98a6aebaae"
-  integrity sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ==
+  version "4.5.5"
+  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.5.tgz#aaffe61c2af58269b3e516b61a73790376326411"
+  integrity sha512-FpI67+ky9J+cDizQUJlIlNZFKual/lUkFr1AG6zOCpwZ9cLrg8UUVakyUQJD7fCDIe9Z2nwTQJNPyonatNmDFQ==
   dependencies:
     regenerate "^1.4.0"
-    regenerate-unicode-properties "^8.0.2"
+    regenerate-unicode-properties "^8.1.0"
     regjsgen "^0.5.0"
     regjsparser "^0.6.0"
     unicode-match-property-ecmascript "^1.0.4"
@@ -2273,9 +2350,9 @@ ret@~0.1.10:
   integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
 
 rimraf@^2.6.1:
-  version "2.6.3"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
-  integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+  integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
   dependencies:
     glob "^7.1.3"
 
@@ -2305,11 +2382,16 @@ rxjs@^6.4.0:
   dependencies:
     tslib "^1.9.0"
 
-safe-buffer@5.1.2, safe-buffer@^5.0.1, 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"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
   integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
 
+safe-buffer@^5.0.1, safe-buffer@^5.1.2:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
+  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
+
 safe-regex@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
@@ -2328,9 +2410,9 @@ sax@^1.2.4:
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
 
 semver@^5.3.0:
-  version "5.7.0"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
-  integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 
 send@0.17.1:
   version "0.17.1"
@@ -2366,20 +2448,10 @@ set-blocking@~2.0.0:
   resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
   integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
 
-set-value@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
-  integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE=
-  dependencies:
-    extend-shallow "^2.0.1"
-    is-extendable "^0.1.1"
-    is-plain-object "^2.0.1"
-    to-object-path "^0.3.0"
-
-set-value@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274"
-  integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==
+set-value@^2.0.0, set-value@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
+  integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
   dependencies:
     extend-shallow "^2.0.1"
     is-extendable "^0.1.1"
@@ -2443,9 +2515,9 @@ source-map-resolve@^0.5.0:
     urix "^0.1.0"
 
 source-map-support@~0.5.10:
-  version "0.5.12"
-  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599"
-  integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==
+  version "0.5.13"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932"
+  integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==
   dependencies:
     buffer-from "^1.0.0"
     source-map "^0.6.0"
@@ -2576,9 +2648,9 @@ supports-color@^5.3.0, supports-color@^5.4.0:
     has-flag "^3.0.0"
 
 tar@^4:
-  version "4.4.9"
-  resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.9.tgz#058fbb152f6fc45733e84585a40c39e59302e1b3"
-  integrity sha512-xisFa7Q2i3HOgfn+nmnWLGHD6Tm23hxjkx6wwGmgxkJFr6wxwXnJOdJYcZjL453PSdF0+bemO03+flAzkIdLBQ==
+  version "4.4.10"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1"
+  integrity sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==
   dependencies:
     chownr "^1.1.1"
     fs-minipass "^1.2.5"
@@ -2603,14 +2675,14 @@ through@^2.3.6:
   integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
 
 tiny-invariant@^1.0.2:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.4.tgz#346b5415fd93cb696b0c4e8a96697ff590f92463"
-  integrity sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g==
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73"
+  integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==
 
 tiny-warning@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.2.tgz#1dfae771ee1a04396bdfde27a3adcebc6b648b28"
-  integrity sha512-rru86D9CpQRLvsFG5XFdy0KdLAvjdQDyZCsRcuu60WtzFylDM3eAWSxEVz5kzL2Gp544XiUvPbVKtOA/txLi9Q==
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
+  integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
 
 tmp@^0.0.33:
   version "0.0.33"
@@ -2670,9 +2742,9 @@ ts-transform-inferno@^4.0.2:
   integrity sha512-CZb4+w/2l2zikPZ/c51fi3n+qnR2HCEfAS73oGQB80aqRLffkZqm25kYYTMmqUW2+oVfs4M5AZa0z14cvxlQ5w==
 
 tslib@^1.8.0, tslib@^1.9.0:
-  version "1.9.3"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
-  integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
+  integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
 
 tunnel-agent@^0.6.0:
   version "0.6.0"
@@ -2706,10 +2778,10 @@ typescript@^2.6.2:
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
   integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==
 
-typescript@^3.3.3333:
-  version "3.5.1"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.1.tgz#ba72a6a600b2158139c5dd8850f700e231464202"
-  integrity sha512-64HkdiRv1yYZsSe4xC1WVgamNigVYjlssIoaH2HcZF0+ijsk5YK2g0G34w9wJkze8+5ow4STd22AynfO6ZYYLw==
+typescript@^3.5.3:
+  version "3.6.2"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.2.tgz#105b0f1934119dde543ac8eb71af3a91009efe54"
+  integrity sha512-lmQ4L+J6mnu3xweP8+rOrUwzmN+MRAj7TgtJtDaXE5PMyX2kCrklhg3rvOsOIfNeAWMQWO2F1GPc1kMD2vLAfw==
 
 uc.micro@^1.0.1, uc.micro@^1.0.5:
   version "1.0.6"
@@ -2745,14 +2817,14 @@ unicode-property-aliases-ecmascript@^1.0.4:
   integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==
 
 union-value@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
-  integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
+  integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
   dependencies:
     arr-union "^3.1.0"
     get-value "^2.0.6"
     is-extendable "^0.1.1"
-    set-value "^0.4.3"
+    set-value "^2.0.1"
 
 universalify@^0.1.0:
   version "0.1.2"
@@ -2805,9 +2877,9 @@ utils-merge@1.0.1:
   integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
 
 uuid@^3.3.2:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
-  integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
+  integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==
 
 value-equal@^0.4.0:
   version "0.4.0"
@@ -2828,6 +2900,11 @@ verror@1.10.0:
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
+void-elements@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+  integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
+
 watch@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c"
@@ -2861,6 +2938,13 @@ ws@^1.1.1:
     options ">=0.0.5"
     ultron "1.0.x"
 
+ws@^7.0.0:
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.1.2.tgz#c672d1629de8bb27a9699eb599be47aeeedd8f73"
+  integrity sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==
+  dependencies:
+    async-limiter "^1.0.0"
+
 yallist@^3.0.0, yallist@^3.0.3:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"