-[Lemmy Dev instance](https://dev.lemmy.ml) *This data is being backed up, and once federation is working, it will be the basis for a main instance.*
-
-This is a **very early beta version**, and a lot of features are currently broken or in active development, such as federation.
+## About The Project
Front Page|Post
---|---
@@ -42,17 +43,22 @@ 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.
-Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
+*Note: Federation is still in active development*
-- [Documentation](https://dev.lemmy.ml/docs/index.html)
-- [Releases / Changelog](/RELEASES.md)
-- [Contributing](https://dev.lemmy.ml/docs/contributing.html)
+### Why's it called Lemmy?
-## Repository Mirrors
+- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
+- The old school [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/).
-- [GitHub](https://github.com/dessalines/lemmy)
-- [Gitea](https://yerbamate.dev/dessalines/lemmy)
-- [GitLab](https://gitlab.com/dessalines/lemmy)
+### Built With
+
+- [Rust](https://www.rust-lang.org)
+- [Actix](https://actix.rs/)
+- [Diesel](http://diesel.rs/)
+- [Inferno](https://infernojs.org)
+- [Typescript](https://www.typescriptlang.org/)
## Features
@@ -84,72 +90,25 @@ Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Infern
- Can transfer site and communities to others.
- Can fully erase your data, replacing all posts and comments.
- NSFW post / community support.
+- OEmbed support via Iframely.
- High performance.
- Server is written in rust.
- Front end is `~80kB` gzipped.
- Supports arm64 / Raspberry Pi.
-## Why's it called Lemmy?
+## Installation
-- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
-- The old school [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/).
-
-## Install
-
-### 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/lemmy.hjson
-# Edit lemmy.hjson to do more configuration
-docker-compose up -d
-```
-
-and go to http://localhost:8536.
-
-[A sample nginx config](/ansible/templates/nginx.conf) (Image uploading won't work without this), could be setup with:
-
-```bash
-wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf
-# Replace the {{ vars }}
-sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
-```
-#### Updating
-
-To update to the newest version, run:
-
-```bash
-wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
-docker-compose up -d
-```
-
-### Ansible
-
-First, you need to [install Ansible on your local computer](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) (e.g. using `sudo apt install ansible`) or the equivalent for you platform.
-
-Then run the following commands on your local computer:
-
-```bash
-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 --become
-```
+- [Docker](https://dev.lemmy.ml/docs/administration_install_docker.html)
+- [Ansible](https://dev.lemmy.ml/docs/administration_install_ansible.html)
+- [Kubernetes](https://dev.lemmy.ml/docs/administration_install_kubernetes.html)
## Support / Donate
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
+- [Support on Liberapay.](https://liberapay.com/Lemmy)
- [Support on Patreon](https://www.patreon.com/dessalines).
- [List of Sponsors](https://dev.lemmy.ml/sponsors).
-- Soon to add either liberapay or opencollective.
### Crypto
@@ -157,28 +116,35 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
- ethereum: `0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01`
- monero: `41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV`
-## Translations
+## Contributing
+
+- [Contributing instructions](https://dev.lemmy.ml/docs/contributing.html)
+- [Docker Development](https://dev.lemmy.ml/docs/contributing_docker_development.html)
+- [Local Development](https://dev.lemmy.ml/docs/contributing_local_development.html)
+
+### Translations
If you'd like to add translations, take a look at the [English translation file](ui/src/translations/en.ts).
-- Languages supported: Catalan, (`ca`), Farsi (`fa`), English (`en`), Chinese (`zh`), Dutch (`nl`), Esperanto (`eo`), Finnish (`fi`), French (`fr`), Spanish (`es`), Swedish (`sv`), German (`de`), Russian (`ru`), Italian (`it`).
+- Languages supported: Brazilian Portuguese (`pt-br`), Catalan, (`ca`), Farsi (`fa`), English (`en`), Chinese (`zh`), Dutch (`nl`), Esperanto (`eo`), Finnish (`fi`), French (`fr`), Spanish (`es`), Swedish (`sv`), German (`de`), Russian (`ru`), Italian (`it`).
lang | done | missing
---- | ---- | -------
-ca | 98% | cross_posted_to,old,time,action
-de | 86% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,old,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-fa | 72% | cross_post,cross_posted_to,subscribed_to_communities,trending_communities,create_private_message,send_secure_message,send_message,message,mod,mods,moderates,remove_as_mod,appoint_as_mod,modlog,stickied,ban,ban_from_site,unban,unban_from_site,banned,number_of_subscribers,subscribers,both,saved,unsubscribe,subscribe,subscribed,old,api,docs,inbox,inbox_for,message_sent,notifications_error,messages,no_email_setup,matrix_user_id,private_message_disclaimer,url,body,copy_suggested_title,community,expand_here,subscribe_to_communities,theme,sponsor_message,general_sponsors,joined,by,to,from,landing_0,logged_in,community_moderator_already_exists,community_follower_already_exists,community_user_already_banned,no_slurs,admin_already_created,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-eo | 74% | cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,donate_to_lemmy,donate,from,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-es | 100% | cross_posted_to
-fi | 98% | cross_posted_to,old,time,action
-fr | 82% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-it | 83% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-nl | 99% | cross_posted_to,time,action
-ru | 71% | cross_posts,cross_post,cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-sv | 82% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-zh | 69% | cross_posts,cross_post,cross_posted_to,users,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+ca | 97% | cross_posted_to,old,support_on_liberapay,couldnt_get_comments,post_title_too_long,time,action
+de | 86% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,old,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+fa | 71% | cross_post,cross_posted_to,subscribed_to_communities,trending_communities,create_private_message,send_secure_message,send_message,message,mod,mods,moderates,remove_as_mod,appoint_as_mod,modlog,stickied,ban,ban_from_site,unban,unban_from_site,banned,number_of_subscribers,subscribers,both,saved,unsubscribe,subscribe,subscribed,old,api,docs,inbox,inbox_for,message_sent,notifications_error,messages,no_email_setup,matrix_user_id,private_message_disclaimer,url,body,copy_suggested_title,community,expand_here,subscribe_to_communities,theme,sponsor_message,support_on_liberapay,general_sponsors,joined,by,to,from,landing_0,logged_in,couldnt_get_comments,community_moderator_already_exists,community_follower_already_exists,community_user_already_banned,post_title_too_long,no_slurs,admin_already_created,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+eo | 73% | cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,support_on_liberapay,donate_to_lemmy,donate,from,are_you_sure,yes,no,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+es | 99% | cross_posted_to,couldnt_get_comments,post_title_too_long
+fi | 97% | cross_posted_to,old,support_on_liberapay,couldnt_get_comments,post_title_too_long,time,action
+fr | 100% |
+it | 82% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+nl | 98% | cross_posted_to,couldnt_get_comments,post_title_too_long,time,action
+pt-br | 99% | couldnt_get_comments,post_title_too_long
+ru | 70% | cross_posts,cross_post,cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,support_on_liberapay,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+sv | 81% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+zh | 69% | cross_posts,cross_post,cross_posted_to,users,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
If you'd like to update this report, run:
@@ -188,6 +154,14 @@ cd ui
ts-node translation_report.ts
```
+## Contact
+
+- [Mastodon](https://mastodon.social/@LemmyDev) - [![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev)
+- [Matrix](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org) - [![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
+- [GitHub](https://github.com/dessalines/lemmy)
+- [Gitea](https://yerbamate.dev/dessalines/lemmy)
+- [GitLab](https://gitlab.com/dessalines/lemmy)
+
## Credits
Logo made by Andy Cuccaro (@andycuccaro) under the CC-BY-SA 4.0 license.
diff --git a/ansible/VERSION b/ansible/VERSION
index 882307ccd..6095ec4eb 100644
--- a/ansible/VERSION
+++ b/ansible/VERSION
@@ -1 +1 @@
-v0.6.11
+v0.6.25
diff --git a/ansible/lemmy.yml b/ansible/lemmy.yml
index 87f71699d..8d5e22641 100644
--- a/ansible/lemmy.yml
+++ b/ansible/lemmy.yml
@@ -35,6 +35,7 @@
with_items:
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' }
+ - { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' }
- name: add config file (only during initial setup)
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
@@ -63,4 +64,4 @@
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'"
+ job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'"
diff --git a/ansible/lemmy_dev.yml b/ansible/lemmy_dev.yml
index c15569faf..e9b8364f3 100644
--- a/ansible/lemmy_dev.yml
+++ b/ansible/lemmy_dev.yml
@@ -37,6 +37,7 @@
with_items:
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' }
+ - { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' }
- name: add config file (only during initial setup)
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
@@ -97,4 +98,4 @@
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'"
+ job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'"
diff --git a/ansible/templates/docker-compose.yml b/ansible/templates/docker-compose.yml
index 2693d7ad2..bf9aeeb5a 100644
--- a/ansible/templates/docker-compose.yml
+++ b/ansible/templates/docker-compose.yml
@@ -30,6 +30,14 @@ services:
- lemmy_pictshare:/usr/share/nginx/html/data
restart: always
+ lemmy_iframely:
+ image: dogbin/iframely:latest
+ ports:
+ - "127.0.0.1:8061:8061"
+ volumes:
+ - ./iframely.config.local.js:/iframely/config.local.js:ro
+ restart: always
+
postfix:
image: mwader/postfix-relay
environment:
@@ -38,3 +46,4 @@ services:
volumes:
lemmy_db:
lemmy_pictshare:
+ lemmy_iframely:
diff --git a/ansible/templates/nginx.conf b/ansible/templates/nginx.conf
index 9f31140b2..04e5a6436 100644
--- a/ansible/templates/nginx.conf
+++ b/ansible/templates/nginx.conf
@@ -80,6 +80,13 @@ server {
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
+
+ location /iframely/ {
+ proxy_pass http://0.0.0.0:8061/;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
}
# Anonymize IP addresses
diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml
index eabd334d5..987be4d5b 100644
--- a/docker/dev/docker-compose.yml
+++ b/docker/dev/docker-compose.yml
@@ -28,6 +28,14 @@ services:
volumes:
- lemmy_pictshare:/usr/share/nginx/html/data
restart: always
+ lemmy_iframely:
+ image: dogbin/iframely:latest
+ ports:
+ - "127.0.0.1:8061:8061"
+ volumes:
+ - ../iframely.config.local.js:/iframely/config.local.js:ro
+ restart: always
volumes:
lemmy_db:
lemmy_pictshare:
+ lemmy_iframely:
diff --git a/docker/iframely.config.local.js b/docker/iframely.config.local.js
new file mode 100644
index 000000000..5c00cb143
--- /dev/null
+++ b/docker/iframely.config.local.js
@@ -0,0 +1,283 @@
+(function() {
+ var config = {
+
+ // Specify a path for custom plugins. Custom plugins will override core plugins.
+ // CUSTOM_PLUGINS_PATH: __dirname + '/yourcustom-plugin-folder',
+
+ DEBUG: false,
+ RICH_LOG_ENABLED: false,
+
+ // For embeds that require render, baseAppUrl will be used as the host.
+ baseAppUrl: "http://yourdomain.com",
+ relativeStaticUrl: "/r",
+
+ // Or just skip built-in renders altogether
+ SKIP_IFRAMELY_RENDERS: true,
+
+ // For legacy reasons the response format of Iframely open-source is
+ // different by default as it does not group the links array by rel.
+ // In order to get the same grouped response as in Cloud API,
+ // add `&group=true` to your request to change response per request
+ // or set `GROUP_LINKS` in your config to `true` for a global change.
+ GROUP_LINKS: true,
+
+ // Number of maximum redirects to follow before aborting the page
+ // request with `redirect loop` error.
+ MAX_REDIRECTS: 4,
+
+ SKIP_OEMBED_RE_LIST: [
+ // /^https?:\/\/yourdomain\.com\//,
+ ],
+
+ /*
+ // Used to pass parameters to the generate functions when creating HTML elements
+ // disableSizeWrapper: Don't wrap element (iframe, video, etc) in a positioned div
+ GENERATE_LINK_PARAMS: {
+ disableSizeWrapper: true
+ },
+ */
+
+ port: 8061, //can be overridden by PORT env var
+ host: '0.0.0.0', // Dockers beware. See https://github.com/itteco/iframely/issues/132#issuecomment-242991246
+ //can be overridden by HOST env var
+
+ // Optional SSL cert, if you serve under HTTPS.
+ /*
+ ssl: {
+ key: require('fs').readFileSync(__dirname + '/key.pem'),
+ cert: require('fs').readFileSync(__dirname + '/cert.pem'),
+ port: 443
+ },
+ */
+
+ /*
+ Supported cache engines:
+ - no-cache - no caching will be used.
+ - node-cache - good for debug, node memory will be used (https://github.com/tcs-de/nodecache).
+ - redis - https://github.com/mranney/node_redis.
+ - memcached - https://github.com/3rd-Eden/node-memcached
+ */
+ CACHE_ENGINE: 'node-cache',
+ CACHE_TTL: 0, // In seconds.
+ // 0 = 'never expire' for memcached & node-cache to let cache engine decide itself when to evict the record
+ // 0 = 'no cache' for redis. Use high enough (e.g. 365*24*60*60*1000) ttl for similar 'never expire' approach instead
+
+ /*
+ // Redis cache options.
+ REDIS_OPTIONS: {
+ host: '127.0.0.1',
+ port: 6379
+ },
+ */
+
+ /*
+ // Memcached options. See https://github.com/3rd-Eden/node-memcached#server-locations
+ MEMCACHED_OPTIONS: {
+ locations: "127.0.0.1:11211"
+ }
+ */
+
+ /*
+ // Access-Control-Allow-Origin list.
+ allowedOrigins: [
+ "*",
+ "http://another_domain.com"
+ ],
+ */
+
+ /*
+ // Uncomment to enable plugin testing framework.
+ tests: {
+ mongodb: 'mongodb://localhost:27017/iframely-tests',
+ single_test_timeout: 10 * 1000,
+ plugin_test_period: 2 * 60 * 60 * 1000,
+ relaunch_script_period: 5 * 60 * 1000
+ },
+ */
+
+ // If there's no response from remote server, the timeout will occur after
+ RESPONSE_TIMEOUT: 5 * 1000, //ms
+
+ /* From v1.4.0, Iframely supports HTTP/2 by default. Disable it, if you'd rather not.
+ Alternatively, you can also disable per origin. See `proxy` option below.
+ */
+ // DISABLE_HTTP2: true,
+
+ // Customize API calls to oembed endpoints.
+ ADD_OEMBED_PARAMS: [{
+ // Endpoint url regexp array.
+ re: [/^http:\/\/api\.instagram\.com\/oembed/],
+ // Custom get params object.
+ params: {
+ hidecaption: true
+ }
+ }, {
+ re: [/^https:\/\/www\.facebook\.com\/plugins\/page\/oembed\.json/i],
+ params: {
+ show_posts: 0,
+ show_facepile: 0,
+ maxwidth: 600
+ }
+ }, {
+ // match i=user or i=moment or i=timeline to configure these types invidually
+ // see params spec at https://dev.twitter.com/web/embedded-timelines/oembed
+ re: [/^https?:\/\/publish\.twitter\.com\/oembed\?i=user/i],
+ params: {
+ limit: 1,
+ maxwidth: 600
+ }
+ /*
+ }, {
+ // Facebook https://developers.facebook.com/docs/plugins/oembed-endpoints
+ re: [/^https:\/\/www\.facebook\.com\/plugins\/\w+\/oembed\.json/i],
+ params: {
+ // Skip script tag and fb-root div.
+ omitscript: true
+ }
+ */
+ }],
+
+ /*
+ // Configure use of HTTP proxies as needed.
+ // You don't have to specify all options per regex - just what you need to override
+ PROXY: [{
+ re: [/^https?:\/\/www\.domain\.com/],
+ proxy_server: 'http://1.2.3.4:8080',
+ user_agent: 'CHANGE YOUR AGENT',
+ headers: {
+ // HTTP headers
+ // Overrides previous params if overlapped.
+ },
+ request_options: {
+ // Refer to: https://github.com/request/request
+ // Overrides previous params if overlapped.
+ },
+ disable_http2: true
+ }],
+ */
+
+ // Customize API calls to 3rd parties. At the very least - configure required keys.
+ providerOptions: {
+ locale: "en_US", // ISO 639-1 two-letter language code, e.g. en_CA or fr_CH.
+ // Will be added as highest priotity in accept-language header with each request.
+ // Plus is used in FB, YouTube and perhaps other plugins
+ "twitter": {
+ "max-width": 550,
+ "min-width": 250,
+ hide_media: false,
+ hide_thread: false,
+ omit_script: false,
+ center: false,
+ // dnt: true,
+ cache_ttl: 100 * 365 * 24 * 3600 // 100 Years.
+ },
+ readability: {
+ enabled: false
+ // allowPTagDescription: true // to enable description fallback to first paragraph
+ },
+ images: {
+ loadSize: false, // if true, will try an load first bytes of all images to get/confirm the sizes
+ checkFavicon: false // if true, will verify all favicons
+ },
+ tumblr: {
+ consumer_key: "INSERT YOUR VALUE"
+ // media_only: true // disables status embeds for images and videos - will return plain media
+ },
+ google: {
+ // https://developers.google.com/maps/documentation/embed/guide#api_key
+ maps_key: "INSERT YOUR VALUE"
+ },
+
+ /*
+ // Optional Camo Proxy to wrap all images: https://github.com/atmos/camo
+ camoProxy: {
+ camo_proxy_key: "INSERT YOUR VALUE",
+ camo_proxy_host: "INSERT YOUR VALUE"
+ // ssl_only: true // will only proxy non-ssl images
+ },
+ */
+
+ // List of query parameters to add to YouTube and Vimeo frames
+ // Start it with leading "?". Or omit alltogether for default values
+ // API key is optional, youtube will work without it too.
+ // It is probably the same API key you use for Google Maps.
+ youtube: {
+ // api_key: "INSERT YOUR VALUE",
+ get_params: "?rel=0&showinfo=1" // https://developers.google.com/youtube/player_parameters
+ },
+ vimeo: {
+ get_params: "?byline=0&badge=0" // https://developer.vimeo.com/player/embedding
+ },
+
+ /*
+ soundcloud: {
+ old_player: true // enables classic player
+ },
+ giphy: {
+ media_only: true // disables branded player for gifs and returns just the image
+ }
+ */
+ /*
+ bandcamp: {
+ get_params: '/size=large/bgcol=333333/linkcol=ffffff/artwork=small/transparent=true/',
+ media: {
+ album: {
+ height: 472,
+ 'max-width': 700
+ },
+ track: {
+ height: 120,
+ 'max-width': 700
+ }
+ }
+ }
+ */
+ },
+
+ // WHITELIST_WILDCARD, if present, will be added to whitelist as record for top level domain: "*"
+ // with it, you can define what parsers do when they run accross unknown publisher.
+ // If absent or empty, all generic media parsers will be disabled except for known domains
+ // More about format: https://iframely.com/docs/qa-format
+
+ /*
+ WHITELIST_WILDCARD: {
+ "twitter": {
+ "player": "allow",
+ "photo": "deny"
+ },
+ "oembed": {
+ "video": "allow",
+ "photo": "allow",
+ "rich": "deny",
+ "link": "deny"
+ },
+ "og": {
+ "video": ["allow", "ssl", "responsive"]
+ },
+ "iframely": {
+ "survey": "allow",
+ "reader": "allow",
+ "player": "allow",
+ "image": "allow"
+ },
+ "html-meta": {
+ "video": ["allow", "responsive"],
+ "promo": "allow"
+ }
+ }
+ */
+
+ // Black-list any of the inappropriate domains. Iframely will return 417
+ // At minimum, keep your localhosts blacklisted to avoid SSRF
+ BLACKLIST_DOMAINS_RE: [
+ /^https?:\/\/127\.0\.0\.1/i,
+ /^https?:\/\/localhost/i,
+
+ // And this is AWS metadata service
+ // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
+ /^https?:\/\/169\.254\.169\.254/
+ ]
+ };
+
+ module.exports = config;
+})();
diff --git a/docker/lemmy.hjson b/docker/lemmy.hjson
index fce4470ca..55c2f2b76 100644
--- a/docker/lemmy.hjson
+++ b/docker/lemmy.hjson
@@ -29,7 +29,7 @@
# rate limits for various user actions, by user ip
rate_limit: {
# maximum number of messages created in interval
- message: 30
+ message: 180
# interval length for message limit
message_per_second: 60
# maximum number of posts created in interval
diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml
index da78126c3..ac46ece30 100644
--- a/docker/prod/docker-compose.yml
+++ b/docker/prod/docker-compose.yml
@@ -11,7 +11,7 @@ services:
- lemmy_db:/var/lib/postgresql/data
restart: always
lemmy:
- image: dessalines/lemmy:v0.6.11
+ image: dessalines/lemmy:v0.6.25
ports:
- "127.0.0.1:8536:8536"
restart: always
@@ -26,6 +26,14 @@ services:
volumes:
- lemmy_pictshare:/usr/share/nginx/html/data
restart: always
+ lemmy_iframely:
+ image: dogbin/iframely:latest
+ ports:
+ - "127.0.0.1:8061:8061"
+ volumes:
+ - ./iframely.config.local.js:/iframely/config.local.js:ro
+ restart: always
volumes:
lemmy_db:
lemmy_pictshare:
+ lemmy_iframely:
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index c2df6223e..0514cbcda 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -4,6 +4,7 @@
- [Features](about_features.md)
- [Goals](about_goals.md)
- [Post and Comment Ranking](about_ranking.md)
+ - [Guide](about_guide.md)
- [Administration](administration.md)
- [Install with Docker](administration_install_docker.md)
- [Install with Ansible](administration_install_ansible.md)
diff --git a/docs/src/about.md b/docs/src/about.md
index 7aa1be27a..33ecb211a 100644
--- a/docs/src/about.md
+++ b/docs/src/about.md
@@ -1,6 +1,8 @@
-# Lemmy - A link aggregator / reddit clone for the fediverse.
+## About The Project
-[Lemmy Dev instance](https://dev.lemmy.ml) *for testing purposes only*
+Front Page|Post
+---|---
+![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png)
[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
@@ -10,6 +12,8 @@ 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.
+*Note: Federation is still in active development*
+
### Why's it called Lemmy?
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
@@ -17,4 +21,10 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins
- 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://infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
+### Built With
+
+- [Rust](https://www.rust-lang.org)
+- [Actix](https://actix.rs/)
+- [Diesel](http://diesel.rs/)
+- [Inferno](https://infernojs.org)
+- [Typescript](https://www.typescriptlang.org/)
diff --git a/docs/src/about_features.md b/docs/src/about_features.md
index 5c70c978e..a8371fc4d 100644
--- a/docs/src/about_features.md
+++ b/docs/src/about_features.md
@@ -1,20 +1,27 @@
# Features
+
- Open source, [AGPL License](/LICENSE).
- Self hostable, easy to deploy.
- Comes with [Docker](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes).
- Clean, mobile-friendly interface.
+ - Only a minimum of a username and password is required to sign up!
+ - User avatar support.
- Live-updating Comment threads.
- Full vote scores `(+/-)` like old reddit.
- Themes, including light, dark, and solarized.
- Emojis with autocomplete support. Start typing `:`
- User tagging using `@`, Community tagging using `#`.
+ - Integrated image uploading in both posts and comments.
+ - A post can consist of a title and any combination of self text, a URL, or nothing else.
- Notifications, on comment replies and when you're tagged.
+ - Notifications can be sent via email.
- i18n / internationalization support.
- RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`.
- Cross-posting support.
- A *similar post search* when creating new posts. Great for question / answer communities.
- Moderation abilities.
- Public Moderation Logs.
+ - Can sticky posts to the top of communities.
- Both site admins, and community moderators, who can appoint other moderators.
- Can lock, remove, and restore posts and comments.
- Can ban and unban users from communities and the site.
diff --git a/docs/src/about_guide.md b/docs/src/about_guide.md
new file mode 100644
index 000000000..f22e201be
--- /dev/null
+++ b/docs/src/about_guide.md
@@ -0,0 +1,28 @@
+# Lemmy Guide
+
+Start typing...
+
+- `@a_user_name` to get a list of usernames.
+- `#a_community` to get a list of communities.
+- `:emoji` to get a list of emojis.
+
+## Markdown Guide
+
+Type | Or | … to Get
+--- | --- | ---
+\*Italic\* | \_Italic\_ | _Italic_
+\*\*Bold\*\* | \_\_Bold\_\_ | **Bold**
+\# Heading 1 | Heading 1 ========= |
+\* List \* List \* List | \- List \- List \- List | * List * List * List
+1\. One 2\. Two 3\. Three | 1) One 2) Two 3) Three | 1. One 2. Two 3. Three
+Horizontal Rule \--- | Horizontal Rule \*\*\* | Horizontal Rule
+\`Inline code\` with backticks | |`Inline code` with backticks
+\`\`\` \# code block print '3 backticks or' print 'indent 4 spaces' \`\`\` | ····\# code block ····print '3 backticks or' ····print 'indent 4 spaces' | \# code block print '3 backticks or' print 'indent 4 spaces'
+::: spoiler hidden or nsfw stuff *a bunch of spoilers here* ::: | | hidden or nsfw stuff
a bunch of spoilers here
+
+[CommonMark Tutorial](https://commonmark.org/help/tutorial/)
+
diff --git a/docs/src/administration.md b/docs/src/administration.md
index c4c2b01f1..9851232d9 100644
--- a/docs/src/administration.md
+++ b/docs/src/administration.md
@@ -1 +1,3 @@
-Information for Lemmy instance admins, and those who want to start an instance.
\ No newline at end of file
+# Admin info
+
+Information for Lemmy instance admins, and those who want to start an instance.
diff --git a/docs/src/administration_configuration.md b/docs/src/administration_configuration.md
index 73ea35042..8900ce8d5 100644
--- a/docs/src/administration_configuration.md
+++ b/docs/src/administration_configuration.md
@@ -1,6 +1,15 @@
+# Configuration
+
The configuration is based on the file [defaults.hjson](server/config/defaults.hjson). This file also contains documentation for all the available options. To override the defaults, you can copy the options you want to change into your local `config.hjson` file.
Additionally, you can override any config files with environment variables. These have the same name as the config options, and are prefixed with `LEMMY_`. For example, you can override the `database.password` with
`LEMMY__DATABASE__POOL_SIZE=10`.
An additional option `LEMMY_DATABASE_URL` is available, which can be used with a PostgreSQL connection string like `postgres://lemmy:password@lemmy_db:5432/lemmy`, passing all connection details at once.
+
+If the Docker container is not used, manually create the database specified above by running the following commands:
+
+```bash
+cd server
+./db-init.sh
+```
diff --git a/docs/src/administration_install_ansible.md b/docs/src/administration_install_ansible.md
index 03642b897..875dae6a1 100644
--- a/docs/src/administration_install_ansible.md
+++ b/docs/src/administration_install_ansible.md
@@ -1,3 +1,5 @@
+# Ansible Installation
+
First, you need to [install Ansible on your local computer](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) (e.g. using `sudo apt install ansible`) or the equivalent for you platform.
Then run the following commands on your local computer:
diff --git a/docs/src/administration_install_docker.md b/docs/src/administration_install_docker.md
index 64abe737e..992049839 100644
--- a/docs/src/administration_install_docker.md
+++ b/docs/src/administration_install_docker.md
@@ -1,3 +1,5 @@
+# Docker Installation
+
Make sure you have both docker and docker-compose(>=`1.24.0`) installed:
```bash
@@ -5,20 +7,21 @@ 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/lemmy.hjson
-# Edit lemmy.hjson to do more configuration
+wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/iframely.config.local.js
+# Edit lemmy.hjson, and docker-compose.yml to do more configuration (like adding a custom password)
docker-compose up -d
```
and go to http://localhost:8536.
-[A sample nginx config](/ansible/templates/nginx.conf), could be setup with:
+[A sample nginx config](/ansible/templates/nginx.conf) (Note: Avatar / Image uploading won't work without this), could be setup with:
```bash
wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf
# Replace the {{ vars }}
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
```
-#### Updating
+## Updating
To update to the newest version, run:
diff --git a/docs/src/administration_install_kubernetes.md b/docs/src/administration_install_kubernetes.md
index 886558dce..729cb1558 100644
--- a/docs/src/administration_install_kubernetes.md
+++ b/docs/src/administration_install_kubernetes.md
@@ -1,3 +1,5 @@
+# Kubernetes Installation
+
You'll need to have an existing Kubernetes cluster and [storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/).
Setting this up will vary depending on your provider.
To try it locally, you can use [MicroK8s](https://microk8s.io/) or [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/).
diff --git a/docs/src/contributing_docker_development.md b/docs/src/contributing_docker_development.md
index 0ed5bde5e..d5ab58293 100644
--- a/docs/src/contributing_docker_development.md
+++ b/docs/src/contributing_docker_development.md
@@ -1,4 +1,6 @@
-Run:
+# Docker Development
+
+## Running
```bash
git clone https://github.com/dessalines/lemmy
@@ -8,4 +10,4 @@ cd lemmy/docker/dev
and go to http://localhost:8536.
-Note that compile times are relatively long with Docker, because builds can't be properly cached. If this is a problem for you, you should use [Local Development](contributing_local_development.md).
\ No newline at end of file
+Note that compile times when changing `Cargo.toml` are relatively long with Docker, because builds can't be incrementally cached. If this is a problem for you, you should use [Local Development](contributing_local_development.md).
diff --git a/docs/src/contributing_local_development.md b/docs/src/contributing_local_development.md
index c19bcba84..175b000c7 100644
--- a/docs/src/contributing_local_development.md
+++ b/docs/src/contributing_local_development.md
@@ -7,9 +7,16 @@
#### 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 LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
+cd server
+./db-init.sh
+```
+
+Or run the commands manually:
+
+```bash
+psql -c "create user lemmy with password 'password' superuser;" -U postgres
+psql -c 'create database lemmy with owner lemmy;' -U postgres
+export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
```
#### Running
diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md
index 9e87d4faa..a73a1c133 100644
--- a/docs/src/contributing_websocket_http_api.md
+++ b/docs/src/contributing_websocket_http_api.md
@@ -1,4 +1,5 @@
# Lemmy API
+
*Note: this may lag behind the actual API endpoints [here](../server/src/api).*
diff --git a/install.sh b/install.sh
index 168a1f6b0..b368891cf 100755
--- a/install.sh
+++ b/install.sh
@@ -1,13 +1,41 @@
-#!/bin/sh
+#!/bin/bash
set -e
+# Set the database variable to the default first.
+# Don't forget to change this string to your actual database parameters
+# if you don't plan to initialize the database in this script.
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
+
+# Set other environment variables
export JWT_SECRET=changeme
export HOSTNAME=rrr
+# Optionally initialize the database
+init_db_valid=0
+init_db_final=0
+while [ "$init_db_valid" == 0 ]
+do
+ read -p "Initialize database (y/n)? " init_db
+ case "${init_db,,}" in
+ y|yes ) init_db_valid=1; init_db_final=1;;
+ n|no ) init_db_valid=1; init_db_final=0;;
+ * ) echo "Invalid input" 1>&2;;
+ esac
+ echo
+done
+if [ "$init_db_final" = 1 ]
+then
+ source ./server/db-init.sh
+ read -n 1 -s -r -p "Press ANY KEY to continue execution of this script, press CTRL+C to quit..."
+ echo
+fi
+
+# Build the web client
cd ui
yarn
yarn build
+
+# Build and run the backend
cd ../server
cargo run
diff --git a/server/config/defaults.hjson b/server/config/defaults.hjson
index 6f4daaae8..7eda46ec4 100644
--- a/server/config/defaults.hjson
+++ b/server/config/defaults.hjson
@@ -32,7 +32,7 @@
# rate limits for various user actions, by user ip
rate_limit: {
# maximum number of messages created in interval
- message: 30
+ message: 180
# interval length for message limit
message_per_second: 60
# maximum number of posts created in interval
diff --git a/server/db-init.sh b/server/db-init.sh
new file mode 100755
index 000000000..c9150e9de
--- /dev/null
+++ b/server/db-init.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+username=lemmy
+dbname=lemmy
+port=5432
+
+password=""
+password_confirm=""
+password_valid=0
+
+while [ "$password_valid" == 0 ]
+do
+ read -p "Enter database password: " -s password
+ echo
+
+ read -p "Verify database password: " -s password_confirm
+ echo
+ echo
+
+ # Start the loop from the top if either check fails
+ if [ -z "$password" ]
+ then
+ echo "Error: Password cannot be empty." 1>&2
+ echo
+ continue
+ fi
+ if [ "$password" != "$password_confirm" ]
+ then
+ echo "Error: Passwords don't match." 1>&2
+ echo
+ continue
+ fi
+
+ # Set the password_valid variable to break out of the loop
+ password_valid=1
+done
+
+
+psql -c "CREATE USER $username WITH PASSWORD '$password' SUPERUSER;" -U postgres
+psql -c 'CREATE DATABASE $dbname WITH OWNER $username;' -U postgres
+export LEMMY_DATABASE_URL=postgres://$username:$password@localhost:$port/$dbname
+
+echo $LEMMY_DATABASE_URL
diff --git a/server/migrations/2020-02-06-165953_change_post_title_length/down.sql b/server/migrations/2020-02-06-165953_change_post_title_length/down.sql
new file mode 100644
index 000000000..2bc765f85
--- /dev/null
+++ b/server/migrations/2020-02-06-165953_change_post_title_length/down.sql
@@ -0,0 +1,132 @@
+-- Drop the dependent views
+drop view post_view;
+drop view post_mview;
+drop materialized view post_aggregates_mview;
+drop view post_aggregates_view;
+drop view mod_remove_post_view;
+drop view mod_sticky_post_view;
+drop view mod_lock_post_view;
+drop view mod_remove_comment_view;
+
+alter table post alter column name type varchar(100);
+
+-- regen post view
+create view post_aggregates_view as
+select
+p.*,
+(select u.banned from user_ u where p.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
+(select name from user_ where p.creator_id = user_.id) as creator_name,
+(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
+(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;
+
+create materialized view post_aggregates_mview as select * from post_aggregates_view;
+
+create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
+
+create view post_view as
+with all_post as (
+ select
+ pa.*
+ from post_aggregates_view pa
+)
+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
+;
+
+create view post_mview as
+with all_post as (
+ select
+ pa.*
+ from post_aggregates_mview pa
+)
+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
+;
+
+-- The mod views
+
+create view mod_remove_post_view as
+select mrp.*,
+(select name from user_ u where mrp.mod_user_id = u.id) as mod_user_name,
+(select name from post p where mrp.post_id = p.id) as post_name,
+(select c.id from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_id,
+(select c.name from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_name
+from mod_remove_post mrp;
+
+create view mod_lock_post_view as
+select mlp.*,
+(select name from user_ u where mlp.mod_user_id = u.id) as mod_user_name,
+(select name from post p where mlp.post_id = p.id) as post_name,
+(select c.id from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_id,
+(select c.name from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_name
+from mod_lock_post mlp;
+
+create view mod_remove_comment_view as
+select mrc.*,
+(select name from user_ u where mrc.mod_user_id = u.id) as mod_user_name,
+(select c.id from comment c where mrc.comment_id = c.id) as comment_user_id,
+(select name from user_ u, comment c where mrc.comment_id = c.id and u.id = c.creator_id) as comment_user_name,
+(select content from comment c where mrc.comment_id = c.id) as comment_content,
+(select p.id from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_id,
+(select p.name from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_name,
+(select co.id from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_id,
+(select co.name from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_name
+from mod_remove_comment mrc;
+
+create view mod_sticky_post_view as
+select msp.*,
+(select name from user_ u where msp.mod_user_id = u.id) as mod_user_name,
+(select name from post p where msp.post_id = p.id) as post_name,
+(select c.id from post p, community c where msp.post_id = p.id and p.community_id = c.id) as community_id,
+(select c.name from post p, community c where msp.post_id = p.id and p.community_id = c.id) as community_name
+from mod_sticky_post msp;
diff --git a/server/migrations/2020-02-06-165953_change_post_title_length/up.sql b/server/migrations/2020-02-06-165953_change_post_title_length/up.sql
new file mode 100644
index 000000000..006a7d049
--- /dev/null
+++ b/server/migrations/2020-02-06-165953_change_post_title_length/up.sql
@@ -0,0 +1,133 @@
+-- Drop the dependent views
+drop view post_view;
+drop view post_mview;
+drop materialized view post_aggregates_mview;
+drop view post_aggregates_view;
+drop view mod_remove_post_view;
+drop view mod_sticky_post_view;
+drop view mod_lock_post_view;
+drop view mod_remove_comment_view;
+
+-- Add the extra post limit
+alter table post alter column name type varchar(200);
+
+-- regen post view
+create view post_aggregates_view as
+select
+p.*,
+(select u.banned from user_ u where p.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
+(select name from user_ where p.creator_id = user_.id) as creator_name,
+(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
+(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;
+
+create materialized view post_aggregates_mview as select * from post_aggregates_view;
+
+create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
+
+create view post_view as
+with all_post as (
+ select
+ pa.*
+ from post_aggregates_view pa
+)
+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
+;
+
+create view post_mview as
+with all_post as (
+ select
+ pa.*
+ from post_aggregates_mview pa
+)
+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
+;
+
+-- The mod views
+
+create view mod_remove_post_view as
+select mrp.*,
+(select name from user_ u where mrp.mod_user_id = u.id) as mod_user_name,
+(select name from post p where mrp.post_id = p.id) as post_name,
+(select c.id from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_id,
+(select c.name from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_name
+from mod_remove_post mrp;
+
+create view mod_lock_post_view as
+select mlp.*,
+(select name from user_ u where mlp.mod_user_id = u.id) as mod_user_name,
+(select name from post p where mlp.post_id = p.id) as post_name,
+(select c.id from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_id,
+(select c.name from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_name
+from mod_lock_post mlp;
+
+create view mod_remove_comment_view as
+select mrc.*,
+(select name from user_ u where mrc.mod_user_id = u.id) as mod_user_name,
+(select c.id from comment c where mrc.comment_id = c.id) as comment_user_id,
+(select name from user_ u, comment c where mrc.comment_id = c.id and u.id = c.creator_id) as comment_user_name,
+(select content from comment c where mrc.comment_id = c.id) as comment_content,
+(select p.id from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_id,
+(select p.name from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_name,
+(select co.id from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_id,
+(select co.name from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_name
+from mod_remove_comment mrc;
+
+create view mod_sticky_post_view as
+select msp.*,
+(select name from user_ u where msp.mod_user_id = u.id) as mod_user_name,
+(select name from post p where msp.post_id = p.id) as post_name,
+(select c.id from post p, community c where msp.post_id = p.id and p.community_id = c.id) as community_id,
+(select c.name from post p, community c where msp.post_id = p.id and p.community_id = c.id) as community_name
+from mod_sticky_post msp;
diff --git a/server/migrations/2020-02-07-210055_add_comment_subscribed/down.sql b/server/migrations/2020-02-07-210055_add_comment_subscribed/down.sql
new file mode 100644
index 000000000..b6120d151
--- /dev/null
+++ b/server/migrations/2020-02-07-210055_add_comment_subscribed/down.sql
@@ -0,0 +1,206 @@
+
+drop view reply_view;
+drop view user_mention_view;
+drop view user_mention_mview;
+drop view comment_view;
+drop view comment_mview;
+drop materialized view comment_aggregates_mview;
+drop view comment_aggregates_view;
+
+-- reply and comment view
+create view comment_aggregates_view as
+select
+c.*,
+(select community_id from post p where p.id = c.post_id),
+(select u.banned from user_ u where c.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
+(select name from user_ where c.creator_id = user_.id) as creator_name,
+(select avatar from user_ where c.creator_id = user_.id) as creator_avatar,
+coalesce(sum(cl.score), 0) as score,
+count (case when cl.score = 1 then 1 else null end) as upvotes,
+count (case when cl.score = -1 then 1 else null end) as downvotes
+from comment c
+left join comment_like cl on c.id = cl.comment_id
+group by c.id;
+
+create materialized view comment_aggregates_mview as select * from comment_aggregates_view;
+
+create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id);
+
+create view comment_view as
+with all_comment as
+(
+ select
+ ca.*
+ from comment_aggregates_view ca
+)
+
+select
+ac.*,
+u.id as user_id,
+coalesce(cl.score, 0) as my_vote,
+(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+
+union all
+
+select
+ ac.*,
+ null as user_id,
+ null as my_vote,
+ null as saved
+from all_comment ac
+;
+
+create view comment_mview as
+with all_comment as
+(
+ select
+ ca.*
+ from comment_aggregates_mview ca
+)
+
+select
+ac.*,
+u.id as user_id,
+coalesce(cl.score, 0) as my_vote,
+(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+
+union all
+
+select
+ ac.*,
+ null as user_id,
+ null as my_vote,
+ null as saved
+from all_comment ac
+;
+
+
+-- Do the reply_view referencing the comment_mview
+create view reply_view as
+with closereply as (
+ select
+ c2.id,
+ c2.creator_id as sender_id,
+ c.creator_id as recipient_id
+ from comment c
+ inner join comment c2 on c.id = c2.parent_id
+ where c2.creator_id != c.creator_id
+ -- Do union where post is null
+ union
+ select
+ c.id,
+ c.creator_id as sender_id,
+ p.creator_id as recipient_id
+ from comment c, post p
+ where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
+)
+select cv.*,
+closereply.recipient_id
+from comment_mview cv, closereply
+where closereply.id = cv.id
+;
+
+-- user mention
+create view user_mention_view as
+select
+ c.id,
+ um.id as user_mention_id,
+ c.creator_id,
+ c.post_id,
+ c.parent_id,
+ c.content,
+ c.removed,
+ um.read,
+ c.published,
+ c.updated,
+ c.deleted,
+ c.community_id,
+ c.banned,
+ c.banned_from_community,
+ c.creator_name,
+ c.creator_avatar,
+ c.score,
+ c.upvotes,
+ c.downvotes,
+ c.user_id,
+ c.my_vote,
+ c.saved,
+ um.recipient_id
+from user_mention um, comment_view c
+where um.comment_id = c.id;
+
+
+create view user_mention_mview as
+with all_comment as
+(
+ select
+ ca.*
+ from comment_aggregates_mview ca
+)
+
+select
+ ac.id,
+ um.id as user_mention_id,
+ ac.creator_id,
+ ac.post_id,
+ ac.parent_id,
+ ac.content,
+ ac.removed,
+ um.read,
+ ac.published,
+ ac.updated,
+ ac.deleted,
+ ac.community_id,
+ ac.banned,
+ ac.banned_from_community,
+ ac.creator_name,
+ ac.creator_avatar,
+ ac.score,
+ ac.upvotes,
+ ac.downvotes,
+ u.id as user_id,
+ coalesce(cl.score, 0) as my_vote,
+ (select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
+ um.recipient_id
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+left join user_mention um on um.comment_id = ac.id
+
+union all
+
+select
+ ac.id,
+ um.id as user_mention_id,
+ ac.creator_id,
+ ac.post_id,
+ ac.parent_id,
+ ac.content,
+ ac.removed,
+ um.read,
+ ac.published,
+ ac.updated,
+ ac.deleted,
+ ac.community_id,
+ ac.banned,
+ ac.banned_from_community,
+ ac.creator_name,
+ ac.creator_avatar,
+ ac.score,
+ ac.upvotes,
+ ac.downvotes,
+ null as user_id,
+ null as my_vote,
+ null as saved,
+ um.recipient_id
+from all_comment ac
+left join user_mention um on um.comment_id = ac.id
+;
+
diff --git a/server/migrations/2020-02-07-210055_add_comment_subscribed/up.sql b/server/migrations/2020-02-07-210055_add_comment_subscribed/up.sql
new file mode 100644
index 000000000..8836a571a
--- /dev/null
+++ b/server/migrations/2020-02-07-210055_add_comment_subscribed/up.sql
@@ -0,0 +1,220 @@
+
+-- Adding community name, hot_rank, to comment_view, user_mention_view, and subscribed to comment_view
+
+-- Rebuild the comment view
+drop view reply_view;
+drop view user_mention_view;
+drop view user_mention_mview;
+drop view comment_view;
+drop view comment_mview;
+drop materialized view comment_aggregates_mview;
+drop view comment_aggregates_view;
+
+-- reply and comment view
+create view comment_aggregates_view as
+select
+c.*,
+(select community_id from post p where p.id = c.post_id),
+(select co.name from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_name,
+(select u.banned from user_ u where c.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
+(select name from user_ where c.creator_id = user_.id) as creator_name,
+(select avatar from user_ where c.creator_id = user_.id) as creator_avatar,
+coalesce(sum(cl.score), 0) as score,
+count (case when cl.score = 1 then 1 else null end) as upvotes,
+count (case when cl.score = -1 then 1 else null end) as downvotes,
+hot_rank(coalesce(sum(cl.score) , 0), c.published) as hot_rank
+from comment c
+left join comment_like cl on c.id = cl.comment_id
+group by c.id;
+
+create materialized view comment_aggregates_mview as select * from comment_aggregates_view;
+
+create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id);
+
+create view comment_view as
+with all_comment as
+(
+ select
+ ca.*
+ from comment_aggregates_view ca
+)
+
+select
+ac.*,
+u.id as user_id,
+coalesce(cl.score, 0) as my_vote,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
+(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+
+union all
+
+select
+ ac.*,
+ null as user_id,
+ null as my_vote,
+ null as subscribed,
+ null as saved
+from all_comment ac
+;
+
+create view comment_mview as
+with all_comment as
+(
+ select
+ ca.*
+ from comment_aggregates_mview ca
+)
+
+select
+ac.*,
+u.id as user_id,
+coalesce(cl.score, 0) as my_vote,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
+(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+
+union all
+
+select
+ ac.*,
+ null as user_id,
+ null as my_vote,
+ null as subscribed,
+ null as saved
+from all_comment ac
+;
+
+-- Do the reply_view referencing the comment_mview
+create view reply_view as
+with closereply as (
+ select
+ c2.id,
+ c2.creator_id as sender_id,
+ c.creator_id as recipient_id
+ from comment c
+ inner join comment c2 on c.id = c2.parent_id
+ where c2.creator_id != c.creator_id
+ -- Do union where post is null
+ union
+ select
+ c.id,
+ c.creator_id as sender_id,
+ p.creator_id as recipient_id
+ from comment c, post p
+ where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
+)
+select cv.*,
+closereply.recipient_id
+from comment_mview cv, closereply
+where closereply.id = cv.id
+;
+
+-- user mention
+create view user_mention_view as
+select
+ c.id,
+ um.id as user_mention_id,
+ c.creator_id,
+ c.post_id,
+ c.parent_id,
+ c.content,
+ c.removed,
+ um.read,
+ c.published,
+ c.updated,
+ c.deleted,
+ c.community_id,
+ c.community_name,
+ c.banned,
+ c.banned_from_community,
+ c.creator_name,
+ c.creator_avatar,
+ c.score,
+ c.upvotes,
+ c.downvotes,
+ c.hot_rank,
+ c.user_id,
+ c.my_vote,
+ c.saved,
+ um.recipient_id
+from user_mention um, comment_view c
+where um.comment_id = c.id;
+
+
+create view user_mention_mview as
+with all_comment as
+(
+ select
+ ca.*
+ from comment_aggregates_mview ca
+)
+
+select
+ ac.id,
+ um.id as user_mention_id,
+ ac.creator_id,
+ ac.post_id,
+ ac.parent_id,
+ ac.content,
+ ac.removed,
+ um.read,
+ ac.published,
+ ac.updated,
+ ac.deleted,
+ ac.community_id,
+ ac.community_name,
+ ac.banned,
+ ac.banned_from_community,
+ ac.creator_name,
+ ac.creator_avatar,
+ ac.score,
+ ac.upvotes,
+ ac.downvotes,
+ ac.hot_rank,
+ u.id as user_id,
+ coalesce(cl.score, 0) as my_vote,
+ (select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
+ um.recipient_id
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+left join user_mention um on um.comment_id = ac.id
+
+union all
+
+select
+ ac.id,
+ um.id as user_mention_id,
+ ac.creator_id,
+ ac.post_id,
+ ac.parent_id,
+ ac.content,
+ ac.removed,
+ um.read,
+ ac.published,
+ ac.updated,
+ ac.deleted,
+ ac.community_id,
+ ac.community_name,
+ ac.banned,
+ ac.banned_from_community,
+ ac.creator_name,
+ ac.creator_avatar,
+ ac.score,
+ ac.upvotes,
+ ac.downvotes,
+ ac.hot_rank,
+ null as user_id,
+ null as my_vote,
+ null as saved,
+ um.recipient_id
+from all_comment ac
+left join user_mention um on um.comment_id = ac.id
+;
+
diff --git a/server/migrations/2020-02-08-145624_add_post_newest_activity_time/down.sql b/server/migrations/2020-02-08-145624_add_post_newest_activity_time/down.sql
new file mode 100644
index 000000000..8b912fa36
--- /dev/null
+++ b/server/migrations/2020-02-08-145624_add_post_newest_activity_time/down.sql
@@ -0,0 +1,88 @@
+drop view post_view;
+drop view post_mview;
+drop materialized view post_aggregates_mview;
+drop view post_aggregates_view;
+
+-- regen post view
+create view post_aggregates_view as
+select
+p.*,
+(select u.banned from user_ u where p.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
+(select name from user_ where p.creator_id = user_.id) as creator_name,
+(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
+(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;
+
+create materialized view post_aggregates_mview as select * from post_aggregates_view;
+
+create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
+
+create view post_view as
+with all_post as (
+ select
+ pa.*
+ from post_aggregates_view pa
+)
+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
+;
+
+create view post_mview as
+with all_post as (
+ select
+ pa.*
+ from post_aggregates_mview pa
+)
+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/2020-02-08-145624_add_post_newest_activity_time/up.sql b/server/migrations/2020-02-08-145624_add_post_newest_activity_time/up.sql
new file mode 100644
index 000000000..e15412771
--- /dev/null
+++ b/server/migrations/2020-02-08-145624_add_post_newest_activity_time/up.sql
@@ -0,0 +1,106 @@
+-- Adds a newest_activity_time for the post_views, in order to sort by newest comment
+drop view post_view;
+drop view post_mview;
+drop materialized view post_aggregates_mview;
+drop view post_aggregates_view;
+
+-- regen post view
+create view post_aggregates_view as
+select
+p.*,
+(select u.banned from user_ u where p.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
+(select name from user_ where p.creator_id = user_.id) as creator_name,
+(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
+(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),
+ (
+ case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
+ else greatest(c.recent_comment_time, p.published)
+ end
+ )
+) as hot_rank,
+(
+ case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
+ else greatest(c.recent_comment_time, p.published)
+ end
+) as newest_activity_time
+from post p
+left join post_like pl on p.id = pl.post_id
+left join (
+ select post_id,
+ max(published) as recent_comment_time
+ from comment
+ group by 1
+) c on p.id = c.post_id
+group by p.id, c.recent_comment_time;
+
+create materialized view post_aggregates_mview as select * from post_aggregates_view;
+
+create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
+
+create view post_view as
+with all_post as (
+ select
+ pa.*
+ from post_aggregates_view pa
+)
+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
+;
+
+create view post_mview as
+with all_post as (
+ select
+ pa.*
+ from post_aggregates_mview pa
+)
+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/src/api/comment.rs b/server/src/api/comment.rs
index 775085e93..5c6149666 100644
--- a/server/src/api/comment.rs
+++ b/server/src/api/comment.rs
@@ -2,6 +2,7 @@ use super::*;
use crate::send_email;
use crate::settings::Settings;
use diesel::PgConnection;
+use std::str::FromStr;
#[derive(Serialize, Deserialize)]
pub struct CreateComment {
@@ -47,6 +48,21 @@ pub struct CreateCommentLike {
auth: String,
}
+#[derive(Serialize, Deserialize)]
+pub struct GetComments {
+ type_: String,
+ sort: String,
+ page: Option,
+ limit: Option,
+ pub community_id: Option,
+ auth: Option,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetCommentsResponse {
+ comments: Vec,
+}
+
impl Perform for Oper {
fn perform(&self, conn: &PgConnection) -> Result {
let data: &CreateComment = &self.data;
@@ -456,3 +472,40 @@ impl Perform for Oper {
})
}
}
+
+impl Perform for Oper {
+ fn perform(&self, conn: &PgConnection) -> Result {
+ let data: &GetComments = &self.data;
+
+ let user_claims: Option = match &data.auth {
+ Some(auth) => match Claims::decode(&auth) {
+ Ok(claims) => Some(claims.claims),
+ Err(_e) => None,
+ },
+ None => None,
+ };
+
+ let user_id = match &user_claims {
+ Some(claims) => Some(claims.id),
+ None => None,
+ };
+
+ let type_ = ListingType::from_str(&data.type_)?;
+ let sort = SortType::from_str(&data.sort)?;
+
+ let comments = match CommentQueryBuilder::create(&conn)
+ .listing_type(type_)
+ .sort(&sort)
+ .for_community_id(data.community_id)
+ .my_user_id(user_id)
+ .page(data.page)
+ .limit(data.limit)
+ .list()
+ {
+ Ok(comments) => comments,
+ Err(_e) => return Err(APIError::err("couldnt_get_comments").into()),
+ };
+
+ Ok(GetCommentsResponse { comments })
+ }
+}
diff --git a/server/src/api/post.rs b/server/src/api/post.rs
index bd276be5a..00bf8e114 100644
--- a/server/src/api/post.rs
+++ b/server/src/api/post.rs
@@ -126,7 +126,15 @@ impl Perform for Oper {
let inserted_post = match Post::create(&conn, &post_form) {
Ok(post) => post,
- Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
+ Err(e) => {
+ let err_type = if e.to_string() == "value too long for type character varying(200)" {
+ "post_title_too_long"
+ } else {
+ "couldnt_create_post"
+ };
+
+ return Err(APIError::err(err_type).into());
+ }
};
// They like their own post by default
@@ -361,7 +369,15 @@ impl Perform for Oper {
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
Ok(post) => post,
- Err(_e) => return Err(APIError::err("couldnt_update_post").into()),
+ Err(e) => {
+ let err_type = if e.to_string() == "value too long for type character varying(200)" {
+ "post_title_too_long"
+ } else {
+ "couldnt_update_post"
+ };
+
+ return Err(APIError::err(err_type).into());
+ }
};
// Mod tables
diff --git a/server/src/db/comment_view.rs b/server/src/db/comment_view.rs
index febf18b78..ff915d5e5 100644
--- a/server/src/db/comment_view.rs
+++ b/server/src/db/comment_view.rs
@@ -15,6 +15,7 @@ table! {
updated -> Nullable,
deleted -> Bool,
community_id -> Int4,
+ community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
@@ -22,8 +23,10 @@ table! {
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
+ hot_rank -> Int4,
user_id -> Nullable,
my_vote -> Nullable,
+ subscribed -> Nullable,
saved -> Nullable,
}
}
@@ -41,6 +44,7 @@ table! {
updated -> Nullable,
deleted -> Bool,
community_id -> Int4,
+ community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
@@ -48,8 +52,10 @@ table! {
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
+ hot_rank -> Int4,
user_id -> Nullable,
my_vote -> Nullable,
+ subscribed -> Nullable,
saved -> Nullable,
}
}
@@ -70,6 +76,7 @@ pub struct CommentView {
pub updated: Option,
pub deleted: bool,
pub community_id: i32,
+ pub community_name: String,
pub banned: bool,
pub banned_from_community: bool,
pub creator_name: String,
@@ -77,15 +84,19 @@ pub struct CommentView {
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
+ pub hot_rank: i32,
pub user_id: Option,
pub my_vote: Option,
+ pub subscribed: Option,
pub saved: Option,
}
pub struct CommentQueryBuilder<'a> {
conn: &'a PgConnection,
query: super::comment_view::comment_mview::BoxedQuery<'a, Pg>,
+ listing_type: ListingType,
sort: &'a SortType,
+ for_community_id: Option,
for_post_id: Option,
for_creator_id: Option,
search_term: Option,
@@ -104,7 +115,9 @@ impl<'a> CommentQueryBuilder<'a> {
CommentQueryBuilder {
conn,
query,
+ listing_type: ListingType::All,
sort: &SortType::New,
+ for_community_id: None,
for_post_id: None,
for_creator_id: None,
search_term: None,
@@ -115,6 +128,11 @@ impl<'a> CommentQueryBuilder<'a> {
}
}
+ pub fn listing_type(mut self, listing_type: ListingType) -> Self {
+ self.listing_type = listing_type;
+ self
+ }
+
pub fn sort(mut self, sort: &'a SortType) -> Self {
self.sort = sort;
self
@@ -130,6 +148,11 @@ impl<'a> CommentQueryBuilder<'a> {
self
}
+ pub fn for_community_id>(mut self, for_community_id: T) -> Self {
+ self.for_community_id = for_community_id.get_optional();
+ self
+ }
+
pub fn search_term>(mut self, search_term: T) -> Self {
self.search_term = search_term.get_optional();
self
@@ -171,6 +194,10 @@ impl<'a> CommentQueryBuilder<'a> {
query = query.filter(creator_id.eq(for_creator_id));
};
+ if let Some(for_community_id) = self.for_community_id {
+ query = query.filter(community_id.eq(for_community_id));
+ }
+
if let Some(for_post_id) = self.for_post_id {
query = query.filter(post_id.eq(for_post_id));
};
@@ -179,12 +206,18 @@ impl<'a> CommentQueryBuilder<'a> {
query = query.filter(content.ilike(fuzzy_search(&search_term)));
};
+ if let ListingType::Subscribed = self.listing_type {
+ query = query.filter(subscribed.eq(true));
+ }
+
if self.saved_only {
query = query.filter(saved.eq(true));
}
query = match self.sort {
- // SortType::Hot => query.order(hot_rank.desc(), published.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
@@ -199,7 +232,7 @@ impl<'a> CommentQueryBuilder<'a> {
SortType::TopDay => query
.filter(published.gt(now - 1.days()))
.order_by(score.desc()),
- _ => query.order_by(published.desc()),
+ // _ => query.order_by(published.desc()),
};
let (limit, offset) = limit_and_offset(self.page, self.limit);
@@ -218,9 +251,8 @@ impl CommentView {
from_comment_id: i32,
my_user_id: Option,
) -> Result {
- use super::comment_view::comment_view::dsl::*;
-
- let mut query = comment_view.into_boxed();
+ use super::comment_view::comment_mview::dsl::*;
+ let mut query = comment_mview.into_boxed();
// The view lets you pass a null user_id, if you're not logged in
if let Some(my_user_id) = my_user_id {
@@ -251,6 +283,7 @@ table! {
updated -> Nullable,
deleted -> Bool,
community_id -> Int4,
+ community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
@@ -258,8 +291,10 @@ table! {
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
+ hot_rank -> Int4,
user_id -> Nullable,
my_vote -> Nullable,
+ subscribed -> Nullable,
saved -> Nullable,
recipient_id -> Int4,
}
@@ -281,6 +316,7 @@ pub struct ReplyView {
pub updated: Option,
pub deleted: bool,
pub community_id: i32,
+ pub community_name: String,
pub banned: bool,
pub banned_from_community: bool,
pub creator_name: String,
@@ -288,8 +324,10 @@ pub struct ReplyView {
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
+ pub hot_rank: i32,
pub user_id: Option,
pub my_vote: Option,
+ pub subscribed: Option,
pub saved: Option,
pub recipient_id: i32,
}
@@ -474,6 +512,7 @@ mod tests {
creator_id: inserted_user.id,
post_id: inserted_post.id,
community_id: inserted_community.id,
+ community_name: inserted_community.name.to_owned(),
parent_id: None,
removed: false,
deleted: false,
@@ -486,9 +525,11 @@ mod tests {
creator_avatar: None,
score: 1,
downvotes: 0,
+ hot_rank: 0,
upvotes: 1,
user_id: None,
my_vote: None,
+ subscribed: None,
saved: None,
};
@@ -498,6 +539,7 @@ mod tests {
creator_id: inserted_user.id,
post_id: inserted_post.id,
community_id: inserted_community.id,
+ community_name: inserted_community.name.to_owned(),
parent_id: None,
removed: false,
deleted: false,
@@ -510,21 +552,26 @@ mod tests {
creator_avatar: None,
score: 1,
downvotes: 0,
+ hot_rank: 0,
upvotes: 1,
user_id: Some(inserted_user.id),
my_vote: Some(1),
+ subscribed: None,
saved: None,
};
- let read_comment_views_no_user = CommentQueryBuilder::create(&conn)
+ let mut read_comment_views_no_user = CommentQueryBuilder::create(&conn)
.for_post_id(inserted_post.id)
.list()
.unwrap();
- let read_comment_views_with_user = CommentQueryBuilder::create(&conn)
+ read_comment_views_no_user[0].hot_rank = 0;
+
+ let mut read_comment_views_with_user = CommentQueryBuilder::create(&conn)
.for_post_id(inserted_post.id)
.my_user_id(inserted_user.id)
.list()
.unwrap();
+ read_comment_views_with_user[0].hot_rank = 0;
let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
diff --git a/server/src/db/community_view.rs b/server/src/db/community_view.rs
index 95e00c65c..18ff67a8a 100644
--- a/server/src/db/community_view.rs
+++ b/server/src/db/community_view.rs
@@ -227,9 +227,9 @@ impl CommunityView {
from_community_id: i32,
from_user_id: Option,
) -> Result {
- use super::community_view::community_view::dsl::*;
+ use super::community_view::community_mview::dsl::*;
- let mut query = community_view.into_boxed();
+ let mut query = community_mview.into_boxed();
query = query.filter(id.eq(from_community_id));
diff --git a/server/src/db/post_view.rs b/server/src/db/post_view.rs
index c80d16967..3f385077f 100644
--- a/server/src/db/post_view.rs
+++ b/server/src/db/post_view.rs
@@ -31,6 +31,7 @@ table! {
upvotes -> BigInt,
downvotes -> BigInt,
hot_rank -> Int4,
+ newest_activity_time -> Timestamp,
user_id -> Nullable,
my_vote -> Nullable,
subscribed -> Nullable,
@@ -70,6 +71,7 @@ pub struct PostView {
pub upvotes: i64,
pub downvotes: i64,
pub hot_rank: i32,
+ pub newest_activity_time: chrono::NaiveDateTime,
pub user_id: Option,
pub my_vote: Option,
pub subscribed: Option,
@@ -106,6 +108,7 @@ table! {
upvotes -> BigInt,
downvotes -> BigInt,
hot_rank -> Int4,
+ newest_activity_time -> Timestamp,
user_id -> Nullable,
my_vote -> Nullable,
subscribed -> Nullable,
@@ -121,6 +124,9 @@ pub struct PostQueryBuilder<'a> {
sort: &'a SortType,
my_user_id: Option,
for_creator_id: Option,
+ for_community_id: Option,
+ search_term: Option,
+ url_search: Option,
show_nsfw: bool,
saved_only: bool,
unread_only: bool,
@@ -137,10 +143,13 @@ impl<'a> PostQueryBuilder<'a> {
PostQueryBuilder {
conn,
query,
- my_user_id: None,
- for_creator_id: None,
listing_type: ListingType::All,
sort: &SortType::Hot,
+ my_user_id: None,
+ for_creator_id: None,
+ for_community_id: None,
+ search_term: None,
+ url_search: None,
show_nsfw: true,
saved_only: false,
unread_only: false,
@@ -160,34 +169,22 @@ impl<'a> PostQueryBuilder<'a> {
}
pub fn for_community_id>(mut self, for_community_id: T) -> Self {
- use super::post_view::post_mview::dsl::*;
- if let Some(for_community_id) = for_community_id.get_optional() {
- self.query = self.query.filter(community_id.eq(for_community_id));
- self.query = self.query.then_order_by(stickied.desc());
- }
+ self.for_community_id = for_community_id.get_optional();
self
}
pub fn for_creator_id>(mut self, for_creator_id: T) -> Self {
- if let Some(for_creator_id) = for_creator_id.get_optional() {
- self.for_creator_id = Some(for_creator_id);
- }
+ self.for_creator_id = for_creator_id.get_optional();
self
}
pub fn search_term>(mut self, search_term: T) -> Self {
- use super::post_view::post_mview::dsl::*;
- if let Some(search_term) = search_term.get_optional() {
- self.query = self.query.filter(name.ilike(fuzzy_search(&search_term)));
- }
+ self.search_term = search_term.get_optional();
self
}
pub fn url_search>(mut self, url_search: T) -> Self {
- use super::post_view::post_mview::dsl::*;
- if let Some(url_search) = url_search.get_optional() {
- self.query = self.query.filter(url.eq(url_search));
- }
+ self.url_search = url_search.get_optional();
self
}
@@ -230,6 +227,22 @@ impl<'a> PostQueryBuilder<'a> {
query = query.filter(subscribed.eq(true));
}
+ if let Some(for_community_id) = self.for_community_id {
+ query = query.filter(community_id.eq(for_community_id));
+ query = query.then_order_by(stickied.desc());
+ }
+
+ if let Some(url_search) = self.url_search {
+ query = query.filter(url.eq(url_search));
+ }
+
+ if let Some(search_term) = self.search_term {
+ let searcher = fuzzy_search(&search_term);
+ query = query
+ .filter(name.ilike(searcher.to_owned()))
+ .or_filter(body.ilike(searcher));
+ }
+
query = match self.sort {
SortType::Hot => query
.then_order_by(hot_rank.desc())
@@ -302,10 +315,10 @@ impl PostView {
from_post_id: i32,
my_user_id: Option,
) -> Result {
- use super::post_view::post_view::dsl::*;
+ use super::post_view::post_mview::dsl::*;
use diesel::prelude::*;
- let mut query = post_view.into_boxed();
+ let mut query = post_mview.into_boxed();
query = query.filter(id.eq(from_post_id));
@@ -435,6 +448,7 @@ mod tests {
downvotes: 0,
hot_rank: 1728,
published: inserted_post.published,
+ newest_activity_time: inserted_post.published,
updated: None,
subscribed: None,
read: None,
@@ -469,6 +483,7 @@ mod tests {
downvotes: 0,
hot_rank: 1728,
published: inserted_post.published,
+ newest_activity_time: inserted_post.published,
updated: None,
subscribed: None,
read: None,
diff --git a/server/src/db/user_mention_view.rs b/server/src/db/user_mention_view.rs
index 1cf43984a..8046747e6 100644
--- a/server/src/db/user_mention_view.rs
+++ b/server/src/db/user_mention_view.rs
@@ -16,6 +16,7 @@ table! {
updated -> Nullable,
deleted -> Bool,
community_id -> Int4,
+ community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
@@ -23,6 +24,7 @@ table! {
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
+ hot_rank -> Int4,
user_id -> Nullable,
my_vote -> Nullable,
saved -> Nullable,
@@ -44,6 +46,7 @@ table! {
updated -> Nullable,
deleted -> Bool,
community_id -> Int4,
+ community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
@@ -51,6 +54,7 @@ table! {
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
+ hot_rank -> Int4,
user_id -> Nullable,
my_vote -> Nullable,
saved -> Nullable,
@@ -75,6 +79,7 @@ pub struct UserMentionView {
pub updated: Option,
pub deleted: bool,
pub community_id: i32,
+ pub community_name: String,
pub banned: bool,
pub banned_from_community: bool,
pub creator_name: String,
@@ -82,6 +87,7 @@ pub struct UserMentionView {
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
+ pub hot_rank: i32,
pub user_id: Option,
pub my_vote: Option,
pub saved: Option,
@@ -149,7 +155,9 @@ impl<'a> UserMentionQueryBuilder<'a> {
.filter(recipient_id.eq(self.for_user_id));
query = match self.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
@@ -164,7 +172,7 @@ impl<'a> UserMentionQueryBuilder<'a> {
SortType::TopDay => query
.filter(published.gt(now - 1.days()))
.order_by(score.desc()),
- _ => query.order_by(published.desc()),
+ // _ => query.order_by(published.desc()),
};
let (limit, offset) = limit_and_offset(self.page, self.limit);
diff --git a/server/src/db/user_view.rs b/server/src/db/user_view.rs
index 3ea506e7f..2274ecbdf 100644
--- a/server/src/db/user_view.rs
+++ b/server/src/db/user_view.rs
@@ -144,9 +144,8 @@ impl<'a> UserQueryBuilder<'a> {
impl UserView {
pub fn read(conn: &PgConnection, from_user_id: i32) -> Result {
- use super::user_view::user_view::dsl::*;
-
- user_view.find(from_user_id).first::(conn)
+ use super::user_view::user_mview::dsl::*;
+ user_mview.find(from_user_id).first::(conn)
}
pub fn admins(conn: &PgConnection) -> Result, Error> {
diff --git a/server/src/routes/index.rs b/server/src/routes/index.rs
index b044833ef..c1c363c98 100644
--- a/server/src/routes/index.rs
+++ b/server/src/routes/index.rs
@@ -6,7 +6,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
cfg
.route("/", web::get().to(index))
.route(
- "/home/type/{type}/sort/{sort}/page/{page}",
+ "/home/data_type/{data_type}/listing_type/{listing_type}/sort/{sort}/page/{page}",
web::get().to(index),
)
.route("/login", web::get().to(index))
@@ -17,7 +17,10 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route("/communities", web::get().to(index))
.route("/post/{id}/comment/{id2}", web::get().to(index))
.route("/post/{id}", web::get().to(index))
- .route("/c/{name}/sort/{sort}/page/{page}", web::get().to(index))
+ .route(
+ "/c/{name}/data_type/{data_type}/sort/{sort}/page/{page}",
+ web::get().to(index),
+ )
.route("/c/{name}", web::get().to(index))
.route("/community/{id}", web::get().to(index))
.route(
diff --git a/server/src/version.rs b/server/src/version.rs
index 28373cf57..e504bd466 100644
--- a/server/src/version.rs
+++ b/server/src/version.rs
@@ -1 +1 @@
-pub const VERSION: &str = "v0.6.11";
+pub const VERSION: &str = "v0.6.25";
diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs
index c9a41a1fc..a1feede25 100644
--- a/server/src/websocket/mod.rs
+++ b/server/src/websocket/mod.rs
@@ -45,4 +45,5 @@ pub enum UserOperation {
EditPrivateMessage,
GetPrivateMessages,
UserJoin,
+ GetComments,
}
diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs
index 0c6062845..76a55540f 100644
--- a/server/src/websocket/server.rs
+++ b/server/src/websocket/server.rs
@@ -12,6 +12,7 @@ use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::time::SystemTime;
+use strum::IntoEnumIterator;
use crate::api::comment::*;
use crate::api::community::*;
@@ -72,6 +73,13 @@ pub struct SessionInfo {
pub ip: IPAddr,
}
+#[derive(Eq, PartialEq, Hash, Debug, EnumIter, Copy, Clone)]
+pub enum RateLimitType {
+ Message,
+ Register,
+ Post,
+}
+
/// `ChatServer` manages chat rooms and responsible for coordinating chat
/// session.
pub struct ChatServer {
@@ -88,8 +96,8 @@ pub struct ChatServer {
/// sessions (IE clients)
user_rooms: HashMap>,
- /// Rate limiting based on IP addr
- rate_limits: HashMap,
+ /// Rate limiting based on rate type and IP addr
+ rate_limit_buckets: HashMap>,
rng: ThreadRng,
db: Pool>,
@@ -99,7 +107,7 @@ impl ChatServer {
pub fn startup(db: Pool>) -> ChatServer {
ChatServer {
sessions: HashMap::new(),
- rate_limits: HashMap::new(),
+ rate_limit_buckets: HashMap::new(),
post_rooms: HashMap::new(),
community_rooms: HashMap::new(),
user_rooms: HashMap::new(),
@@ -114,6 +122,12 @@ impl ChatServer {
sessions.remove(&id);
}
+ // Also leave all post rooms
+ // This avoids double messages
+ for sessions in self.post_rooms.values_mut() {
+ sessions.remove(&id);
+ }
+
// If the room doesn't exist yet
if self.community_rooms.get_mut(&community_id).is_none() {
self.community_rooms.insert(community_id, HashSet::new());
@@ -132,6 +146,12 @@ impl ChatServer {
sessions.remove(&id);
}
+ // Also leave all communities
+ // This avoids double messages
+ for sessions in self.community_rooms.values_mut() {
+ sessions.remove(&id);
+ }
+
// If the room doesn't exist yet
if self.post_rooms.get_mut(&post_id).is_none() {
self.post_rooms.insert(post_id, HashSet::new());
@@ -236,6 +256,10 @@ impl ChatServer {
self.send_user_room_message(recipient_id, &comment_reply_sent_str, id);
}
+ // Send it to the community too
+ self.send_community_room_message(0, &comment_post_sent_str, id);
+ self.send_community_room_message(comment.comment.community_id, &comment_post_sent_str, id);
+
Ok(comment_user_sent_str)
}
@@ -257,63 +281,86 @@ impl ChatServer {
self.send_community_room_message(0, &post_sent_str, id);
self.send_community_room_message(community_id, &post_sent_str, id);
+ // Send it to the post room
+ self.send_post_room_message(post_sent.post.id, &post_sent_str, id);
+
to_json_string(&user_operation, post)
}
- fn check_rate_limit_register(&mut self, id: usize) -> Result<(), Error> {
+ fn check_rate_limit_register(&mut self, id: usize, check_only: bool) -> Result<(), Error> {
self.check_rate_limit_full(
+ RateLimitType::Register,
id,
Settings::get().rate_limit.register,
Settings::get().rate_limit.register_per_second,
+ check_only,
)
}
- fn check_rate_limit_post(&mut self, id: usize) -> Result<(), Error> {
+ fn check_rate_limit_post(&mut self, id: usize, check_only: bool) -> Result<(), Error> {
self.check_rate_limit_full(
+ RateLimitType::Post,
id,
Settings::get().rate_limit.post,
Settings::get().rate_limit.post_per_second,
+ check_only,
)
}
- fn check_rate_limit_message(&mut self, id: usize) -> Result<(), Error> {
+ fn check_rate_limit_message(&mut self, id: usize, check_only: bool) -> Result<(), Error> {
self.check_rate_limit_full(
+ RateLimitType::Message,
id,
Settings::get().rate_limit.message,
Settings::get().rate_limit.message_per_second,
+ check_only,
)
}
#[allow(clippy::float_cmp)]
- fn check_rate_limit_full(&mut self, id: usize, rate: i32, per: i32) -> Result<(), Error> {
+ fn check_rate_limit_full(
+ &mut self,
+ type_: RateLimitType,
+ id: usize,
+ rate: i32,
+ per: i32,
+ check_only: bool,
+ ) -> Result<(), Error> {
if let Some(info) = self.sessions.get(&id) {
- if let Some(rate_limit) = self.rate_limits.get_mut(&info.ip) {
- // The initial value
- if rate_limit.allowance == -2f64 {
- rate_limit.allowance = rate as f64;
- };
+ if let Some(bucket) = self.rate_limit_buckets.get_mut(&type_) {
+ if let Some(rate_limit) = bucket.get_mut(&info.ip) {
+ let current = SystemTime::now();
+ let time_passed = current.duration_since(rate_limit.last_checked)?.as_secs() as f64;
- let current = SystemTime::now();
- let time_passed = current.duration_since(rate_limit.last_checked)?.as_secs() as f64;
- rate_limit.last_checked = current;
- rate_limit.allowance += time_passed * (rate as f64 / per as f64);
- if rate_limit.allowance > rate as f64 {
- rate_limit.allowance = rate as f64;
- }
+ // The initial value
+ if rate_limit.allowance == -2f64 {
+ rate_limit.allowance = rate as f64;
+ };
- if rate_limit.allowance < 1.0 {
- println!(
- "Rate limited IP: {}, time_passed: {}, allowance: {}",
- &info.ip, time_passed, rate_limit.allowance
- );
- Err(
- APIError {
- message: format!("Too many requests. {} per {} seconds", rate, per),
+ rate_limit.last_checked = current;
+ rate_limit.allowance += time_passed * (rate as f64 / per as f64);
+ if !check_only && rate_limit.allowance > rate as f64 {
+ rate_limit.allowance = rate as f64;
+ }
+
+ if rate_limit.allowance < 1.0 {
+ println!(
+ "Rate limited IP: {}, time_passed: {}, allowance: {}",
+ &info.ip, time_passed, rate_limit.allowance
+ );
+ Err(
+ APIError {
+ message: format!("Too many requests. {} per {} seconds", rate, per),
+ }
+ .into(),
+ )
+ } else {
+ if !check_only {
+ rate_limit.allowance -= 1.0;
}
- .into(),
- )
+ Ok(())
+ }
} else {
- rate_limit.allowance -= 1.0;
Ok(())
}
} else {
@@ -351,14 +398,24 @@ impl Handler for ChatServer {
},
);
- if self.rate_limits.get(&msg.ip).is_none() {
- self.rate_limits.insert(
- msg.ip,
- RateLimitBucket {
- last_checked: SystemTime::now(),
- allowance: -2f64,
- },
- );
+ for rate_limit_type in RateLimitType::iter() {
+ if self.rate_limit_buckets.get(&rate_limit_type).is_none() {
+ self
+ .rate_limit_buckets
+ .insert(rate_limit_type, HashMap::new());
+ }
+
+ if let Some(bucket) = self.rate_limit_buckets.get_mut(&rate_limit_type) {
+ if bucket.get(&msg.ip).is_none() {
+ bucket.insert(
+ msg.ip.to_owned(),
+ RateLimitBucket {
+ last_checked: SystemTime::now(),
+ allowance: -2f64,
+ },
+ );
+ }
+ }
}
id
@@ -447,11 +504,18 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result do_user_operation::(user_operation, data, &conn),
UserOperation::Register => {
- chat.check_rate_limit_register(msg.id)?;
- do_user_operation::(user_operation, data, &conn)
+ chat.check_rate_limit_register(msg.id, true)?;
+ let register: Register = serde_json::from_str(data)?;
+ let res = Oper::new(register).perform(&conn)?;
+ chat.check_rate_limit_register(msg.id, false)?;
+ to_json_string(&user_operation, &res)
}
UserOperation::GetUserDetails => {
do_user_operation::(user_operation, data, &conn)
@@ -527,8 +591,11 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result {
- chat.check_rate_limit_register(msg.id)?;
- do_user_operation::(user_operation, data, &conn)
+ chat.check_rate_limit_register(msg.id, true)?;
+ let create_community: CreateCommunity = serde_json::from_str(data)?;
+ let res = Oper::new(create_community).perform(&conn)?;
+ chat.check_rate_limit_register(msg.id, false)?;
+ to_json_string(&user_operation, &res)
}
UserOperation::EditCommunity => {
let edit_community: EditCommunity = serde_json::from_str(data)?;
@@ -589,15 +656,24 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result {
+ let get_comments: GetComments = serde_json::from_str(data)?;
+ if get_comments.community_id.is_none() {
+ // 0 is the "all" community
+ chat.join_community_room(0, msg.id);
+ }
+ let res = Oper::new(get_comments).perform(&conn)?;
+ to_json_string(&user_operation, &res)
+ }
UserOperation::CreatePost => {
- chat.check_rate_limit_post(msg.id)?;
+ chat.check_rate_limit_post(msg.id, true)?;
let create_post: CreatePost = serde_json::from_str(data)?;
let res = Oper::new(create_post).perform(&conn)?;
+ chat.check_rate_limit_post(msg.id, false)?;
chat.post_sends(UserOperation::CreatePost, res, msg.id)
}
UserOperation::CreatePostLike => {
- chat.check_rate_limit_message(msg.id)?;
let create_post_like: CreatePostLike = serde_json::from_str(data)?;
let res = Oper::new(create_post_like).perform(&conn)?;
@@ -613,7 +689,6 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result(user_operation, data, &conn)
}
UserOperation::CreateComment => {
- chat.check_rate_limit_message(msg.id)?;
let create_comment: CreateComment = serde_json::from_str(data)?;
let res = Oper::new(create_comment).perform(&conn)?;
@@ -629,7 +704,6 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result(user_operation, data, &conn)
}
UserOperation::CreateCommentLike => {
- chat.check_rate_limit_message(msg.id)?;
let create_comment_like: CreateCommentLike = serde_json::from_str(data)?;
let res = Oper::new(create_comment_like).perform(&conn)?;
@@ -673,7 +747,6 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result(user_operation, data, &conn)
}
UserOperation::CreatePrivateMessage => {
- chat.check_rate_limit_message(msg.id)?;
let create_private_message: CreatePrivateMessage = serde_json::from_str(data)?;
let recipient_id = create_private_message.recipient_id;
let res = Oper::new(create_private_message).perform(&conn)?;
diff --git a/ui/assets/css/main.css b/ui/assets/css/main.css
index 2703d550c..048f687ec 100644
--- a/ui/assets/css/main.css
+++ b/ui/assets/css/main.css
@@ -37,6 +37,7 @@
}
.md-div img {
+ max-height: 90vh;
max-width: 100%;
height: auto;
}
@@ -170,3 +171,40 @@ hr {
-o-filter: blur(10px);
-ms-filter: blur(10px);
}
+
+.img-expanded {
+ max-height: 90vh;
+}
+
+.vote-animate:active {
+ transform: scale(1.2);
+ -webkit-transform: scale(1.2);
+ -ms-transform: scale(1.2);
+}
+
+.selectr-selected, .selectr-options-container {
+ background-color: var(--secondary);
+ color: var(--white);
+ border: unset;
+}
+
+.link-overlay:hover {
+ transition: .1s;
+ opacity: 1;
+}
+
+.link-overlay {
+ transition: opacity .1s ease-in-out;
+ position: absolute;
+ opacity: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ padding: 10px;
+ background: rgba(0,0,0,.6);
+}
+
+.placeholder {
+ height: 50px;
+ width: 50px;
+}
diff --git a/ui/assets/css/selectr.min.css b/ui/assets/css/selectr.min.css
new file mode 100644
index 000000000..78bab83fc
--- /dev/null
+++ b/ui/assets/css/selectr.min.css
@@ -0,0 +1,7 @@
+/*!
+ * Selectr 2.4.13
+ * http://mobius.ovh/docs/selectr
+ *
+ * Released under the MIT license
+ */
+.selectr-container li,.selectr-option,.selectr-tag{list-style:none}.selectr-container{position:relative}.selectr-hidden{position:absolute;overflow:hidden;clip:rect(0,0,0,0);width:1px;height:1px;margin:-1px;padding:0;border:0}.selectr-visible{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;z-index:11}.selectr-desktop.multiple .selectr-visible{display:none}.selectr-desktop.multiple.native-open .selectr-visible{top:100%;min-height:200px!important;height:auto;opacity:1;display:block}.selectr-container.multiple.selectr-mobile .selectr-selected{z-index:0}.selectr-selected{position:relative;z-index:1;box-sizing:border-box;width:100%;padding:7px 28px 7px 14px;cursor:pointer;border:1px solid #999;border-radius:3px;}.selectr-selected::before{position:absolute;top:50%;right:10px;width:0;height:0;content:'';-o-transform:rotate(0) translate3d(0,-50%,0);-ms-transform:rotate(0) translate3d(0,-50%,0);-moz-transform:rotate(0) translate3d(0,-50%,0);-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0);border-width:4px 4px 0;border-style:solid;border-color:#6c7a86 transparent transparent}.selectr-container.native-open .selectr-selected::before,.selectr-container.open .selectr-selected::before{border-width:0 4px 4px;border-style:solid;border-color:transparent transparent #6c7a86}.selectr-label{display:none;overflow:hidden;width:100%;white-space:nowrap;text-overflow:ellipsis}.selectr-placeholder{color:#6c7a86}.selectr-tags{margin:0;padding:0;white-space:normal}.has-selected .selectr-tags{margin:0 0 -2px}.selectr-tag{position:relative;float:left;padding:2px 25px 2px 8px;margin:0 2px 2px 0;cursor:default;color:#fff;border:none;border-radius:10px;background:#acb7bf}.selectr-container.multiple.has-selected .selectr-selected{padding:5px 28px 5px 5px}.selectr-options-container{position:absolute;z-index:10000;top:calc(100% - 1px);left:0;display:none;box-sizing:border-box;width:100%;border-width:0 1px 1px;border-style:solid;border-color:transparent #999 #999;border-radius:0 0 3px 3px;}.selectr-container.open .selectr-options-container{display:block}.selectr-input-container{position:relative;display:none}.selectr-clear,.selectr-input-clear,.selectr-tag-remove{position:absolute;top:50%;right:22px;width:20px;height:20px;padding:0;cursor:pointer;-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);border:none;background-color:transparent;z-index:11}.selectr-clear,.selectr-input-clear{display:none}.selectr-container.has-selected .selectr-clear,.selectr-input-container.active,.selectr-input-container.active .selectr-clear,.selectr-input-container.active .selectr-input-clear{display:block}.selectr-selected .selectr-tag-remove{right:2px}.selectr-clear::after,.selectr-clear::before,.selectr-input-clear::after,.selectr-input-clear::before,.selectr-tag-remove::after,.selectr-tag-remove::before{position:absolute;top:5px;left:9px;width:2px;height:10px;content:' ';background-color:#6c7a86}.selectr-tag-remove::after,.selectr-tag-remove::before{top:4px;width:3px;height:12px;}.selectr-clear:before,.selectr-input-clear::before,.selectr-tag-remove::before{-o-transform:rotate(45deg);-ms-transform:rotate(45deg);-moz-transform:rotate(45deg);-webkit-transform:rotate(45deg);transform:rotate(45deg)}.selectr-clear:after,.selectr-input-clear::after,.selectr-tag-remove::after{-o-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.selectr-input{top:5px;left:5px;box-sizing:border-box;width:calc(100% - 30px);margin:10px 15px;padding:7px 30px 7px 9px;border:1px solid #999;border-radius:3px}.selectr-notice{display:none;box-sizing:border-box;width:100%;padding:8px 16px;border-top:1px solid #999;border-radius:0 0 3px 3px;}.input-tag,.taggable .selectr-label{width:auto}.selectr-container.notice .selectr-notice{display:block}.selectr-container.notice .selectr-selected{border-radius:3px 3px 0 0}.selectr-options{position:relative;top:calc(100% + 2px);display:none;overflow-x:auto;overflow-y:scroll;max-height:200px;margin:0;padding:0}.selectr-container.notice .selectr-options-container,.selectr-container.open .selectr-input-container,.selectr-container.open .selectr-options{display:block}.selectr-option{position:relative;display:block;padding:5px 20px;cursor:pointer;font-weight:400}.has-selected .selectr-placeholder,.selectr-empty,.selectr-option.excluded{display:none}.selectr-options.optgroups>.selectr-option{padding-left:25px}.selectr-optgroup{font-weight:700;padding:0}.selectr-optgroup--label{font-weight:700;margin-top:10px;padding:5px 15px}.selectr-match{text-decoration:underline}.selectr-option.selected{background-color:#ddd}.selectr-option.active{color:#fff;background-color:#5897fb}.selectr-option.disabled{opacity:.4}.selectr-container.open .selectr-selected{border-color:#999 #999 transparent;border-radius:3px 3px 0 0}.selectr-container.open .selectr-selected::after{-o-transform:rotate(180deg) translate3d(0,50%,0);-ms-transform:rotate(180deg) translate3d(0,50%,0);-moz-transform:rotate(180deg) translate3d(0,50%,0);-webkit-transform:rotate(180deg) translate3d(0,50%,0);transform:rotate(180deg) translate3d(0,50%,0)}.selectr-disabled{opacity:.6}.has-selected .selectr-label{display:block}.taggable .selectr-selected{padding:4px 28px 4px 4px}.taggable .selectr-selected::after{display:table;content:" ";clear:both}.taggable .selectr-tags{float:left;display:block}.taggable .selectr-placeholder{display:none}.input-tag{float:left;min-width:90px}.selectr-tag-input{border:none;padding:3px 10px;width:100%;font-family:inherit;font-weight:inherit;font-size:inherit}.selectr-input-container.loading::after{position:absolute;top:50%;right:20px;width:20px;height:20px;content:'';-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);-o-transform-origin:50% 0 0;-ms-transform-origin:50% 0 0;-moz-transform-origin:50% 0 0;-webkit-transform-origin:50% 0 0;transform-origin:50% 0 0;-moz-animation:.5s linear 0s normal forwards infinite running spin;-webkit-animation:.5s linear 0s normal forwards infinite running spin;animation:.5s linear 0s normal forwards infinite running spin;border-width:3px;border-style:solid;border-color:#aaa #ddd #ddd;border-radius:50%}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}@keyframes spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}.selectr-container.open.inverted .selectr-selected{border-color:transparent #999 #999;border-radius:0 0 3px 3px}.selectr-container.inverted .selectr-options-container{border-width:1px 1px 0;border-color:#999 #999 transparent;border-radius:3px 3px 0 0;top:auto;bottom:calc(100% - 1px)}.selectr-container ::-webkit-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::-moz-placeholder{color:#6c7a86;opacity:1}.selectr-container :-ms-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::placeholder{color:#6c7a86;opacity:1}
diff --git a/ui/package.json b/ui/package.json
index 31d91bb4c..79756bc69 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,13 +1,13 @@
{
"name": "lemmy",
- "description": "A simple UI for lemmy",
+ "description": "The official Lemmy UI",
"version": "1.0.0",
"author": "Dessalines",
- "license": "GPL-2.0-or-later",
+ "license": "AGPL-3.0-or-later",
"main": "index.js",
"scripts": {
"build": "node fuse prod",
- "lint": "eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
+ "lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
"start": "node fuse dev"
},
"keywords": [],
@@ -22,7 +22,7 @@
"bootswatch": "^4.3.1",
"classcat": "^1.1.3",
"dotenv": "^8.2.0",
- "emoji-short-name": "^0.1.0",
+ "emoji-short-name": "^1.0.0",
"husky": "^4.2.1",
"i18next": "^19.0.3",
"inferno": "^7.0.1",
@@ -33,9 +33,10 @@
"markdown-it": "^10.0.0",
"markdown-it-container": "^2.0.0",
"markdown-it-emoji": "^1.4.0",
+ "mobius1-selectr": "^2.4.13",
"moment": "^2.24.0",
"prettier": "^1.18.2",
- "reconnecting-websocket": "^4.3.0",
+ "reconnecting-websocket": "^4.4.0",
"rxjs": "^6.4.0",
"terser": "^4.6.3",
"toastify-js": "^1.6.2",
diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx
index 3296a5c8e..1d0b12cad 100644
--- a/ui/src/components/comment-node.tsx
+++ b/ui/src/components/comment-node.tsx
@@ -15,6 +15,8 @@ import {
TransferCommunityForm,
TransferSiteForm,
BanType,
+ CommentSortType,
+ SortType,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
@@ -46,8 +48,10 @@ interface CommentNodeState {
showConfirmAppointAsAdmin: boolean;
collapsed: boolean;
viewSource: boolean;
- upvoteLoading: boolean;
- downvoteLoading: boolean;
+ my_vote: number;
+ score: number;
+ upvotes: number;
+ downvotes: number;
}
interface CommentNodeProps {
@@ -58,7 +62,11 @@ interface CommentNodeProps {
markable?: boolean;
moderators: Array;
admins: Array;
+ // TODO is this necessary, can't I get it from the node itself?
postCreatorId?: number;
+ showCommunity?: boolean;
+ sort?: CommentSortType;
+ sortType?: SortType;
}
export class CommentNode extends Component {
@@ -77,8 +85,10 @@ export class CommentNode extends Component {
showConfirmTransferCommunity: false,
showConfirmAppointAsMod: false,
showConfirmAppointAsAdmin: false,
- upvoteLoading: this.props.node.comment.upvoteLoading,
- downvoteLoading: this.props.node.comment.downvoteLoading,
+ my_vote: this.props.node.comment.my_vote,
+ score: this.props.node.comment.score,
+ upvotes: this.props.node.comment.upvotes,
+ downvotes: this.props.node.comment.downvotes,
};
constructor(props: any, context: any) {
@@ -91,15 +101,11 @@ export class CommentNode extends Component {
}
componentWillReceiveProps(nextProps: CommentNodeProps) {
- if (
- nextProps.node.comment.upvoteLoading !== this.state.upvoteLoading ||
- nextProps.node.comment.downvoteLoading !== this.state.downvoteLoading
- ) {
- this.setState({
- upvoteLoading: false,
- downvoteLoading: false,
- });
- }
+ this.state.my_vote = nextProps.node.comment.my_vote;
+ this.state.upvotes = nextProps.node.comment.upvotes;
+ this.state.downvotes = nextProps.node.comment.downvotes;
+ this.state.score = nextProps.node.comment.score;
+ this.setState(this.state);
}
render() {
@@ -116,40 +122,26 @@ export class CommentNode extends Component {
.viewOnly && 'no-click'}`}
>
-