mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-12-22 19:01:32 +00:00
Merge branch 'master' into federation
This commit is contained in:
commit
f9443dfbd3
80 changed files with 3249 additions and 692 deletions
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
@ -1,3 +1,4 @@
|
||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
patreon: dessalines
|
patreon: dessalines
|
||||||
|
liberapay: Lemmy
|
||||||
|
|
162
README.md
vendored
162
README.md
vendored
|
@ -1,34 +1,35 @@
|
||||||
<p align="center">
|
|
||||||
<a href="https://dev.lemmy.ml/" rel="noopener">
|
|
||||||
<img width=200px height=200px src="ui/assets/favicon.svg"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3 align="center">Lemmy</h3>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
|
|
||||||
[![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev)
|
|
||||||
[![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
|
|
||||||
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
|
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
|
||||||
[![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/lemmy)
|
[![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/lemmy)
|
||||||
[![GitHub issues](https://img.shields.io/github/issues-raw/dessalines/lemmy.svg)](https://github.com/dessalines/lemmy/issues)
|
[![GitHub issues](https://img.shields.io/github/issues-raw/dessalines/lemmy.svg)](https://github.com/dessalines/lemmy/issues)
|
||||||
[![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/)
|
[![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/)
|
||||||
![GitHub commit activity](https://img.shields.io/github/commit-activity/m/dessalines/lemmy.svg)
|
|
||||||
![GitHub repo size](https://img.shields.io/github/repo-size/dessalines/lemmy.svg)
|
|
||||||
[![License](https://img.shields.io/github/license/dessalines/lemmy.svg)](LICENSE)
|
[![License](https://img.shields.io/github/license/dessalines/lemmy.svg)](LICENSE)
|
||||||
[![Patreon](https://img.shields.io/badge/-Support%20on%20Patreon-blueviolet.svg)](https://www.patreon.com/dessalines)
|
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
<p align="center">
|
||||||
|
<a href="https://dev.lemmy.ml/" rel="noopener">
|
||||||
|
<img width=200px height=200px src="ui/assets/favicon.svg"></a>
|
||||||
|
|
||||||
<p align="center">A link aggregator / reddit clone for the fediverse.
|
<h3 align="center"><a href="https://dev.lemmy.ml">Lemmy</a></h3>
|
||||||
<br>
|
<p align="center">
|
||||||
|
A link aggregator / reddit clone for the fediverse.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<a href="https://dev.lemmy.ml">View Site</a>
|
||||||
|
·
|
||||||
|
<a href="https://dev.lemmy.ml/docs/index.html">Documentation</a>
|
||||||
|
·
|
||||||
|
<a href="https://github.com/dessalines/lemmy/issues">Report Bug</a>
|
||||||
|
·
|
||||||
|
<a href="https://github.com/dessalines/lemmy/issues">Request Feature</a>
|
||||||
|
·
|
||||||
|
<a href="https://github.com/dessalines/lemmy/blob/master/RELEASES.md">Releases</a>
|
||||||
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[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.*
|
## About The Project
|
||||||
|
|
||||||
This is a **very early beta version**, and a lot of features are currently broken or in active development, such as federation.
|
|
||||||
|
|
||||||
Front Page|Post
|
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.
|
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)
|
### Why's it called Lemmy?
|
||||||
- [Releases / Changelog](/RELEASES.md)
|
|
||||||
- [Contributing](https://dev.lemmy.ml/docs/contributing.html)
|
|
||||||
|
|
||||||
## Repository Mirrors
|
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
||||||
|
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
||||||
|
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
||||||
|
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
||||||
|
|
||||||
- [GitHub](https://github.com/dessalines/lemmy)
|
### Built With
|
||||||
- [Gitea](https://yerbamate.dev/dessalines/lemmy)
|
|
||||||
- [GitLab](https://gitlab.com/dessalines/lemmy)
|
- [Rust](https://www.rust-lang.org)
|
||||||
|
- [Actix](https://actix.rs/)
|
||||||
|
- [Diesel](http://diesel.rs/)
|
||||||
|
- [Inferno](https://infernojs.org)
|
||||||
|
- [Typescript](https://www.typescriptlang.org/)
|
||||||
|
|
||||||
## Features
|
## 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 transfer site and communities to others.
|
||||||
- Can fully erase your data, replacing all posts and comments.
|
- Can fully erase your data, replacing all posts and comments.
|
||||||
- NSFW post / community support.
|
- NSFW post / community support.
|
||||||
|
- OEmbed support via Iframely.
|
||||||
- High performance.
|
- High performance.
|
||||||
- Server is written in rust.
|
- Server is written in rust.
|
||||||
- Front end is `~80kB` gzipped.
|
- Front end is `~80kB` gzipped.
|
||||||
- Supports arm64 / Raspberry Pi.
|
- Supports arm64 / Raspberry Pi.
|
||||||
|
|
||||||
## Why's it called Lemmy?
|
## Installation
|
||||||
|
|
||||||
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
- [Docker](https://dev.lemmy.ml/docs/administration_install_docker.html)
|
||||||
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
- [Ansible](https://dev.lemmy.ml/docs/administration_install_ansible.html)
|
||||||
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
- [Kubernetes](https://dev.lemmy.ml/docs/administration_install_kubernetes.html)
|
||||||
- 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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Support / Donate
|
## 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.
|
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).
|
- [Support on Patreon](https://www.patreon.com/dessalines).
|
||||||
- [List of Sponsors](https://dev.lemmy.ml/sponsors).
|
- [List of Sponsors](https://dev.lemmy.ml/sponsors).
|
||||||
- Soon to add either liberapay or opencollective.
|
|
||||||
|
|
||||||
### Crypto
|
### Crypto
|
||||||
|
|
||||||
|
@ -157,28 +116,35 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
|
||||||
- ethereum: `0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01`
|
- ethereum: `0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01`
|
||||||
- monero: `41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV`
|
- 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).
|
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`).
|
||||||
|
|
||||||
<!-- translations -->
|
<!-- translations -->
|
||||||
|
|
||||||
lang | done | missing
|
lang | done | missing
|
||||||
---- | ---- | -------
|
---- | ---- | -------
|
||||||
ca | 98% | cross_posted_to,old,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,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,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 | 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
|
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 | 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
|
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 | 100% | cross_posted_to
|
es | 99% | cross_posted_to,couldnt_get_comments,post_title_too_long
|
||||||
fi | 98% | cross_posted_to,old,time,action
|
fi | 97% | cross_posted_to,old,support_on_liberapay,couldnt_get_comments,post_title_too_long,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
|
fr | 100% |
|
||||||
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
|
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 | 99% | cross_posted_to,time,action
|
nl | 98% | cross_posted_to,couldnt_get_comments,post_title_too_long,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
|
pt-br | 99% | couldnt_get_comments,post_title_too_long
|
||||||
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
|
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
|
||||||
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
|
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
|
||||||
<!-- translationsstop -->
|
<!-- translationsstop -->
|
||||||
|
|
||||||
If you'd like to update this report, run:
|
If you'd like to update this report, run:
|
||||||
|
@ -188,6 +154,14 @@ cd ui
|
||||||
ts-node translation_report.ts
|
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
|
## Credits
|
||||||
|
|
||||||
Logo made by Andy Cuccaro (@andycuccaro) under the CC-BY-SA 4.0 license.
|
Logo made by Andy Cuccaro (@andycuccaro) under the CC-BY-SA 4.0 license.
|
||||||
|
|
2
ansible/VERSION
vendored
2
ansible/VERSION
vendored
|
@ -1 +1 @@
|
||||||
v0.6.11
|
v0.6.25
|
||||||
|
|
3
ansible/lemmy.yml
vendored
3
ansible/lemmy.yml
vendored
|
@ -35,6 +35,7 @@
|
||||||
with_items:
|
with_items:
|
||||||
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
|
- { 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: '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)
|
- 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'
|
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
|
||||||
|
@ -63,4 +64,4 @@
|
||||||
special_time=daily
|
special_time=daily
|
||||||
name=certbot-renew-lemmy
|
name=certbot-renew-lemmy
|
||||||
user=root
|
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'"
|
||||||
|
|
3
ansible/lemmy_dev.yml
vendored
3
ansible/lemmy_dev.yml
vendored
|
@ -37,6 +37,7 @@
|
||||||
with_items:
|
with_items:
|
||||||
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
|
- { 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: '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)
|
- 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'
|
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
|
||||||
|
@ -97,4 +98,4 @@
|
||||||
special_time=daily
|
special_time=daily
|
||||||
name=certbot-renew-lemmy
|
name=certbot-renew-lemmy
|
||||||
user=root
|
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'"
|
||||||
|
|
9
ansible/templates/docker-compose.yml
vendored
9
ansible/templates/docker-compose.yml
vendored
|
@ -30,6 +30,14 @@ services:
|
||||||
- lemmy_pictshare:/usr/share/nginx/html/data
|
- lemmy_pictshare:/usr/share/nginx/html/data
|
||||||
restart: always
|
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:
|
postfix:
|
||||||
image: mwader/postfix-relay
|
image: mwader/postfix-relay
|
||||||
environment:
|
environment:
|
||||||
|
@ -38,3 +46,4 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
lemmy_db:
|
lemmy_db:
|
||||||
lemmy_pictshare:
|
lemmy_pictshare:
|
||||||
|
lemmy_iframely:
|
||||||
|
|
7
ansible/templates/nginx.conf
vendored
7
ansible/templates/nginx.conf
vendored
|
@ -80,6 +80,13 @@ server {
|
||||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
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
|
# Anonymize IP addresses
|
||||||
|
|
8
docker/dev/docker-compose.yml
vendored
8
docker/dev/docker-compose.yml
vendored
|
@ -28,6 +28,14 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- lemmy_pictshare:/usr/share/nginx/html/data
|
- lemmy_pictshare:/usr/share/nginx/html/data
|
||||||
restart: always
|
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:
|
volumes:
|
||||||
lemmy_db:
|
lemmy_db:
|
||||||
lemmy_pictshare:
|
lemmy_pictshare:
|
||||||
|
lemmy_iframely:
|
||||||
|
|
283
docker/iframely.config.local.js
vendored
Normal file
283
docker/iframely.config.local.js
vendored
Normal file
|
@ -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;
|
||||||
|
})();
|
2
docker/lemmy.hjson
vendored
2
docker/lemmy.hjson
vendored
|
@ -29,7 +29,7 @@
|
||||||
# rate limits for various user actions, by user ip
|
# rate limits for various user actions, by user ip
|
||||||
rate_limit: {
|
rate_limit: {
|
||||||
# maximum number of messages created in interval
|
# maximum number of messages created in interval
|
||||||
message: 30
|
message: 180
|
||||||
# interval length for message limit
|
# interval length for message limit
|
||||||
message_per_second: 60
|
message_per_second: 60
|
||||||
# maximum number of posts created in interval
|
# maximum number of posts created in interval
|
||||||
|
|
10
docker/prod/docker-compose.yml
vendored
10
docker/prod/docker-compose.yml
vendored
|
@ -11,7 +11,7 @@ services:
|
||||||
- lemmy_db:/var/lib/postgresql/data
|
- lemmy_db:/var/lib/postgresql/data
|
||||||
restart: always
|
restart: always
|
||||||
lemmy:
|
lemmy:
|
||||||
image: dessalines/lemmy:v0.6.11
|
image: dessalines/lemmy:v0.6.25
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8536:8536"
|
- "127.0.0.1:8536:8536"
|
||||||
restart: always
|
restart: always
|
||||||
|
@ -26,6 +26,14 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- lemmy_pictshare:/usr/share/nginx/html/data
|
- lemmy_pictshare:/usr/share/nginx/html/data
|
||||||
restart: always
|
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:
|
volumes:
|
||||||
lemmy_db:
|
lemmy_db:
|
||||||
lemmy_pictshare:
|
lemmy_pictshare:
|
||||||
|
lemmy_iframely:
|
||||||
|
|
1
docs/src/SUMMARY.md
vendored
1
docs/src/SUMMARY.md
vendored
|
@ -4,6 +4,7 @@
|
||||||
- [Features](about_features.md)
|
- [Features](about_features.md)
|
||||||
- [Goals](about_goals.md)
|
- [Goals](about_goals.md)
|
||||||
- [Post and Comment Ranking](about_ranking.md)
|
- [Post and Comment Ranking](about_ranking.md)
|
||||||
|
- [Guide](about_guide.md)
|
||||||
- [Administration](administration.md)
|
- [Administration](administration.md)
|
||||||
- [Install with Docker](administration_install_docker.md)
|
- [Install with Docker](administration_install_docker.md)
|
||||||
- [Install with Ansible](administration_install_ansible.md)
|
- [Install with Ansible](administration_install_ansible.md)
|
||||||
|
|
16
docs/src/about.md
vendored
16
docs/src/about.md
vendored
|
@ -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).
|
[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.
|
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?
|
### Why's it called Lemmy?
|
||||||
|
|
||||||
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
- 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 [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
||||||
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
- 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/)
|
||||||
|
|
7
docs/src/about_features.md
vendored
7
docs/src/about_features.md
vendored
|
@ -1,20 +1,27 @@
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
- Open source, [AGPL License](/LICENSE).
|
- Open source, [AGPL License](/LICENSE).
|
||||||
- Self hostable, easy to deploy.
|
- Self hostable, easy to deploy.
|
||||||
- Comes with [Docker](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes).
|
- Comes with [Docker](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes).
|
||||||
- Clean, mobile-friendly interface.
|
- Clean, mobile-friendly interface.
|
||||||
|
- Only a minimum of a username and password is required to sign up!
|
||||||
|
- User avatar support.
|
||||||
- Live-updating Comment threads.
|
- Live-updating Comment threads.
|
||||||
- Full vote scores `(+/-)` like old reddit.
|
- Full vote scores `(+/-)` like old reddit.
|
||||||
- Themes, including light, dark, and solarized.
|
- Themes, including light, dark, and solarized.
|
||||||
- Emojis with autocomplete support. Start typing `:`
|
- Emojis with autocomplete support. Start typing `:`
|
||||||
- User tagging using `@`, Community tagging using `#`.
|
- 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, on comment replies and when you're tagged.
|
||||||
|
- Notifications can be sent via email.
|
||||||
- i18n / internationalization support.
|
- i18n / internationalization support.
|
||||||
- RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`.
|
- RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`.
|
||||||
- Cross-posting support.
|
- Cross-posting support.
|
||||||
- A *similar post search* when creating new posts. Great for question / answer communities.
|
- A *similar post search* when creating new posts. Great for question / answer communities.
|
||||||
- Moderation abilities.
|
- Moderation abilities.
|
||||||
- Public Moderation Logs.
|
- Public Moderation Logs.
|
||||||
|
- Can sticky posts to the top of communities.
|
||||||
- Both site admins, and community moderators, who can appoint other moderators.
|
- Both site admins, and community moderators, who can appoint other moderators.
|
||||||
- Can lock, remove, and restore posts and comments.
|
- Can lock, remove, and restore posts and comments.
|
||||||
- Can ban and unban users from communities and the site.
|
- Can ban and unban users from communities and the site.
|
||||||
|
|
28
docs/src/about_guide.md
vendored
Normal file
28
docs/src/about_guide.md
vendored
Normal file
|
@ -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 <br> ========= | <h4>Heading 1</h4>
|
||||||
|
\## Heading 2 | Heading 2 <br>--------- | <h5>Heading 2</h5>
|
||||||
|
\[Link\](http://a.com) | \[Link\]\[1\]<br>⋮ <br>\[1\]: http://b.org | [Link](https://commonmark.org/)
|
||||||
|
!\[Image\](http://url/a.png) | !\[Image\]\[1\]<br>⋮ <br>\[1\]: http://url/b.jpg | ![Markdown](https://commonmark.org/help/images/favicon.png)
|
||||||
|
\> Blockquote | | <blockquote>Blockquote</blockquote>
|
||||||
|
\* List <br>\* List <br>\* List | \- List <br>\- List <br>\- List <br> | * List <br>* List <br>* List <br>
|
||||||
|
1\. One <br>2\. Two <br>3\. Three | 1) One<br>2) Two<br>3) Three | 1. One<br>2. Two<br>3. Three
|
||||||
|
Horizontal Rule <br>\--- | Horizontal Rule<br>\*\*\* | Horizontal Rule <br><hr>
|
||||||
|
\`Inline code\` with backticks | |`Inline code` with backticks
|
||||||
|
\`\`\`<br>\# code block <br>print '3 backticks or'<br>print 'indent 4 spaces' <br>\`\`\` | ····\# code block<br>····print '3 backticks or'<br>····print 'indent 4 spaces' | \# code block <br>print '3 backticks or'<br>print 'indent 4 spaces'
|
||||||
|
::: spoiler hidden or nsfw stuff<br>*a bunch of spoilers here*<br>::: | | <details><summary> hidden or nsfw stuff </summary><p><em>a bunch of spoilers here</em></p></details>
|
||||||
|
|
||||||
|
[CommonMark Tutorial](https://commonmark.org/help/tutorial/)
|
||||||
|
|
4
docs/src/administration.md
vendored
4
docs/src/administration.md
vendored
|
@ -1 +1,3 @@
|
||||||
Information for Lemmy instance admins, and those who want to start an instance.
|
# Admin info
|
||||||
|
|
||||||
|
Information for Lemmy instance admins, and those who want to start an instance.
|
||||||
|
|
9
docs/src/administration_configuration.md
vendored
9
docs/src/administration_configuration.md
vendored
|
@ -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.
|
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
|
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`.
|
`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.
|
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
|
||||||
|
```
|
||||||
|
|
2
docs/src/administration_install_ansible.md
vendored
2
docs/src/administration_install_ansible.md
vendored
|
@ -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.
|
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:
|
Then run the following commands on your local computer:
|
||||||
|
|
9
docs/src/administration_install_docker.md
vendored
9
docs/src/administration_install_docker.md
vendored
|
@ -1,3 +1,5 @@
|
||||||
|
# Docker Installation
|
||||||
|
|
||||||
Make sure you have both docker and docker-compose(>=`1.24.0`) installed:
|
Make sure you have both docker and docker-compose(>=`1.24.0`) installed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -5,20 +7,21 @@ mkdir lemmy/
|
||||||
cd lemmy/
|
cd lemmy/
|
||||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
|
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
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
and go to http://localhost:8536.
|
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
|
```bash
|
||||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf
|
||||||
# Replace the {{ vars }}
|
# Replace the {{ vars }}
|
||||||
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
|
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
|
||||||
```
|
```
|
||||||
#### Updating
|
## Updating
|
||||||
|
|
||||||
To update to the newest version, run:
|
To update to the newest version, run:
|
||||||
|
|
||||||
|
|
|
@ -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/).
|
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.
|
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/).
|
To try it locally, you can use [MicroK8s](https://microk8s.io/) or [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/).
|
||||||
|
|
6
docs/src/contributing_docker_development.md
vendored
6
docs/src/contributing_docker_development.md
vendored
|
@ -1,4 +1,6 @@
|
||||||
Run:
|
# Docker Development
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/dessalines/lemmy
|
git clone https://github.com/dessalines/lemmy
|
||||||
|
@ -8,4 +10,4 @@ cd lemmy/docker/dev
|
||||||
|
|
||||||
and go to http://localhost:8536.
|
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).
|
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).
|
||||||
|
|
13
docs/src/contributing_local_development.md
vendored
13
docs/src/contributing_local_development.md
vendored
|
@ -7,9 +7,16 @@
|
||||||
#### Set up Postgres DB
|
#### Set up Postgres DB
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
cd server
|
||||||
psql -c 'create database lemmy with owner lemmy;' -U postgres
|
./db-init.sh
|
||||||
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
```
|
||||||
|
|
||||||
|
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
|
#### Running
|
||||||
|
|
1
docs/src/contributing_websocket_http_api.md
vendored
1
docs/src/contributing_websocket_http_api.md
vendored
|
@ -1,4 +1,5 @@
|
||||||
# Lemmy API
|
# Lemmy API
|
||||||
|
|
||||||
*Note: this may lag behind the actual API endpoints [here](../server/src/api).*
|
*Note: this may lag behind the actual API endpoints [here](../server/src/api).*
|
||||||
|
|
||||||
<!-- toc -->
|
<!-- toc -->
|
||||||
|
|
30
install.sh
vendored
30
install.sh
vendored
|
@ -1,13 +1,41 @@
|
||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
set -e
|
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
|
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||||
|
|
||||||
|
# Set other environment variables
|
||||||
export JWT_SECRET=changeme
|
export JWT_SECRET=changeme
|
||||||
export HOSTNAME=rrr
|
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
|
cd ui
|
||||||
yarn
|
yarn
|
||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
|
# Build and run the backend
|
||||||
cd ../server
|
cd ../server
|
||||||
cargo run
|
cargo run
|
||||||
|
|
||||||
|
|
2
server/config/defaults.hjson
vendored
2
server/config/defaults.hjson
vendored
|
@ -32,7 +32,7 @@
|
||||||
# rate limits for various user actions, by user ip
|
# rate limits for various user actions, by user ip
|
||||||
rate_limit: {
|
rate_limit: {
|
||||||
# maximum number of messages created in interval
|
# maximum number of messages created in interval
|
||||||
message: 30
|
message: 180
|
||||||
# interval length for message limit
|
# interval length for message limit
|
||||||
message_per_second: 60
|
message_per_second: 60
|
||||||
# maximum number of posts created in interval
|
# maximum number of posts created in interval
|
||||||
|
|
43
server/db-init.sh
vendored
Executable file
43
server/db-init.sh
vendored
Executable file
|
@ -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
|
132
server/migrations/2020-02-06-165953_change_post_title_length/down.sql
vendored
Normal file
132
server/migrations/2020-02-06-165953_change_post_title_length/down.sql
vendored
Normal file
|
@ -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;
|
133
server/migrations/2020-02-06-165953_change_post_title_length/up.sql
vendored
Normal file
133
server/migrations/2020-02-06-165953_change_post_title_length/up.sql
vendored
Normal file
|
@ -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;
|
206
server/migrations/2020-02-07-210055_add_comment_subscribed/down.sql
vendored
Normal file
206
server/migrations/2020-02-07-210055_add_comment_subscribed/down.sql
vendored
Normal file
|
@ -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
|
||||||
|
;
|
||||||
|
|
220
server/migrations/2020-02-07-210055_add_comment_subscribed/up.sql
vendored
Normal file
220
server/migrations/2020-02-07-210055_add_comment_subscribed/up.sql
vendored
Normal file
|
@ -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
|
||||||
|
;
|
||||||
|
|
88
server/migrations/2020-02-08-145624_add_post_newest_activity_time/down.sql
vendored
Normal file
88
server/migrations/2020-02-08-145624_add_post_newest_activity_time/down.sql
vendored
Normal file
|
@ -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
|
||||||
|
;
|
||||||
|
|
106
server/migrations/2020-02-08-145624_add_post_newest_activity_time/up.sql
vendored
Normal file
106
server/migrations/2020-02-08-145624_add_post_newest_activity_time/up.sql
vendored
Normal file
|
@ -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
|
||||||
|
;
|
||||||
|
|
|
@ -2,6 +2,7 @@ use super::*;
|
||||||
use crate::send_email;
|
use crate::send_email;
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use diesel::PgConnection;
|
use diesel::PgConnection;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct CreateComment {
|
pub struct CreateComment {
|
||||||
|
@ -47,6 +48,21 @@ pub struct CreateCommentLike {
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GetComments {
|
||||||
|
type_: String,
|
||||||
|
sort: String,
|
||||||
|
page: Option<i64>,
|
||||||
|
limit: Option<i64>,
|
||||||
|
pub community_id: Option<i32>,
|
||||||
|
auth: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GetCommentsResponse {
|
||||||
|
comments: Vec<CommentView>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Perform<CommentResponse> for Oper<CreateComment> {
|
impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||||
let data: &CreateComment = &self.data;
|
let data: &CreateComment = &self.data;
|
||||||
|
@ -456,3 +472,40 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Perform<GetCommentsResponse> for Oper<GetComments> {
|
||||||
|
fn perform(&self, conn: &PgConnection) -> Result<GetCommentsResponse, Error> {
|
||||||
|
let data: &GetComments = &self.data;
|
||||||
|
|
||||||
|
let user_claims: Option<Claims> = 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -126,7 +126,15 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
||||||
|
|
||||||
let inserted_post = match Post::create(&conn, &post_form) {
|
let inserted_post = match Post::create(&conn, &post_form) {
|
||||||
Ok(post) => post,
|
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
|
// They like their own post by default
|
||||||
|
@ -361,7 +369,15 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
||||||
|
|
||||||
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
|
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
|
||||||
Ok(post) => post,
|
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
|
// Mod tables
|
||||||
|
|
|
@ -15,6 +15,7 @@ table! {
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
|
community_name -> Varchar,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
@ -22,8 +23,10 @@ table! {
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
hot_rank -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
|
subscribed -> Nullable<Bool>,
|
||||||
saved -> Nullable<Bool>,
|
saved -> Nullable<Bool>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +44,7 @@ table! {
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
|
community_name -> Varchar,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
@ -48,8 +52,10 @@ table! {
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
hot_rank -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
|
subscribed -> Nullable<Bool>,
|
||||||
saved -> Nullable<Bool>,
|
saved -> Nullable<Bool>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,6 +76,7 @@ pub struct CommentView {
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
pub community_id: i32,
|
pub community_id: i32,
|
||||||
|
pub community_name: String,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
@ -77,15 +84,19 @@ pub struct CommentView {
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
|
pub hot_rank: i32,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub my_vote: Option<i32>,
|
pub my_vote: Option<i32>,
|
||||||
|
pub subscribed: Option<bool>,
|
||||||
pub saved: Option<bool>,
|
pub saved: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CommentQueryBuilder<'a> {
|
pub struct CommentQueryBuilder<'a> {
|
||||||
conn: &'a PgConnection,
|
conn: &'a PgConnection,
|
||||||
query: super::comment_view::comment_mview::BoxedQuery<'a, Pg>,
|
query: super::comment_view::comment_mview::BoxedQuery<'a, Pg>,
|
||||||
|
listing_type: ListingType,
|
||||||
sort: &'a SortType,
|
sort: &'a SortType,
|
||||||
|
for_community_id: Option<i32>,
|
||||||
for_post_id: Option<i32>,
|
for_post_id: Option<i32>,
|
||||||
for_creator_id: Option<i32>,
|
for_creator_id: Option<i32>,
|
||||||
search_term: Option<String>,
|
search_term: Option<String>,
|
||||||
|
@ -104,7 +115,9 @@ impl<'a> CommentQueryBuilder<'a> {
|
||||||
CommentQueryBuilder {
|
CommentQueryBuilder {
|
||||||
conn,
|
conn,
|
||||||
query,
|
query,
|
||||||
|
listing_type: ListingType::All,
|
||||||
sort: &SortType::New,
|
sort: &SortType::New,
|
||||||
|
for_community_id: None,
|
||||||
for_post_id: None,
|
for_post_id: None,
|
||||||
for_creator_id: None,
|
for_creator_id: None,
|
||||||
search_term: 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 {
|
pub fn sort(mut self, sort: &'a SortType) -> Self {
|
||||||
self.sort = sort;
|
self.sort = sort;
|
||||||
self
|
self
|
||||||
|
@ -130,6 +148,11 @@ impl<'a> CommentQueryBuilder<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
|
||||||
|
self.for_community_id = for_community_id.get_optional();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
|
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
|
||||||
self.search_term = search_term.get_optional();
|
self.search_term = search_term.get_optional();
|
||||||
self
|
self
|
||||||
|
@ -171,6 +194,10 @@ impl<'a> CommentQueryBuilder<'a> {
|
||||||
query = query.filter(creator_id.eq(for_creator_id));
|
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 {
|
if let Some(for_post_id) = self.for_post_id {
|
||||||
query = query.filter(post_id.eq(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)));
|
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 {
|
if self.saved_only {
|
||||||
query = query.filter(saved.eq(true));
|
query = query.filter(saved.eq(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
query = match self.sort {
|
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::New => query.order_by(published.desc()),
|
||||||
SortType::TopAll => query.order_by(score.desc()),
|
SortType::TopAll => query.order_by(score.desc()),
|
||||||
SortType::TopYear => query
|
SortType::TopYear => query
|
||||||
|
@ -199,7 +232,7 @@ impl<'a> CommentQueryBuilder<'a> {
|
||||||
SortType::TopDay => query
|
SortType::TopDay => query
|
||||||
.filter(published.gt(now - 1.days()))
|
.filter(published.gt(now - 1.days()))
|
||||||
.order_by(score.desc()),
|
.order_by(score.desc()),
|
||||||
_ => query.order_by(published.desc()),
|
// _ => query.order_by(published.desc()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
||||||
|
@ -218,9 +251,8 @@ impl CommentView {
|
||||||
from_comment_id: i32,
|
from_comment_id: i32,
|
||||||
my_user_id: Option<i32>,
|
my_user_id: Option<i32>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
use super::comment_view::comment_view::dsl::*;
|
use super::comment_view::comment_mview::dsl::*;
|
||||||
|
let mut query = comment_mview.into_boxed();
|
||||||
let mut query = comment_view.into_boxed();
|
|
||||||
|
|
||||||
// The view lets you pass a null user_id, if you're not logged in
|
// The view lets you pass a null user_id, if you're not logged in
|
||||||
if let Some(my_user_id) = my_user_id {
|
if let Some(my_user_id) = my_user_id {
|
||||||
|
@ -251,6 +283,7 @@ table! {
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
|
community_name -> Varchar,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
@ -258,8 +291,10 @@ table! {
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
hot_rank -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
|
subscribed -> Nullable<Bool>,
|
||||||
saved -> Nullable<Bool>,
|
saved -> Nullable<Bool>,
|
||||||
recipient_id -> Int4,
|
recipient_id -> Int4,
|
||||||
}
|
}
|
||||||
|
@ -281,6 +316,7 @@ pub struct ReplyView {
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
pub community_id: i32,
|
pub community_id: i32,
|
||||||
|
pub community_name: String,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
@ -288,8 +324,10 @@ pub struct ReplyView {
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
|
pub hot_rank: i32,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub my_vote: Option<i32>,
|
pub my_vote: Option<i32>,
|
||||||
|
pub subscribed: Option<bool>,
|
||||||
pub saved: Option<bool>,
|
pub saved: Option<bool>,
|
||||||
pub recipient_id: i32,
|
pub recipient_id: i32,
|
||||||
}
|
}
|
||||||
|
@ -474,6 +512,7 @@ mod tests {
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
post_id: inserted_post.id,
|
post_id: inserted_post.id,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
|
community_name: inserted_community.name.to_owned(),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
removed: false,
|
removed: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
@ -486,9 +525,11 @@ mod tests {
|
||||||
creator_avatar: None,
|
creator_avatar: None,
|
||||||
score: 1,
|
score: 1,
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
|
hot_rank: 0,
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
user_id: None,
|
user_id: None,
|
||||||
my_vote: None,
|
my_vote: None,
|
||||||
|
subscribed: None,
|
||||||
saved: None,
|
saved: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -498,6 +539,7 @@ mod tests {
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
post_id: inserted_post.id,
|
post_id: inserted_post.id,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
|
community_name: inserted_community.name.to_owned(),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
removed: false,
|
removed: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
@ -510,21 +552,26 @@ mod tests {
|
||||||
creator_avatar: None,
|
creator_avatar: None,
|
||||||
score: 1,
|
score: 1,
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
|
hot_rank: 0,
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
user_id: Some(inserted_user.id),
|
user_id: Some(inserted_user.id),
|
||||||
my_vote: Some(1),
|
my_vote: Some(1),
|
||||||
|
subscribed: None,
|
||||||
saved: 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)
|
.for_post_id(inserted_post.id)
|
||||||
.list()
|
.list()
|
||||||
.unwrap();
|
.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)
|
.for_post_id(inserted_post.id)
|
||||||
.my_user_id(inserted_user.id)
|
.my_user_id(inserted_user.id)
|
||||||
.list()
|
.list()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
read_comment_views_with_user[0].hot_rank = 0;
|
||||||
|
|
||||||
let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
|
let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
|
||||||
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
|
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
|
||||||
|
|
|
@ -227,9 +227,9 @@ impl CommunityView {
|
||||||
from_community_id: i32,
|
from_community_id: i32,
|
||||||
from_user_id: Option<i32>,
|
from_user_id: Option<i32>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
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));
|
query = query.filter(id.eq(from_community_id));
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ table! {
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
hot_rank -> Int4,
|
hot_rank -> Int4,
|
||||||
|
newest_activity_time -> Timestamp,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
subscribed -> Nullable<Bool>,
|
subscribed -> Nullable<Bool>,
|
||||||
|
@ -70,6 +71,7 @@ pub struct PostView {
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
pub hot_rank: i32,
|
pub hot_rank: i32,
|
||||||
|
pub newest_activity_time: chrono::NaiveDateTime,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub my_vote: Option<i32>,
|
pub my_vote: Option<i32>,
|
||||||
pub subscribed: Option<bool>,
|
pub subscribed: Option<bool>,
|
||||||
|
@ -106,6 +108,7 @@ table! {
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
hot_rank -> Int4,
|
hot_rank -> Int4,
|
||||||
|
newest_activity_time -> Timestamp,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
subscribed -> Nullable<Bool>,
|
subscribed -> Nullable<Bool>,
|
||||||
|
@ -121,6 +124,9 @@ pub struct PostQueryBuilder<'a> {
|
||||||
sort: &'a SortType,
|
sort: &'a SortType,
|
||||||
my_user_id: Option<i32>,
|
my_user_id: Option<i32>,
|
||||||
for_creator_id: Option<i32>,
|
for_creator_id: Option<i32>,
|
||||||
|
for_community_id: Option<i32>,
|
||||||
|
search_term: Option<String>,
|
||||||
|
url_search: Option<String>,
|
||||||
show_nsfw: bool,
|
show_nsfw: bool,
|
||||||
saved_only: bool,
|
saved_only: bool,
|
||||||
unread_only: bool,
|
unread_only: bool,
|
||||||
|
@ -137,10 +143,13 @@ impl<'a> PostQueryBuilder<'a> {
|
||||||
PostQueryBuilder {
|
PostQueryBuilder {
|
||||||
conn,
|
conn,
|
||||||
query,
|
query,
|
||||||
my_user_id: None,
|
|
||||||
for_creator_id: None,
|
|
||||||
listing_type: ListingType::All,
|
listing_type: ListingType::All,
|
||||||
sort: &SortType::Hot,
|
sort: &SortType::Hot,
|
||||||
|
my_user_id: None,
|
||||||
|
for_creator_id: None,
|
||||||
|
for_community_id: None,
|
||||||
|
search_term: None,
|
||||||
|
url_search: None,
|
||||||
show_nsfw: true,
|
show_nsfw: true,
|
||||||
saved_only: false,
|
saved_only: false,
|
||||||
unread_only: false,
|
unread_only: false,
|
||||||
|
@ -160,34 +169,22 @@ impl<'a> PostQueryBuilder<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
|
pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
|
||||||
use super::post_view::post_mview::dsl::*;
|
self.for_community_id = for_community_id.get_optional();
|
||||||
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_creator_id<T: MaybeOptional<i32>>(mut self, for_creator_id: T) -> Self {
|
pub fn for_creator_id<T: MaybeOptional<i32>>(mut self, for_creator_id: T) -> Self {
|
||||||
if let Some(for_creator_id) = for_creator_id.get_optional() {
|
self.for_creator_id = for_creator_id.get_optional();
|
||||||
self.for_creator_id = Some(for_creator_id);
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
|
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
|
||||||
use super::post_view::post_mview::dsl::*;
|
self.search_term = search_term.get_optional();
|
||||||
if let Some(search_term) = search_term.get_optional() {
|
|
||||||
self.query = self.query.filter(name.ilike(fuzzy_search(&search_term)));
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
|
pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
|
||||||
use super::post_view::post_mview::dsl::*;
|
self.url_search = url_search.get_optional();
|
||||||
if let Some(url_search) = url_search.get_optional() {
|
|
||||||
self.query = self.query.filter(url.eq(url_search));
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,6 +227,22 @@ impl<'a> PostQueryBuilder<'a> {
|
||||||
query = query.filter(subscribed.eq(true));
|
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 {
|
query = match self.sort {
|
||||||
SortType::Hot => query
|
SortType::Hot => query
|
||||||
.then_order_by(hot_rank.desc())
|
.then_order_by(hot_rank.desc())
|
||||||
|
@ -302,10 +315,10 @@ impl PostView {
|
||||||
from_post_id: i32,
|
from_post_id: i32,
|
||||||
my_user_id: Option<i32>,
|
my_user_id: Option<i32>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
use super::post_view::post_view::dsl::*;
|
use super::post_view::post_mview::dsl::*;
|
||||||
use diesel::prelude::*;
|
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));
|
query = query.filter(id.eq(from_post_id));
|
||||||
|
|
||||||
|
@ -435,6 +448,7 @@ mod tests {
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
hot_rank: 1728,
|
hot_rank: 1728,
|
||||||
published: inserted_post.published,
|
published: inserted_post.published,
|
||||||
|
newest_activity_time: inserted_post.published,
|
||||||
updated: None,
|
updated: None,
|
||||||
subscribed: None,
|
subscribed: None,
|
||||||
read: None,
|
read: None,
|
||||||
|
@ -469,6 +483,7 @@ mod tests {
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
hot_rank: 1728,
|
hot_rank: 1728,
|
||||||
published: inserted_post.published,
|
published: inserted_post.published,
|
||||||
|
newest_activity_time: inserted_post.published,
|
||||||
updated: None,
|
updated: None,
|
||||||
subscribed: None,
|
subscribed: None,
|
||||||
read: None,
|
read: None,
|
||||||
|
|
|
@ -16,6 +16,7 @@ table! {
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
|
community_name -> Varchar,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
@ -23,6 +24,7 @@ table! {
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
hot_rank -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
saved -> Nullable<Bool>,
|
saved -> Nullable<Bool>,
|
||||||
|
@ -44,6 +46,7 @@ table! {
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
|
community_name -> Varchar,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
@ -51,6 +54,7 @@ table! {
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
hot_rank -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
saved -> Nullable<Bool>,
|
saved -> Nullable<Bool>,
|
||||||
|
@ -75,6 +79,7 @@ pub struct UserMentionView {
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
pub community_id: i32,
|
pub community_id: i32,
|
||||||
|
pub community_name: String,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
@ -82,6 +87,7 @@ pub struct UserMentionView {
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
|
pub hot_rank: i32,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub my_vote: Option<i32>,
|
pub my_vote: Option<i32>,
|
||||||
pub saved: Option<bool>,
|
pub saved: Option<bool>,
|
||||||
|
@ -149,7 +155,9 @@ impl<'a> UserMentionQueryBuilder<'a> {
|
||||||
.filter(recipient_id.eq(self.for_user_id));
|
.filter(recipient_id.eq(self.for_user_id));
|
||||||
|
|
||||||
query = match self.sort {
|
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::New => query.order_by(published.desc()),
|
||||||
SortType::TopAll => query.order_by(score.desc()),
|
SortType::TopAll => query.order_by(score.desc()),
|
||||||
SortType::TopYear => query
|
SortType::TopYear => query
|
||||||
|
@ -164,7 +172,7 @@ impl<'a> UserMentionQueryBuilder<'a> {
|
||||||
SortType::TopDay => query
|
SortType::TopDay => query
|
||||||
.filter(published.gt(now - 1.days()))
|
.filter(published.gt(now - 1.days()))
|
||||||
.order_by(score.desc()),
|
.order_by(score.desc()),
|
||||||
_ => query.order_by(published.desc()),
|
// _ => query.order_by(published.desc()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
||||||
|
|
|
@ -144,9 +144,8 @@ impl<'a> UserQueryBuilder<'a> {
|
||||||
|
|
||||||
impl UserView {
|
impl UserView {
|
||||||
pub fn read(conn: &PgConnection, from_user_id: i32) -> Result<Self, Error> {
|
pub fn read(conn: &PgConnection, from_user_id: i32) -> Result<Self, Error> {
|
||||||
use super::user_view::user_view::dsl::*;
|
use super::user_view::user_mview::dsl::*;
|
||||||
|
user_mview.find(from_user_id).first::<Self>(conn)
|
||||||
user_view.find(from_user_id).first::<Self>(conn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
||||||
|
|
|
@ -6,7 +6,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg
|
cfg
|
||||||
.route("/", web::get().to(index))
|
.route("/", web::get().to(index))
|
||||||
.route(
|
.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),
|
web::get().to(index),
|
||||||
)
|
)
|
||||||
.route("/login", 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("/communities", web::get().to(index))
|
||||||
.route("/post/{id}/comment/{id2}", web::get().to(index))
|
.route("/post/{id}/comment/{id2}", web::get().to(index))
|
||||||
.route("/post/{id}", 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("/c/{name}", web::get().to(index))
|
||||||
.route("/community/{id}", web::get().to(index))
|
.route("/community/{id}", web::get().to(index))
|
||||||
.route(
|
.route(
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
pub const VERSION: &str = "v0.6.11";
|
pub const VERSION: &str = "v0.6.25";
|
||||||
|
|
|
@ -45,4 +45,5 @@ pub enum UserOperation {
|
||||||
EditPrivateMessage,
|
EditPrivateMessage,
|
||||||
GetPrivateMessages,
|
GetPrivateMessages,
|
||||||
UserJoin,
|
UserJoin,
|
||||||
|
GetComments,
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ use serde_json::Value;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
use crate::api::comment::*;
|
use crate::api::comment::*;
|
||||||
use crate::api::community::*;
|
use crate::api::community::*;
|
||||||
|
@ -72,6 +73,13 @@ pub struct SessionInfo {
|
||||||
pub ip: IPAddr,
|
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
|
/// `ChatServer` manages chat rooms and responsible for coordinating chat
|
||||||
/// session.
|
/// session.
|
||||||
pub struct ChatServer {
|
pub struct ChatServer {
|
||||||
|
@ -88,8 +96,8 @@ pub struct ChatServer {
|
||||||
/// sessions (IE clients)
|
/// sessions (IE clients)
|
||||||
user_rooms: HashMap<UserId, HashSet<ConnectionId>>,
|
user_rooms: HashMap<UserId, HashSet<ConnectionId>>,
|
||||||
|
|
||||||
/// Rate limiting based on IP addr
|
/// Rate limiting based on rate type and IP addr
|
||||||
rate_limits: HashMap<IPAddr, RateLimitBucket>,
|
rate_limit_buckets: HashMap<RateLimitType, HashMap<IPAddr, RateLimitBucket>>,
|
||||||
|
|
||||||
rng: ThreadRng,
|
rng: ThreadRng,
|
||||||
db: Pool<ConnectionManager<PgConnection>>,
|
db: Pool<ConnectionManager<PgConnection>>,
|
||||||
|
@ -99,7 +107,7 @@ impl ChatServer {
|
||||||
pub fn startup(db: Pool<ConnectionManager<PgConnection>>) -> ChatServer {
|
pub fn startup(db: Pool<ConnectionManager<PgConnection>>) -> ChatServer {
|
||||||
ChatServer {
|
ChatServer {
|
||||||
sessions: HashMap::new(),
|
sessions: HashMap::new(),
|
||||||
rate_limits: HashMap::new(),
|
rate_limit_buckets: HashMap::new(),
|
||||||
post_rooms: HashMap::new(),
|
post_rooms: HashMap::new(),
|
||||||
community_rooms: HashMap::new(),
|
community_rooms: HashMap::new(),
|
||||||
user_rooms: HashMap::new(),
|
user_rooms: HashMap::new(),
|
||||||
|
@ -114,6 +122,12 @@ impl ChatServer {
|
||||||
sessions.remove(&id);
|
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 the room doesn't exist yet
|
||||||
if self.community_rooms.get_mut(&community_id).is_none() {
|
if self.community_rooms.get_mut(&community_id).is_none() {
|
||||||
self.community_rooms.insert(community_id, HashSet::new());
|
self.community_rooms.insert(community_id, HashSet::new());
|
||||||
|
@ -132,6 +146,12 @@ impl ChatServer {
|
||||||
sessions.remove(&id);
|
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 the room doesn't exist yet
|
||||||
if self.post_rooms.get_mut(&post_id).is_none() {
|
if self.post_rooms.get_mut(&post_id).is_none() {
|
||||||
self.post_rooms.insert(post_id, HashSet::new());
|
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);
|
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)
|
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(0, &post_sent_str, id);
|
||||||
self.send_community_room_message(community_id, &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)
|
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(
|
self.check_rate_limit_full(
|
||||||
|
RateLimitType::Register,
|
||||||
id,
|
id,
|
||||||
Settings::get().rate_limit.register,
|
Settings::get().rate_limit.register,
|
||||||
Settings::get().rate_limit.register_per_second,
|
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(
|
self.check_rate_limit_full(
|
||||||
|
RateLimitType::Post,
|
||||||
id,
|
id,
|
||||||
Settings::get().rate_limit.post,
|
Settings::get().rate_limit.post,
|
||||||
Settings::get().rate_limit.post_per_second,
|
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(
|
self.check_rate_limit_full(
|
||||||
|
RateLimitType::Message,
|
||||||
id,
|
id,
|
||||||
Settings::get().rate_limit.message,
|
Settings::get().rate_limit.message,
|
||||||
Settings::get().rate_limit.message_per_second,
|
Settings::get().rate_limit.message_per_second,
|
||||||
|
check_only,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::float_cmp)]
|
#[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(info) = self.sessions.get(&id) {
|
||||||
if let Some(rate_limit) = self.rate_limits.get_mut(&info.ip) {
|
if let Some(bucket) = self.rate_limit_buckets.get_mut(&type_) {
|
||||||
// The initial value
|
if let Some(rate_limit) = bucket.get_mut(&info.ip) {
|
||||||
if rate_limit.allowance == -2f64 {
|
let current = SystemTime::now();
|
||||||
rate_limit.allowance = rate as f64;
|
let time_passed = current.duration_since(rate_limit.last_checked)?.as_secs() as f64;
|
||||||
};
|
|
||||||
|
|
||||||
let current = SystemTime::now();
|
// The initial value
|
||||||
let time_passed = current.duration_since(rate_limit.last_checked)?.as_secs() as f64;
|
if rate_limit.allowance == -2f64 {
|
||||||
rate_limit.last_checked = current;
|
rate_limit.allowance = rate as f64;
|
||||||
rate_limit.allowance += time_passed * (rate as f64 / per as f64);
|
};
|
||||||
if rate_limit.allowance > rate as f64 {
|
|
||||||
rate_limit.allowance = rate as f64;
|
|
||||||
}
|
|
||||||
|
|
||||||
if rate_limit.allowance < 1.0 {
|
rate_limit.last_checked = current;
|
||||||
println!(
|
rate_limit.allowance += time_passed * (rate as f64 / per as f64);
|
||||||
"Rate limited IP: {}, time_passed: {}, allowance: {}",
|
if !check_only && rate_limit.allowance > rate as f64 {
|
||||||
&info.ip, time_passed, rate_limit.allowance
|
rate_limit.allowance = rate as f64;
|
||||||
);
|
}
|
||||||
Err(
|
|
||||||
APIError {
|
if rate_limit.allowance < 1.0 {
|
||||||
message: format!("Too many requests. {} per {} seconds", rate, per),
|
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 {
|
} else {
|
||||||
rate_limit.allowance -= 1.0;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -351,14 +398,24 @@ impl Handler<Connect> for ChatServer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if self.rate_limits.get(&msg.ip).is_none() {
|
for rate_limit_type in RateLimitType::iter() {
|
||||||
self.rate_limits.insert(
|
if self.rate_limit_buckets.get(&rate_limit_type).is_none() {
|
||||||
msg.ip,
|
self
|
||||||
RateLimitBucket {
|
.rate_limit_buckets
|
||||||
last_checked: SystemTime::now(),
|
.insert(rate_limit_type, HashMap::new());
|
||||||
allowance: -2f64,
|
}
|
||||||
},
|
|
||||||
);
|
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
|
id
|
||||||
|
@ -447,11 +504,18 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
|
|
||||||
// TODO: none of the chat messages are going to work if stuff is submitted via http api,
|
// TODO: none of the chat messages are going to work if stuff is submitted via http api,
|
||||||
// need to move that handling elsewhere
|
// need to move that handling elsewhere
|
||||||
|
|
||||||
|
// A DDOS check
|
||||||
|
chat.check_rate_limit_message(msg.id, false)?;
|
||||||
|
|
||||||
match user_operation {
|
match user_operation {
|
||||||
UserOperation::Login => do_user_operation::<Login, LoginResponse>(user_operation, data, &conn),
|
UserOperation::Login => do_user_operation::<Login, LoginResponse>(user_operation, data, &conn),
|
||||||
UserOperation::Register => {
|
UserOperation::Register => {
|
||||||
chat.check_rate_limit_register(msg.id)?;
|
chat.check_rate_limit_register(msg.id, true)?;
|
||||||
do_user_operation::<Register, LoginResponse>(user_operation, data, &conn)
|
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 => {
|
UserOperation::GetUserDetails => {
|
||||||
do_user_operation::<GetUserDetails, GetUserDetailsResponse>(user_operation, data, &conn)
|
do_user_operation::<GetUserDetails, GetUserDetailsResponse>(user_operation, data, &conn)
|
||||||
|
@ -527,8 +591,11 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UserOperation::CreateCommunity => {
|
UserOperation::CreateCommunity => {
|
||||||
chat.check_rate_limit_register(msg.id)?;
|
chat.check_rate_limit_register(msg.id, true)?;
|
||||||
do_user_operation::<CreateCommunity, CommunityResponse>(user_operation, data, &conn)
|
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 => {
|
UserOperation::EditCommunity => {
|
||||||
let edit_community: EditCommunity = serde_json::from_str(data)?;
|
let edit_community: EditCommunity = serde_json::from_str(data)?;
|
||||||
|
@ -589,15 +656,24 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
let res = Oper::new(get_posts).perform(&conn)?;
|
let res = Oper::new(get_posts).perform(&conn)?;
|
||||||
to_json_string(&user_operation, &res)
|
to_json_string(&user_operation, &res)
|
||||||
}
|
}
|
||||||
|
UserOperation::GetComments => {
|
||||||
|
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 => {
|
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 create_post: CreatePost = serde_json::from_str(data)?;
|
||||||
let res = Oper::new(create_post).perform(&conn)?;
|
let res = Oper::new(create_post).perform(&conn)?;
|
||||||
|
chat.check_rate_limit_post(msg.id, false)?;
|
||||||
|
|
||||||
chat.post_sends(UserOperation::CreatePost, res, msg.id)
|
chat.post_sends(UserOperation::CreatePost, res, msg.id)
|
||||||
}
|
}
|
||||||
UserOperation::CreatePostLike => {
|
UserOperation::CreatePostLike => {
|
||||||
chat.check_rate_limit_message(msg.id)?;
|
|
||||||
let create_post_like: CreatePostLike = serde_json::from_str(data)?;
|
let create_post_like: CreatePostLike = serde_json::from_str(data)?;
|
||||||
let res = Oper::new(create_post_like).perform(&conn)?;
|
let res = Oper::new(create_post_like).perform(&conn)?;
|
||||||
|
|
||||||
|
@ -613,7 +689,6 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
do_user_operation::<SavePost, PostResponse>(user_operation, data, &conn)
|
do_user_operation::<SavePost, PostResponse>(user_operation, data, &conn)
|
||||||
}
|
}
|
||||||
UserOperation::CreateComment => {
|
UserOperation::CreateComment => {
|
||||||
chat.check_rate_limit_message(msg.id)?;
|
|
||||||
let create_comment: CreateComment = serde_json::from_str(data)?;
|
let create_comment: CreateComment = serde_json::from_str(data)?;
|
||||||
let res = Oper::new(create_comment).perform(&conn)?;
|
let res = Oper::new(create_comment).perform(&conn)?;
|
||||||
|
|
||||||
|
@ -629,7 +704,6 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
do_user_operation::<SaveComment, CommentResponse>(user_operation, data, &conn)
|
do_user_operation::<SaveComment, CommentResponse>(user_operation, data, &conn)
|
||||||
}
|
}
|
||||||
UserOperation::CreateCommentLike => {
|
UserOperation::CreateCommentLike => {
|
||||||
chat.check_rate_limit_message(msg.id)?;
|
|
||||||
let create_comment_like: CreateCommentLike = serde_json::from_str(data)?;
|
let create_comment_like: CreateCommentLike = serde_json::from_str(data)?;
|
||||||
let res = Oper::new(create_comment_like).perform(&conn)?;
|
let res = Oper::new(create_comment_like).perform(&conn)?;
|
||||||
|
|
||||||
|
@ -673,7 +747,6 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
do_user_operation::<PasswordChange, LoginResponse>(user_operation, data, &conn)
|
do_user_operation::<PasswordChange, LoginResponse>(user_operation, data, &conn)
|
||||||
}
|
}
|
||||||
UserOperation::CreatePrivateMessage => {
|
UserOperation::CreatePrivateMessage => {
|
||||||
chat.check_rate_limit_message(msg.id)?;
|
|
||||||
let create_private_message: CreatePrivateMessage = serde_json::from_str(data)?;
|
let create_private_message: CreatePrivateMessage = serde_json::from_str(data)?;
|
||||||
let recipient_id = create_private_message.recipient_id;
|
let recipient_id = create_private_message.recipient_id;
|
||||||
let res = Oper::new(create_private_message).perform(&conn)?;
|
let res = Oper::new(create_private_message).perform(&conn)?;
|
||||||
|
|
38
ui/assets/css/main.css
vendored
38
ui/assets/css/main.css
vendored
|
@ -37,6 +37,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-div img {
|
.md-div img {
|
||||||
|
max-height: 90vh;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
@ -170,3 +171,40 @@ hr {
|
||||||
-o-filter: blur(10px);
|
-o-filter: blur(10px);
|
||||||
-ms-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;
|
||||||
|
}
|
||||||
|
|
7
ui/assets/css/selectr.min.css
vendored
Normal file
7
ui/assets/css/selectr.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11
ui/package.json
vendored
11
ui/package.json
vendored
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "lemmy",
|
"name": "lemmy",
|
||||||
"description": "A simple UI for lemmy",
|
"description": "The official Lemmy UI",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "Dessalines",
|
"author": "Dessalines",
|
||||||
"license": "GPL-2.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node fuse prod",
|
"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"
|
"start": "node fuse dev"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
"bootswatch": "^4.3.1",
|
"bootswatch": "^4.3.1",
|
||||||
"classcat": "^1.1.3",
|
"classcat": "^1.1.3",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"emoji-short-name": "^0.1.0",
|
"emoji-short-name": "^1.0.0",
|
||||||
"husky": "^4.2.1",
|
"husky": "^4.2.1",
|
||||||
"i18next": "^19.0.3",
|
"i18next": "^19.0.3",
|
||||||
"inferno": "^7.0.1",
|
"inferno": "^7.0.1",
|
||||||
|
@ -33,9 +33,10 @@
|
||||||
"markdown-it": "^10.0.0",
|
"markdown-it": "^10.0.0",
|
||||||
"markdown-it-container": "^2.0.0",
|
"markdown-it-container": "^2.0.0",
|
||||||
"markdown-it-emoji": "^1.4.0",
|
"markdown-it-emoji": "^1.4.0",
|
||||||
|
"mobius1-selectr": "^2.4.13",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"prettier": "^1.18.2",
|
"prettier": "^1.18.2",
|
||||||
"reconnecting-websocket": "^4.3.0",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
"rxjs": "^6.4.0",
|
"rxjs": "^6.4.0",
|
||||||
"terser": "^4.6.3",
|
"terser": "^4.6.3",
|
||||||
"toastify-js": "^1.6.2",
|
"toastify-js": "^1.6.2",
|
||||||
|
|
128
ui/src/components/comment-node.tsx
vendored
128
ui/src/components/comment-node.tsx
vendored
|
@ -15,6 +15,8 @@ import {
|
||||||
TransferCommunityForm,
|
TransferCommunityForm,
|
||||||
TransferSiteForm,
|
TransferSiteForm,
|
||||||
BanType,
|
BanType,
|
||||||
|
CommentSortType,
|
||||||
|
SortType,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import {
|
import {
|
||||||
|
@ -46,8 +48,10 @@ interface CommentNodeState {
|
||||||
showConfirmAppointAsAdmin: boolean;
|
showConfirmAppointAsAdmin: boolean;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
viewSource: boolean;
|
viewSource: boolean;
|
||||||
upvoteLoading: boolean;
|
my_vote: number;
|
||||||
downvoteLoading: boolean;
|
score: number;
|
||||||
|
upvotes: number;
|
||||||
|
downvotes: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommentNodeProps {
|
interface CommentNodeProps {
|
||||||
|
@ -58,7 +62,11 @@ interface CommentNodeProps {
|
||||||
markable?: boolean;
|
markable?: boolean;
|
||||||
moderators: Array<CommunityUser>;
|
moderators: Array<CommunityUser>;
|
||||||
admins: Array<UserView>;
|
admins: Array<UserView>;
|
||||||
|
// TODO is this necessary, can't I get it from the node itself?
|
||||||
postCreatorId?: number;
|
postCreatorId?: number;
|
||||||
|
showCommunity?: boolean;
|
||||||
|
sort?: CommentSortType;
|
||||||
|
sortType?: SortType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
|
@ -77,8 +85,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
showConfirmTransferCommunity: false,
|
showConfirmTransferCommunity: false,
|
||||||
showConfirmAppointAsMod: false,
|
showConfirmAppointAsMod: false,
|
||||||
showConfirmAppointAsAdmin: false,
|
showConfirmAppointAsAdmin: false,
|
||||||
upvoteLoading: this.props.node.comment.upvoteLoading,
|
my_vote: this.props.node.comment.my_vote,
|
||||||
downvoteLoading: this.props.node.comment.downvoteLoading,
|
score: this.props.node.comment.score,
|
||||||
|
upvotes: this.props.node.comment.upvotes,
|
||||||
|
downvotes: this.props.node.comment.downvotes,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -91,15 +101,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps: CommentNodeProps) {
|
componentWillReceiveProps(nextProps: CommentNodeProps) {
|
||||||
if (
|
this.state.my_vote = nextProps.node.comment.my_vote;
|
||||||
nextProps.node.comment.upvoteLoading !== this.state.upvoteLoading ||
|
this.state.upvotes = nextProps.node.comment.upvotes;
|
||||||
nextProps.node.comment.downvoteLoading !== this.state.downvoteLoading
|
this.state.downvotes = nextProps.node.comment.downvotes;
|
||||||
) {
|
this.state.score = nextProps.node.comment.score;
|
||||||
this.setState({
|
this.setState(this.state);
|
||||||
upvoteLoading: false,
|
|
||||||
downvoteLoading: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -116,40 +122,26 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
.viewOnly && 'no-click'}`}
|
.viewOnly && 'no-click'}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-link p-0 ${
|
className={`vote-animate btn btn-link p-0 ${
|
||||||
node.comment.my_vote == 1 ? 'text-info' : 'text-muted'
|
this.state.my_vote == 1 ? 'text-info' : 'text-muted'
|
||||||
}`}
|
}`}
|
||||||
onClick={linkEvent(node, this.handleCommentUpvote)}
|
onClick={linkEvent(node, this.handleCommentUpvote)}
|
||||||
>
|
>
|
||||||
{this.state.upvoteLoading ? (
|
<svg class="icon upvote">
|
||||||
<svg class="icon icon-spinner spin upvote">
|
<use xlinkHref="#icon-arrow-up"></use>
|
||||||
<use xlinkHref="#icon-spinner"></use>
|
</svg>
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg class="icon upvote">
|
|
||||||
<use xlinkHref="#icon-arrow-up"></use>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
<div class={`font-weight-bold text-muted`}>
|
<div class={`font-weight-bold text-muted`}>{this.state.score}</div>
|
||||||
{node.comment.score}
|
|
||||||
</div>
|
|
||||||
{WebSocketService.Instance.site.enable_downvotes && (
|
{WebSocketService.Instance.site.enable_downvotes && (
|
||||||
<button
|
<button
|
||||||
className={`btn btn-link p-0 ${
|
className={`vote-animate btn btn-link p-0 ${
|
||||||
node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'
|
this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
|
||||||
}`}
|
}`}
|
||||||
onClick={linkEvent(node, this.handleCommentDownvote)}
|
onClick={linkEvent(node, this.handleCommentDownvote)}
|
||||||
>
|
>
|
||||||
{this.state.downvoteLoading ? (
|
<svg class="icon downvote">
|
||||||
<svg class="icon icon-spinner spin downvote">
|
<use xlinkHref="#icon-arrow-down"></use>
|
||||||
<use xlinkHref="#icon-spinner"></use>
|
</svg>
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg class="icon downvote">
|
|
||||||
<use xlinkHref="#icon-arrow-down"></use>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -199,12 +191,20 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
)}
|
)}
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span>
|
<span>
|
||||||
(<span className="text-info">+{node.comment.upvotes}</span>
|
(<span className="text-info">+{this.state.upvotes}</span>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<span className="text-danger">-{node.comment.downvotes}</span>
|
<span className="text-danger">-{this.state.downvotes}</span>
|
||||||
<span>) </span>
|
<span>) </span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
{this.props.showCommunity && (
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<span> {i18n.t('to')} </span>
|
||||||
|
<Link to={`/c/${node.comment.community_name}`}>
|
||||||
|
{node.comment.community_name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span>
|
<span>
|
||||||
<MomentTime data={node.comment} />
|
<MomentTime data={node.comment} />
|
||||||
|
@ -620,6 +620,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
moderators={this.props.moderators}
|
moderators={this.props.moderators}
|
||||||
admins={this.props.admins}
|
admins={this.props.admins}
|
||||||
postCreatorId={this.props.postCreatorId}
|
postCreatorId={this.props.postCreatorId}
|
||||||
|
sort={this.props.sort}
|
||||||
|
sortType={this.props.sortType}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* A collapsed clearfix */}
|
{/* A collapsed clearfix */}
|
||||||
|
@ -756,31 +758,57 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommentUpvote(i: CommentNodeI) {
|
handleCommentUpvote(i: CommentNodeI) {
|
||||||
if (UserService.Instance.user) {
|
let new_vote = this.state.my_vote == 1 ? 0 : 1;
|
||||||
this.setState({
|
|
||||||
upvoteLoading: true,
|
if (this.state.my_vote == 1) {
|
||||||
});
|
this.state.score--;
|
||||||
|
this.state.upvotes--;
|
||||||
|
} else if (this.state.my_vote == -1) {
|
||||||
|
this.state.downvotes--;
|
||||||
|
this.state.upvotes++;
|
||||||
|
this.state.score += 2;
|
||||||
|
} else {
|
||||||
|
this.state.upvotes++;
|
||||||
|
this.state.score++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.state.my_vote = new_vote;
|
||||||
|
|
||||||
let form: CommentLikeForm = {
|
let form: CommentLikeForm = {
|
||||||
comment_id: i.comment.id,
|
comment_id: i.comment.id,
|
||||||
post_id: i.comment.post_id,
|
post_id: i.comment.post_id,
|
||||||
score: i.comment.my_vote == 1 ? 0 : 1,
|
score: this.state.my_vote,
|
||||||
};
|
};
|
||||||
|
|
||||||
WebSocketService.Instance.likeComment(form);
|
WebSocketService.Instance.likeComment(form);
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommentDownvote(i: CommentNodeI) {
|
handleCommentDownvote(i: CommentNodeI) {
|
||||||
if (UserService.Instance.user) {
|
let new_vote = this.state.my_vote == -1 ? 0 : -1;
|
||||||
this.setState({
|
|
||||||
downvoteLoading: true,
|
if (this.state.my_vote == 1) {
|
||||||
});
|
this.state.score -= 2;
|
||||||
|
this.state.upvotes--;
|
||||||
|
this.state.downvotes++;
|
||||||
|
} else if (this.state.my_vote == -1) {
|
||||||
|
this.state.downvotes--;
|
||||||
|
this.state.score++;
|
||||||
|
} else {
|
||||||
|
this.state.downvotes++;
|
||||||
|
this.state.score--;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.state.my_vote = new_vote;
|
||||||
|
|
||||||
let form: CommentLikeForm = {
|
let form: CommentLikeForm = {
|
||||||
comment_id: i.comment.id,
|
comment_id: i.comment.id,
|
||||||
post_id: i.comment.post_id,
|
post_id: i.comment.post_id,
|
||||||
score: i.comment.my_vote == -1 ? 0 : -1,
|
score: this.state.my_vote,
|
||||||
};
|
};
|
||||||
|
|
||||||
WebSocketService.Instance.likeComment(form);
|
WebSocketService.Instance.likeComment(form);
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModRemoveShow(i: CommentNode) {
|
handleModRemoveShow(i: CommentNode) {
|
||||||
|
|
21
ui/src/components/comment-nodes.tsx
vendored
21
ui/src/components/comment-nodes.tsx
vendored
|
@ -3,7 +3,10 @@ import {
|
||||||
CommentNode as CommentNodeI,
|
CommentNode as CommentNodeI,
|
||||||
CommunityUser,
|
CommunityUser,
|
||||||
UserView,
|
UserView,
|
||||||
|
CommentSortType,
|
||||||
|
SortType,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
|
import { commentSort, commentSortSortType } from '../utils';
|
||||||
import { CommentNode } from './comment-node';
|
import { CommentNode } from './comment-node';
|
||||||
|
|
||||||
interface CommentNodesState {}
|
interface CommentNodesState {}
|
||||||
|
@ -17,6 +20,9 @@ interface CommentNodesProps {
|
||||||
viewOnly?: boolean;
|
viewOnly?: boolean;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
markable?: boolean;
|
markable?: boolean;
|
||||||
|
showCommunity?: boolean;
|
||||||
|
sort?: CommentSortType;
|
||||||
|
sortType?: SortType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommentNodes extends Component<
|
export class CommentNodes extends Component<
|
||||||
|
@ -30,7 +36,7 @@ export class CommentNodes extends Component<
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="comments">
|
<div className="comments">
|
||||||
{this.props.nodes.map(node => (
|
{this.sorter().map(node => (
|
||||||
<CommentNode
|
<CommentNode
|
||||||
node={node}
|
node={node}
|
||||||
noIndent={this.props.noIndent}
|
noIndent={this.props.noIndent}
|
||||||
|
@ -40,9 +46,22 @@ export class CommentNodes extends Component<
|
||||||
admins={this.props.admins}
|
admins={this.props.admins}
|
||||||
postCreatorId={this.props.postCreatorId}
|
postCreatorId={this.props.postCreatorId}
|
||||||
markable={this.props.markable}
|
markable={this.props.markable}
|
||||||
|
showCommunity={this.props.showCommunity}
|
||||||
|
sort={this.props.sort}
|
||||||
|
sortType={this.props.sortType}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sorter(): Array<CommentNodeI> {
|
||||||
|
if (this.props.sort !== undefined) {
|
||||||
|
commentSort(this.props.nodes, this.props.sort);
|
||||||
|
} else if (this.props.sortType !== undefined) {
|
||||||
|
commentSortSortType(this.props.nodes, this.props.sortType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.nodes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
179
ui/src/components/community.tsx
vendored
179
ui/src/components/community.tsx
vendored
|
@ -13,17 +13,37 @@ import {
|
||||||
GetPostsForm,
|
GetPostsForm,
|
||||||
GetCommunityForm,
|
GetCommunityForm,
|
||||||
ListingType,
|
ListingType,
|
||||||
|
DataType,
|
||||||
GetPostsResponse,
|
GetPostsResponse,
|
||||||
PostResponse,
|
PostResponse,
|
||||||
AddModToCommunityResponse,
|
AddModToCommunityResponse,
|
||||||
BanFromCommunityResponse,
|
BanFromCommunityResponse,
|
||||||
|
Comment,
|
||||||
|
GetCommentsForm,
|
||||||
|
GetCommentsResponse,
|
||||||
|
CommentResponse,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { PostListings } from './post-listings';
|
import { PostListings } from './post-listings';
|
||||||
|
import { CommentNodes } from './comment-nodes';
|
||||||
import { SortSelect } from './sort-select';
|
import { SortSelect } from './sort-select';
|
||||||
|
import { DataTypeSelect } from './data-type-select';
|
||||||
import { Sidebar } from './sidebar';
|
import { Sidebar } from './sidebar';
|
||||||
import { wsJsonToRes, routeSortTypeToEnum, fetchLimit, toast } from '../utils';
|
import {
|
||||||
|
wsJsonToRes,
|
||||||
|
fetchLimit,
|
||||||
|
toast,
|
||||||
|
getPageFromProps,
|
||||||
|
getSortTypeFromProps,
|
||||||
|
getDataTypeFromProps,
|
||||||
|
editCommentRes,
|
||||||
|
saveCommentRes,
|
||||||
|
createCommentLikeRes,
|
||||||
|
createPostLikeFindRes,
|
||||||
|
editPostFindRes,
|
||||||
|
commentsToFlatNodes,
|
||||||
|
} from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -35,6 +55,8 @@ interface State {
|
||||||
online: number;
|
online: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
posts: Array<Post>;
|
posts: Array<Post>;
|
||||||
|
comments: Array<Comment>;
|
||||||
|
dataType: DataType;
|
||||||
sort: SortType;
|
sort: SortType;
|
||||||
page: number;
|
page: number;
|
||||||
}
|
}
|
||||||
|
@ -65,27 +87,18 @@ export class Community extends Component<any, State> {
|
||||||
online: null,
|
online: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
posts: [],
|
posts: [],
|
||||||
sort: this.getSortTypeFromProps(this.props),
|
comments: [],
|
||||||
page: this.getPageFromProps(this.props),
|
dataType: getDataTypeFromProps(this.props),
|
||||||
|
sort: getSortTypeFromProps(this.props),
|
||||||
|
page: getPageFromProps(this.props),
|
||||||
};
|
};
|
||||||
|
|
||||||
getSortTypeFromProps(props: any): SortType {
|
|
||||||
return props.match.params.sort
|
|
||||||
? routeSortTypeToEnum(props.match.params.sort)
|
|
||||||
: UserService.Instance.user
|
|
||||||
? UserService.Instance.user.default_sort_type
|
|
||||||
: SortType.Hot;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPageFromProps(props: any): number {
|
|
||||||
return props.match.params.page ? Number(props.match.params.page) : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
this.handleSortChange = this.handleSortChange.bind(this);
|
this.handleSortChange = this.handleSortChange.bind(this);
|
||||||
|
this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
|
||||||
|
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||||
|
@ -112,16 +125,18 @@ export class Community extends Component<any, State> {
|
||||||
nextProps.history.action == 'POP' ||
|
nextProps.history.action == 'POP' ||
|
||||||
nextProps.history.action == 'PUSH'
|
nextProps.history.action == 'PUSH'
|
||||||
) {
|
) {
|
||||||
this.state.sort = this.getSortTypeFromProps(nextProps);
|
this.state.dataType = getDataTypeFromProps(nextProps);
|
||||||
this.state.page = this.getPageFromProps(nextProps);
|
this.state.sort = getSortTypeFromProps(nextProps);
|
||||||
|
this.state.page = getPageFromProps(nextProps);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
{this.selects()}
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -144,8 +159,7 @@ export class Community extends Component<any, State> {
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</h5>
|
</h5>
|
||||||
{this.selects()}
|
{this.listings()}
|
||||||
<PostListings posts={this.state.posts} removeDuplicates />
|
|
||||||
{this.paginator()}
|
{this.paginator()}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
|
@ -162,17 +176,40 @@ export class Community extends Component<any, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listings() {
|
||||||
|
return this.state.dataType == DataType.Post ? (
|
||||||
|
<PostListings
|
||||||
|
posts={this.state.posts}
|
||||||
|
removeDuplicates
|
||||||
|
sort={this.state.sort}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CommentNodes
|
||||||
|
nodes={commentsToFlatNodes(this.state.comments)}
|
||||||
|
noIndent
|
||||||
|
sortType={this.state.sort}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
selects() {
|
selects() {
|
||||||
return (
|
return (
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
|
<DataTypeSelect
|
||||||
|
type_={this.state.dataType}
|
||||||
|
onChange={this.handleDataTypeChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="mx-3">
|
||||||
|
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
|
||||||
|
</span>
|
||||||
<a
|
<a
|
||||||
href={`/feeds/c/${this.state.communityName}.xml?sort=${
|
href={`/feeds/c/${this.state.communityName}.xml?sort=${
|
||||||
SortType[this.state.sort]
|
SortType[this.state.sort]
|
||||||
}`}
|
}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<svg class="icon mx-2 text-muted small">
|
<svg class="icon text-muted small">
|
||||||
<use xlinkHref="#icon-rss">#</use>
|
<use xlinkHref="#icon-rss">#</use>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
@ -207,7 +244,7 @@ export class Community extends Component<any, State> {
|
||||||
i.state.page++;
|
i.state.page++;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchData();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,7 +252,7 @@ export class Community extends Component<any, State> {
|
||||||
i.state.page--;
|
i.state.page--;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchData();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,26 +262,48 @@ export class Community extends Component<any, State> {
|
||||||
this.state.loading = true;
|
this.state.loading = true;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.updateUrl();
|
this.updateUrl();
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDataTypeChange(val: DataType) {
|
||||||
|
this.state.dataType = val;
|
||||||
|
this.state.page = 1;
|
||||||
|
this.state.loading = true;
|
||||||
|
this.setState(this.state);
|
||||||
|
this.updateUrl();
|
||||||
|
this.fetchData();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrl() {
|
updateUrl() {
|
||||||
|
let dataTypeStr = DataType[this.state.dataType].toLowerCase();
|
||||||
let sortStr = SortType[this.state.sort].toLowerCase();
|
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||||
this.props.history.push(
|
this.props.history.push(
|
||||||
`/c/${this.state.community.name}/sort/${sortStr}/page/${this.state.page}`
|
`/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${this.state.page}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchPosts() {
|
fetchData() {
|
||||||
let getPostsForm: GetPostsForm = {
|
if (this.state.dataType == DataType.Post) {
|
||||||
page: this.state.page,
|
let getPostsForm: GetPostsForm = {
|
||||||
limit: fetchLimit,
|
page: this.state.page,
|
||||||
sort: SortType[this.state.sort],
|
limit: fetchLimit,
|
||||||
type_: ListingType[ListingType.Community],
|
sort: SortType[this.state.sort],
|
||||||
community_id: this.state.community.id,
|
type_: ListingType[ListingType.Community],
|
||||||
};
|
community_id: this.state.community.id,
|
||||||
WebSocketService.Instance.getPosts(getPostsForm);
|
};
|
||||||
|
WebSocketService.Instance.getPosts(getPostsForm);
|
||||||
|
} else {
|
||||||
|
let getCommentsForm: GetCommentsForm = {
|
||||||
|
page: this.state.page,
|
||||||
|
limit: fetchLimit,
|
||||||
|
sort: SortType[this.state.sort],
|
||||||
|
type_: ListingType[ListingType.Community],
|
||||||
|
community_id: this.state.community.id,
|
||||||
|
};
|
||||||
|
WebSocketService.Instance.getComments(getCommentsForm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: WebSocketJsonResponse) {
|
parseMessage(msg: WebSocketJsonResponse) {
|
||||||
|
@ -255,7 +314,7 @@ export class Community extends Component<any, State> {
|
||||||
this.context.router.history.push('/');
|
this.context.router.history.push('/');
|
||||||
return;
|
return;
|
||||||
} else if (msg.reconnect) {
|
} else if (msg.reconnect) {
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
} else if (res.op == UserOperation.GetCommunity) {
|
} else if (res.op == UserOperation.GetCommunity) {
|
||||||
let data = res.data as GetCommunityResponse;
|
let data = res.data as GetCommunityResponse;
|
||||||
this.state.community = data.community;
|
this.state.community = data.community;
|
||||||
|
@ -264,7 +323,7 @@ export class Community extends Component<any, State> {
|
||||||
this.state.online = data.online;
|
this.state.online = data.online;
|
||||||
document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`;
|
document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
} else if (res.op == UserOperation.EditCommunity) {
|
} else if (res.op == UserOperation.EditCommunity) {
|
||||||
let data = res.data as CommunityResponse;
|
let data = res.data as CommunityResponse;
|
||||||
this.state.community = data.community;
|
this.state.community = data.community;
|
||||||
|
@ -282,12 +341,7 @@ export class Community extends Component<any, State> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.EditPost) {
|
} else if (res.op == UserOperation.EditPost) {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
let found = this.state.posts.find(c => c.id == data.post.id);
|
editPostFindRes(data, this.state.posts);
|
||||||
|
|
||||||
found.url = data.post.url;
|
|
||||||
found.name = data.post.name;
|
|
||||||
found.nsfw = data.post.nsfw;
|
|
||||||
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreatePost) {
|
} else if (res.op == UserOperation.CreatePost) {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
|
@ -295,17 +349,7 @@ export class Community extends Component<any, State> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreatePostLike) {
|
} else if (res.op == UserOperation.CreatePostLike) {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
let found = this.state.posts.find(c => c.id == data.post.id);
|
createPostLikeFindRes(data, this.state.posts);
|
||||||
|
|
||||||
found.score = data.post.score;
|
|
||||||
found.upvotes = data.post.upvotes;
|
|
||||||
found.downvotes = data.post.downvotes;
|
|
||||||
if (data.post.my_vote !== null) {
|
|
||||||
found.my_vote = data.post.my_vote;
|
|
||||||
found.upvoteLoading = false;
|
|
||||||
found.downvoteLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.AddModToCommunity) {
|
} else if (res.op == UserOperation.AddModToCommunity) {
|
||||||
let data = res.data as AddModToCommunityResponse;
|
let data = res.data as AddModToCommunityResponse;
|
||||||
|
@ -319,6 +363,31 @@ export class Community extends Component<any, State> {
|
||||||
.forEach(p => (p.banned = data.banned));
|
.forEach(p => (p.banned = data.banned));
|
||||||
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
} else if (res.op == UserOperation.GetComments) {
|
||||||
|
let data = res.data as GetCommentsResponse;
|
||||||
|
this.state.comments = data.comments;
|
||||||
|
this.state.loading = false;
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (res.op == UserOperation.EditComment) {
|
||||||
|
let data = res.data as CommentResponse;
|
||||||
|
editCommentRes(data, this.state.comments);
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (res.op == UserOperation.CreateComment) {
|
||||||
|
let data = res.data as CommentResponse;
|
||||||
|
|
||||||
|
// Necessary since it might be a user reply
|
||||||
|
if (data.recipient_ids.length == 0) {
|
||||||
|
this.state.comments.unshift(data.comment);
|
||||||
|
this.setState(this.state);
|
||||||
|
}
|
||||||
|
} else if (res.op == UserOperation.SaveComment) {
|
||||||
|
let data = res.data as CommentResponse;
|
||||||
|
saveCommentRes(data, this.state.comments);
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (res.op == UserOperation.CreateCommentLike) {
|
||||||
|
let data = res.data as CommentResponse;
|
||||||
|
createCommentLikeRes(data, this.state.comments);
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
65
ui/src/components/data-type-select.tsx
vendored
Normal file
65
ui/src/components/data-type-select.tsx
vendored
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { DataType } from '../interfaces';
|
||||||
|
|
||||||
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
|
interface DataTypeSelectProps {
|
||||||
|
type_: DataType;
|
||||||
|
onChange?(val: DataType): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DataTypeSelectState {
|
||||||
|
type_: DataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataTypeSelect extends Component<
|
||||||
|
DataTypeSelectProps,
|
||||||
|
DataTypeSelectState
|
||||||
|
> {
|
||||||
|
private emptyState: DataTypeSelectState = {
|
||||||
|
type_: this.props.type_,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: any, context: any) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = this.emptyState;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div class="btn-group btn-group-toggle">
|
||||||
|
<label
|
||||||
|
className={`pointer btn btn-sm btn-secondary
|
||||||
|
${this.state.type_ == DataType.Post && 'active'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value={DataType.Post}
|
||||||
|
checked={this.state.type_ == DataType.Post}
|
||||||
|
onChange={linkEvent(this, this.handleTypeChange)}
|
||||||
|
/>
|
||||||
|
{i18n.t('posts')}
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className={`pointer btn btn-sm btn-secondary ${this.state.type_ ==
|
||||||
|
DataType.Comment && 'active'}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value={DataType.Comment}
|
||||||
|
checked={this.state.type_ == DataType.Comment}
|
||||||
|
onChange={linkEvent(this, this.handleTypeChange)}
|
||||||
|
/>
|
||||||
|
{i18n.t('comments')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTypeChange(i: DataTypeSelect, event: any) {
|
||||||
|
i.state.type_ = Number(event.target.value);
|
||||||
|
i.setState(i.state);
|
||||||
|
i.props.onChange(i.state.type_);
|
||||||
|
}
|
||||||
|
}
|
87
ui/src/components/iframely-card.tsx
vendored
Normal file
87
ui/src/components/iframely-card.tsx
vendored
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { FramelyData } from '../interfaces';
|
||||||
|
import { mdToHtml } from '../utils';
|
||||||
|
|
||||||
|
interface FramelyCardProps {
|
||||||
|
iframely: FramelyData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FramelyCardState {
|
||||||
|
expanded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IFramelyCard extends Component<
|
||||||
|
FramelyCardProps,
|
||||||
|
FramelyCardState
|
||||||
|
> {
|
||||||
|
private emptyState: FramelyCardState = {
|
||||||
|
expanded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: any, context: any) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = this.emptyState;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let iframely = this.props.iframely;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{iframely.title && !this.state.expanded && (
|
||||||
|
<div class="card mt-3 mb-2">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title d-inline">
|
||||||
|
<span>
|
||||||
|
<a class="text-body" target="_blank" href={iframely.url}>
|
||||||
|
{iframely.title}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
<span class="d-inline-block ml-2 mb-2 small text-muted">
|
||||||
|
<a
|
||||||
|
class="text-muted font-italic"
|
||||||
|
target="_blank"
|
||||||
|
href={iframely.url}
|
||||||
|
>
|
||||||
|
{new URL(iframely.url).hostname}
|
||||||
|
<svg class="ml-1 icon">
|
||||||
|
<use xlinkHref="#icon-external-link"></use>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
{iframely.html && (
|
||||||
|
<span
|
||||||
|
class="ml-2 pointer text-monospace"
|
||||||
|
onClick={linkEvent(this, this.handleIframeExpand)}
|
||||||
|
>
|
||||||
|
{this.state.expanded ? '[-]' : '[+]'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{iframely.description && (
|
||||||
|
<div
|
||||||
|
className="card-text small text-muted md-div"
|
||||||
|
dangerouslySetInnerHTML={mdToHtml(iframely.description)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{this.state.expanded && (
|
||||||
|
<div
|
||||||
|
class="mt-3 mb-2"
|
||||||
|
dangerouslySetInnerHTML={{ __html: iframely.html }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleIframeExpand(i: IFramelyCard) {
|
||||||
|
i.state.expanded = !i.state.expanded;
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
}
|
46
ui/src/components/inbox.tsx
vendored
46
ui/src/components/inbox.tsx
vendored
|
@ -19,7 +19,16 @@ import {
|
||||||
PrivateMessageResponse,
|
PrivateMessageResponse,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { wsJsonToRes, fetchLimit, isCommentType, toast } from '../utils';
|
import {
|
||||||
|
wsJsonToRes,
|
||||||
|
fetchLimit,
|
||||||
|
isCommentType,
|
||||||
|
toast,
|
||||||
|
editCommentRes,
|
||||||
|
saveCommentRes,
|
||||||
|
createCommentLikeRes,
|
||||||
|
commentsToFlatNodes,
|
||||||
|
} from '../utils';
|
||||||
import { CommentNodes } from './comment-nodes';
|
import { CommentNodes } from './comment-nodes';
|
||||||
import { PrivateMessage } from './private-message';
|
import { PrivateMessage } from './private-message';
|
||||||
import { SortSelect } from './sort-select';
|
import { SortSelect } from './sort-select';
|
||||||
|
@ -197,9 +206,11 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
replies() {
|
replies() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.replies.map(reply => (
|
<CommentNodes
|
||||||
<CommentNodes nodes={[{ comment: reply }]} noIndent markable />
|
nodes={commentsToFlatNodes(this.state.replies)}
|
||||||
))}
|
noIndent
|
||||||
|
markable
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -362,15 +373,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.EditComment) {
|
} else if (res.op == UserOperation.EditComment) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
|
editCommentRes(data, this.state.replies);
|
||||||
let found = this.state.replies.find(c => c.id == data.comment.id);
|
|
||||||
found.content = data.comment.content;
|
|
||||||
found.updated = data.comment.updated;
|
|
||||||
found.removed = data.comment.removed;
|
|
||||||
found.deleted = data.comment.deleted;
|
|
||||||
found.upvotes = data.comment.upvotes;
|
|
||||||
found.downvotes = data.comment.downvotes;
|
|
||||||
found.score = data.comment.score;
|
|
||||||
|
|
||||||
// If youre in the unread view, just remove it from the list
|
// If youre in the unread view, just remove it from the list
|
||||||
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
|
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
|
||||||
|
@ -418,28 +421,17 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreatePrivateMessage) {
|
} else if (res.op == UserOperation.CreatePrivateMessage) {
|
||||||
let data = res.data as PrivateMessageResponse;
|
let data = res.data as PrivateMessageResponse;
|
||||||
|
|
||||||
if (data.message.recipient_id == UserService.Instance.user.id) {
|
if (data.message.recipient_id == UserService.Instance.user.id) {
|
||||||
this.state.messages.unshift(data.message);
|
this.state.messages.unshift(data.message);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (data.message.creator_id == UserService.Instance.user.id) {
|
|
||||||
toast(i18n.t('message_sent'));
|
|
||||||
}
|
}
|
||||||
this.setState(this.state);
|
|
||||||
} else if (res.op == UserOperation.SaveComment) {
|
} else if (res.op == UserOperation.SaveComment) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
let found = this.state.replies.find(c => c.id == data.comment.id);
|
saveCommentRes(data, this.state.replies);
|
||||||
found.saved = data.comment.saved;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreateCommentLike) {
|
} else if (res.op == UserOperation.CreateCommentLike) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
let found: Comment = this.state.replies.find(
|
createCommentLikeRes(data, this.state.replies);
|
||||||
c => c.id === data.comment.id
|
|
||||||
);
|
|
||||||
found.score = data.comment.score;
|
|
||||||
found.upvotes = data.comment.upvotes;
|
|
||||||
found.downvotes = data.comment.downvotes;
|
|
||||||
if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -451,9 +443,9 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
this.state.messages.filter(
|
this.state.messages.filter(
|
||||||
r => !r.read && r.creator_id !== UserService.Instance.user.id
|
r => !r.read && r.creator_id !== UserService.Instance.user.id
|
||||||
).length;
|
).length;
|
||||||
|
UserService.Instance.user.unreadCount = count;
|
||||||
UserService.Instance.sub.next({
|
UserService.Instance.sub.next({
|
||||||
user: UserService.Instance.user,
|
user: UserService.Instance.user,
|
||||||
unreadCount: count,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
222
ui/src/components/main.tsx
vendored
222
ui/src/components/main.tsx
vendored
|
@ -12,30 +12,46 @@ import {
|
||||||
SortType,
|
SortType,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
ListingType,
|
ListingType,
|
||||||
|
DataType,
|
||||||
SiteResponse,
|
SiteResponse,
|
||||||
GetPostsResponse,
|
GetPostsResponse,
|
||||||
PostResponse,
|
PostResponse,
|
||||||
Post,
|
Post,
|
||||||
GetPostsForm,
|
GetPostsForm,
|
||||||
|
Comment,
|
||||||
|
GetCommentsForm,
|
||||||
|
GetCommentsResponse,
|
||||||
|
CommentResponse,
|
||||||
AddAdminResponse,
|
AddAdminResponse,
|
||||||
BanUserResponse,
|
BanUserResponse,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { PostListings } from './post-listings';
|
import { PostListings } from './post-listings';
|
||||||
|
import { CommentNodes } from './comment-nodes';
|
||||||
import { SortSelect } from './sort-select';
|
import { SortSelect } from './sort-select';
|
||||||
import { ListingTypeSelect } from './listing-type-select';
|
import { ListingTypeSelect } from './listing-type-select';
|
||||||
|
import { DataTypeSelect } from './data-type-select';
|
||||||
import { SiteForm } from './site-form';
|
import { SiteForm } from './site-form';
|
||||||
import {
|
import {
|
||||||
wsJsonToRes,
|
wsJsonToRes,
|
||||||
repoUrl,
|
repoUrl,
|
||||||
mdToHtml,
|
mdToHtml,
|
||||||
fetchLimit,
|
fetchLimit,
|
||||||
routeSortTypeToEnum,
|
|
||||||
routeListingTypeToEnum,
|
|
||||||
pictshareAvatarThumbnail,
|
pictshareAvatarThumbnail,
|
||||||
showAvatars,
|
showAvatars,
|
||||||
toast,
|
toast,
|
||||||
|
getListingTypeFromProps,
|
||||||
|
getPageFromProps,
|
||||||
|
getSortTypeFromProps,
|
||||||
|
getDataTypeFromProps,
|
||||||
|
editCommentRes,
|
||||||
|
saveCommentRes,
|
||||||
|
createCommentLikeRes,
|
||||||
|
createPostLikeFindRes,
|
||||||
|
editPostFindRes,
|
||||||
|
commentsToFlatNodes,
|
||||||
|
commentSortSortType,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
@ -47,7 +63,9 @@ interface MainState {
|
||||||
showEditSite: boolean;
|
showEditSite: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
posts: Array<Post>;
|
posts: Array<Post>;
|
||||||
type_: ListingType;
|
comments: Array<Comment>;
|
||||||
|
listingType: ListingType;
|
||||||
|
dataType: DataType;
|
||||||
sort: SortType;
|
sort: SortType;
|
||||||
page: number;
|
page: number;
|
||||||
}
|
}
|
||||||
|
@ -79,38 +97,21 @@ export class Main extends Component<any, MainState> {
|
||||||
showEditSite: false,
|
showEditSite: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
posts: [],
|
posts: [],
|
||||||
type_: this.getListingTypeFromProps(this.props),
|
comments: [],
|
||||||
sort: this.getSortTypeFromProps(this.props),
|
listingType: getListingTypeFromProps(this.props),
|
||||||
page: this.getPageFromProps(this.props),
|
dataType: getDataTypeFromProps(this.props),
|
||||||
|
sort: getSortTypeFromProps(this.props),
|
||||||
|
page: getPageFromProps(this.props),
|
||||||
};
|
};
|
||||||
|
|
||||||
getListingTypeFromProps(props: any): ListingType {
|
|
||||||
return props.match.params.type
|
|
||||||
? routeListingTypeToEnum(props.match.params.type)
|
|
||||||
: UserService.Instance.user
|
|
||||||
? UserService.Instance.user.default_listing_type
|
|
||||||
: ListingType.All;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSortTypeFromProps(props: any): SortType {
|
|
||||||
return props.match.params.sort
|
|
||||||
? routeSortTypeToEnum(props.match.params.sort)
|
|
||||||
: UserService.Instance.user
|
|
||||||
? UserService.Instance.user.default_sort_type
|
|
||||||
: SortType.Hot;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPageFromProps(props: any): number {
|
|
||||||
return props.match.params.page ? Number(props.match.params.page) : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
this.handleEditCancel = this.handleEditCancel.bind(this);
|
this.handleEditCancel = this.handleEditCancel.bind(this);
|
||||||
this.handleSortChange = this.handleSortChange.bind(this);
|
this.handleSortChange = this.handleSortChange.bind(this);
|
||||||
this.handleTypeChange = this.handleTypeChange.bind(this);
|
this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
|
||||||
|
this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
|
||||||
|
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||||
|
@ -133,7 +134,7 @@ export class Main extends Component<any, MainState> {
|
||||||
|
|
||||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||||
|
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -146,11 +147,12 @@ export class Main extends Component<any, MainState> {
|
||||||
nextProps.history.action == 'POP' ||
|
nextProps.history.action == 'POP' ||
|
||||||
nextProps.history.action == 'PUSH'
|
nextProps.history.action == 'PUSH'
|
||||||
) {
|
) {
|
||||||
this.state.type_ = this.getListingTypeFromProps(nextProps);
|
this.state.listingType = getListingTypeFromProps(nextProps);
|
||||||
this.state.sort = this.getSortTypeFromProps(nextProps);
|
this.state.dataType = getDataTypeFromProps(nextProps);
|
||||||
this.state.page = this.getPageFromProps(nextProps);
|
this.state.sort = getSortTypeFromProps(nextProps);
|
||||||
|
this.state.page = getPageFromProps(nextProps);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,10 +253,11 @@ export class Main extends Component<any, MainState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrl() {
|
updateUrl() {
|
||||||
let typeStr = ListingType[this.state.type_].toLowerCase();
|
let listingTypeStr = ListingType[this.state.listingType].toLowerCase();
|
||||||
|
let dataTypeStr = DataType[this.state.dataType].toLowerCase();
|
||||||
let sortStr = SortType[this.state.sort].toLowerCase();
|
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||||
this.props.history.push(
|
this.props.history.push(
|
||||||
`/home/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
|
`/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${this.state.page}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,6 +386,7 @@ export class Main extends Component<any, MainState> {
|
||||||
posts() {
|
posts() {
|
||||||
return (
|
return (
|
||||||
<div class="main-content-wrapper">
|
<div class="main-content-wrapper">
|
||||||
|
{this.selects()}
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -391,12 +395,7 @@ export class Main extends Component<any, MainState> {
|
||||||
</h5>
|
</h5>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{this.selects()}
|
{this.listings()}
|
||||||
<PostListings
|
|
||||||
posts={this.state.posts}
|
|
||||||
showCommunity
|
|
||||||
removeDuplicates
|
|
||||||
/>
|
|
||||||
{this.paginator()}
|
{this.paginator()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -404,17 +403,41 @@ export class Main extends Component<any, MainState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listings() {
|
||||||
|
return this.state.dataType == DataType.Post ? (
|
||||||
|
<PostListings
|
||||||
|
posts={this.state.posts}
|
||||||
|
showCommunity
|
||||||
|
removeDuplicates
|
||||||
|
sort={this.state.sort}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CommentNodes
|
||||||
|
nodes={commentsToFlatNodes(this.state.comments)}
|
||||||
|
noIndent
|
||||||
|
showCommunity
|
||||||
|
sortType={this.state.sort}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
selects() {
|
selects() {
|
||||||
return (
|
return (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<ListingTypeSelect
|
<DataTypeSelect
|
||||||
type_={this.state.type_}
|
type_={this.state.dataType}
|
||||||
onChange={this.handleTypeChange}
|
onChange={this.handleDataTypeChange}
|
||||||
/>
|
/>
|
||||||
<span class="mx-2">
|
<span class="mx-3">
|
||||||
|
<ListingTypeSelect
|
||||||
|
type_={this.state.listingType}
|
||||||
|
onChange={this.handleListingTypeChange}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="mr-2">
|
||||||
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
|
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
|
||||||
</span>
|
</span>
|
||||||
{this.state.type_ == ListingType.All && (
|
{this.state.listingType == ListingType.All && (
|
||||||
<a
|
<a
|
||||||
href={`/feeds/all.xml?sort=${SortType[this.state.sort]}`}
|
href={`/feeds/all.xml?sort=${SortType[this.state.sort]}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -425,7 +448,7 @@ export class Main extends Component<any, MainState> {
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{UserService.Instance.user &&
|
{UserService.Instance.user &&
|
||||||
this.state.type_ == ListingType.Subscribed && (
|
this.state.listingType == ListingType.Subscribed && (
|
||||||
<a
|
<a
|
||||||
href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${
|
href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${
|
||||||
SortType[this.state.sort]
|
SortType[this.state.sort]
|
||||||
|
@ -488,7 +511,7 @@ export class Main extends Component<any, MainState> {
|
||||||
i.state.loading = true;
|
i.state.loading = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchData();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -497,7 +520,7 @@ export class Main extends Component<any, MainState> {
|
||||||
i.state.loading = true;
|
i.state.loading = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchData();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -507,28 +530,48 @@ export class Main extends Component<any, MainState> {
|
||||||
this.state.loading = true;
|
this.state.loading = true;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.updateUrl();
|
this.updateUrl();
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTypeChange(val: ListingType) {
|
handleListingTypeChange(val: ListingType) {
|
||||||
this.state.type_ = val;
|
this.state.listingType = val;
|
||||||
this.state.page = 1;
|
this.state.page = 1;
|
||||||
this.state.loading = true;
|
this.state.loading = true;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.updateUrl();
|
this.updateUrl();
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchPosts() {
|
handleDataTypeChange(val: DataType) {
|
||||||
let getPostsForm: GetPostsForm = {
|
this.state.dataType = val;
|
||||||
page: this.state.page,
|
this.state.page = 1;
|
||||||
limit: fetchLimit,
|
this.state.loading = true;
|
||||||
sort: SortType[this.state.sort],
|
this.setState(this.state);
|
||||||
type_: ListingType[this.state.type_],
|
this.updateUrl();
|
||||||
};
|
this.fetchData();
|
||||||
WebSocketService.Instance.getPosts(getPostsForm);
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData() {
|
||||||
|
if (this.state.dataType == DataType.Post) {
|
||||||
|
let getPostsForm: GetPostsForm = {
|
||||||
|
page: this.state.page,
|
||||||
|
limit: fetchLimit,
|
||||||
|
sort: SortType[this.state.sort],
|
||||||
|
type_: ListingType[this.state.listingType],
|
||||||
|
};
|
||||||
|
WebSocketService.Instance.getPosts(getPostsForm);
|
||||||
|
} else {
|
||||||
|
let getCommentsForm: GetCommentsForm = {
|
||||||
|
page: this.state.page,
|
||||||
|
limit: fetchLimit,
|
||||||
|
sort: SortType[this.state.sort],
|
||||||
|
type_: ListingType[this.state.listingType],
|
||||||
|
};
|
||||||
|
WebSocketService.Instance.getComments(getCommentsForm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: WebSocketJsonResponse) {
|
parseMessage(msg: WebSocketJsonResponse) {
|
||||||
|
@ -538,7 +581,7 @@ export class Main extends Component<any, MainState> {
|
||||||
toast(i18n.t(msg.error), 'danger');
|
toast(i18n.t(msg.error), 'danger');
|
||||||
return;
|
return;
|
||||||
} else if (msg.reconnect) {
|
} else if (msg.reconnect) {
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
} else if (res.op == UserOperation.GetFollowedCommunities) {
|
} else if (res.op == UserOperation.GetFollowedCommunities) {
|
||||||
let data = res.data as GetFollowedCommunitiesResponse;
|
let data = res.data as GetFollowedCommunitiesResponse;
|
||||||
this.state.subscribedCommunities = data.communities;
|
this.state.subscribedCommunities = data.communities;
|
||||||
|
@ -574,7 +617,7 @@ export class Main extends Component<any, MainState> {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
|
|
||||||
// If you're on subscribed, only push it if you're subscribed.
|
// If you're on subscribed, only push it if you're subscribed.
|
||||||
if (this.state.type_ == ListingType.Subscribed) {
|
if (this.state.listingType == ListingType.Subscribed) {
|
||||||
if (
|
if (
|
||||||
this.state.subscribedCommunities
|
this.state.subscribedCommunities
|
||||||
.map(c => c.community_id)
|
.map(c => c.community_id)
|
||||||
|
@ -589,26 +632,11 @@ export class Main extends Component<any, MainState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.EditPost) {
|
} else if (res.op == UserOperation.EditPost) {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
let found = this.state.posts.find(c => c.id == data.post.id);
|
editPostFindRes(data, this.state.posts);
|
||||||
|
|
||||||
found.url = data.post.url;
|
|
||||||
found.name = data.post.name;
|
|
||||||
found.nsfw = data.post.nsfw;
|
|
||||||
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreatePostLike) {
|
} else if (res.op == UserOperation.CreatePostLike) {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
let found = this.state.posts.find(c => c.id == data.post.id);
|
createPostLikeFindRes(data, this.state.posts);
|
||||||
|
|
||||||
found.score = data.post.score;
|
|
||||||
found.upvotes = data.post.upvotes;
|
|
||||||
found.downvotes = data.post.downvotes;
|
|
||||||
if (data.post.my_vote !== null) {
|
|
||||||
found.my_vote = data.post.my_vote;
|
|
||||||
found.upvoteLoading = false;
|
|
||||||
found.downvoteLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.AddAdmin) {
|
} else if (res.op == UserOperation.AddAdmin) {
|
||||||
let data = res.data as AddAdminResponse;
|
let data = res.data as AddAdminResponse;
|
||||||
|
@ -632,6 +660,42 @@ export class Main extends Component<any, MainState> {
|
||||||
.forEach(p => (p.banned = data.banned));
|
.forEach(p => (p.banned = data.banned));
|
||||||
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
} else if (res.op == UserOperation.GetComments) {
|
||||||
|
let data = res.data as GetCommentsResponse;
|
||||||
|
this.state.comments = data.comments;
|
||||||
|
this.state.loading = false;
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (res.op == UserOperation.EditComment) {
|
||||||
|
let data = res.data as CommentResponse;
|
||||||
|
editCommentRes(data, this.state.comments);
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (res.op == UserOperation.CreateComment) {
|
||||||
|
let data = res.data as CommentResponse;
|
||||||
|
|
||||||
|
// Necessary since it might be a user reply
|
||||||
|
if (data.recipient_ids.length == 0) {
|
||||||
|
// If you're on subscribed, only push it if you're subscribed.
|
||||||
|
if (this.state.listingType == ListingType.Subscribed) {
|
||||||
|
if (
|
||||||
|
this.state.subscribedCommunities
|
||||||
|
.map(c => c.community_id)
|
||||||
|
.includes(data.comment.community_id)
|
||||||
|
) {
|
||||||
|
this.state.comments.unshift(data.comment);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.state.comments.unshift(data.comment);
|
||||||
|
}
|
||||||
|
this.setState(this.state);
|
||||||
|
}
|
||||||
|
} else if (res.op == UserOperation.SaveComment) {
|
||||||
|
let data = res.data as CommentResponse;
|
||||||
|
saveCommentRes(data, this.state.comments);
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (res.op == UserOperation.CreateCommentLike) {
|
||||||
|
let data = res.data as CommentResponse;
|
||||||
|
createCommentLikeRes(data, this.state.comments);
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
8
ui/src/components/navbar.tsx
vendored
8
ui/src/components/navbar.tsx
vendored
|
@ -60,8 +60,10 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
// Subscribe to user changes
|
// Subscribe to user changes
|
||||||
this.userSub = UserService.Instance.sub.subscribe(user => {
|
this.userSub = UserService.Instance.sub.subscribe(user => {
|
||||||
this.state.isLoggedIn = user.user !== undefined;
|
this.state.isLoggedIn = user.user !== undefined;
|
||||||
this.state.unreadCount = user.unreadCount;
|
if (this.state.isLoggedIn) {
|
||||||
this.requestNotificationPermission();
|
this.state.unreadCount = user.user.unreadCount;
|
||||||
|
this.requestNotificationPermission();
|
||||||
|
}
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -304,9 +306,9 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendUnreadCount() {
|
sendUnreadCount() {
|
||||||
|
UserService.Instance.user.unreadCount = this.state.unreadCount;
|
||||||
UserService.Instance.sub.next({
|
UserService.Instance.sub.next({
|
||||||
user: UserService.Instance.user,
|
user: UserService.Instance.user,
|
||||||
unreadCount: this.state.unreadCount,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
31
ui/src/components/post-form.tsx
vendored
31
ui/src/components/post-form.tsx
vendored
|
@ -35,8 +35,11 @@ import {
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import autosize from 'autosize';
|
import autosize from 'autosize';
|
||||||
import Tribute from 'tributejs/src/Tribute.js';
|
import Tribute from 'tributejs/src/Tribute.js';
|
||||||
|
import Selectr from 'mobius1-selectr';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
|
const MAX_POST_TITLE_LENGTH = 200;
|
||||||
|
|
||||||
interface PostFormProps {
|
interface PostFormProps {
|
||||||
post?: Post; // If a post is given, that means this is an edit
|
post?: Post; // If a post is given, that means this is an edit
|
||||||
params?: PostFormParams;
|
params?: PostFormParams;
|
||||||
|
@ -232,7 +235,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
required
|
required
|
||||||
rows={2}
|
rows={2}
|
||||||
minLength={3}
|
minLength={3}
|
||||||
maxLength={100}
|
maxLength={MAX_POST_TITLE_LENGTH}
|
||||||
/>
|
/>
|
||||||
{this.state.suggestedPosts.length > 0 && (
|
{this.state.suggestedPosts.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
@ -360,7 +363,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
copySuggestedTitle(i: PostForm) {
|
copySuggestedTitle(i: PostForm) {
|
||||||
i.state.postForm.name = i.state.suggestedTitle;
|
i.state.postForm.name = i.state.suggestedTitle.substring(
|
||||||
|
0,
|
||||||
|
MAX_POST_TITLE_LENGTH
|
||||||
|
);
|
||||||
i.state.suggestedTitle = undefined;
|
i.state.suggestedTitle = undefined;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
@ -509,14 +515,27 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
this.state.postForm.community_id = data.communities[0].id;
|
this.state.postForm.community_id = data.communities[0].id;
|
||||||
}
|
}
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
|
||||||
|
// Set up select searching
|
||||||
|
let selectId: any = document.getElementById('post-community');
|
||||||
|
if (selectId) {
|
||||||
|
let selector = new Selectr(selectId, { nativeDropdown: false });
|
||||||
|
selector.on('selectr.select', option => {
|
||||||
|
this.state.postForm.community_id = Number(option.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (res.op == UserOperation.CreatePost) {
|
} else if (res.op == UserOperation.CreatePost) {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
this.state.loading = false;
|
if (data.post.creator_id == UserService.Instance.user.id) {
|
||||||
this.props.onCreate(data.post.id);
|
this.state.loading = false;
|
||||||
|
this.props.onCreate(data.post.id);
|
||||||
|
}
|
||||||
} else if (res.op == UserOperation.EditPost) {
|
} else if (res.op == UserOperation.EditPost) {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
this.state.loading = false;
|
if (data.post.creator_id == UserService.Instance.user.id) {
|
||||||
this.props.onEdit(data.post);
|
this.state.loading = false;
|
||||||
|
this.props.onEdit(data.post);
|
||||||
|
}
|
||||||
} else if (res.op == UserOperation.Search) {
|
} else if (res.op == UserOperation.Search) {
|
||||||
let data = res.data as SearchResponse;
|
let data = res.data as SearchResponse;
|
||||||
|
|
||||||
|
|
281
ui/src/components/post-listing.tsx
vendored
281
ui/src/components/post-listing.tsx
vendored
|
@ -15,9 +15,11 @@ import {
|
||||||
AddAdminForm,
|
AddAdminForm,
|
||||||
TransferSiteForm,
|
TransferSiteForm,
|
||||||
TransferCommunityForm,
|
TransferCommunityForm,
|
||||||
|
FramelyData,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { MomentTime } from './moment-time';
|
import { MomentTime } from './moment-time';
|
||||||
import { PostForm } from './post-form';
|
import { PostForm } from './post-form';
|
||||||
|
import { IFramelyCard } from './iframely-card';
|
||||||
import {
|
import {
|
||||||
mdToHtml,
|
mdToHtml,
|
||||||
canMod,
|
canMod,
|
||||||
|
@ -43,8 +45,13 @@ interface PostListingState {
|
||||||
showConfirmTransferCommunity: boolean;
|
showConfirmTransferCommunity: boolean;
|
||||||
imageExpanded: boolean;
|
imageExpanded: boolean;
|
||||||
viewSource: boolean;
|
viewSource: boolean;
|
||||||
upvoteLoading: boolean;
|
my_vote: number;
|
||||||
downvoteLoading: boolean;
|
score: number;
|
||||||
|
upvotes: number;
|
||||||
|
downvotes: number;
|
||||||
|
url: string;
|
||||||
|
iframely: FramelyData;
|
||||||
|
thumbnail: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PostListingProps {
|
interface PostListingProps {
|
||||||
|
@ -68,8 +75,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
showConfirmTransferCommunity: false,
|
showConfirmTransferCommunity: false,
|
||||||
imageExpanded: false,
|
imageExpanded: false,
|
||||||
viewSource: false,
|
viewSource: false,
|
||||||
upvoteLoading: this.props.post.upvoteLoading,
|
my_vote: this.props.post.my_vote,
|
||||||
downvoteLoading: this.props.post.downvoteLoading,
|
score: this.props.post.score,
|
||||||
|
upvotes: this.props.post.upvotes,
|
||||||
|
downvotes: this.props.post.downvotes,
|
||||||
|
url: this.props.post.url,
|
||||||
|
iframely: null,
|
||||||
|
thumbnail: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -80,18 +92,31 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
this.handlePostDisLike = this.handlePostDisLike.bind(this);
|
this.handlePostDisLike = this.handlePostDisLike.bind(this);
|
||||||
this.handleEditPost = this.handleEditPost.bind(this);
|
this.handleEditPost = this.handleEditPost.bind(this);
|
||||||
this.handleEditCancel = this.handleEditCancel.bind(this);
|
this.handleEditCancel = this.handleEditCancel.bind(this);
|
||||||
|
|
||||||
|
if (this.state.url) {
|
||||||
|
this.setThumbnail();
|
||||||
|
this.fetchIframely();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps: PostListingProps) {
|
componentWillReceiveProps(nextProps: PostListingProps) {
|
||||||
if (
|
this.state.my_vote = nextProps.post.my_vote;
|
||||||
nextProps.post.upvoteLoading !== this.state.upvoteLoading ||
|
this.state.upvotes = nextProps.post.upvotes;
|
||||||
nextProps.post.downvoteLoading !== this.state.downvoteLoading
|
this.state.downvotes = nextProps.post.downvotes;
|
||||||
) {
|
this.state.score = nextProps.post.score;
|
||||||
this.setState({
|
|
||||||
upvoteLoading: false,
|
if (nextProps.post.url !== this.state.url) {
|
||||||
downvoteLoading: false,
|
this.state.url = nextProps.post.url;
|
||||||
});
|
if (this.state.url) {
|
||||||
|
this.setThumbnail();
|
||||||
|
this.fetchIframely();
|
||||||
|
} else {
|
||||||
|
this.state.iframely = null;
|
||||||
|
this.state.thumbnail = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -112,62 +137,76 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imgThumbnail() {
|
||||||
|
let post = this.props.post;
|
||||||
|
return (
|
||||||
|
<object
|
||||||
|
className={`img-fluid thumbnail rounded ${(post.nsfw ||
|
||||||
|
post.community_nsfw) &&
|
||||||
|
'img-blur'}`}
|
||||||
|
data={imageThumbnailer(this.state.thumbnail)}
|
||||||
|
></object>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
listing() {
|
listing() {
|
||||||
let post = this.props.post;
|
let post = this.props.post;
|
||||||
return (
|
return (
|
||||||
<div class="listing col-12">
|
<div class="listing col-12">
|
||||||
<div className={`vote-bar mr-2 float-left small text-center`}>
|
<div className={`vote-bar mr-2 float-left small text-center`}>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-link p-0 ${
|
className={`vote-animate btn btn-link p-0 ${
|
||||||
post.my_vote == 1 ? 'text-info' : 'text-muted'
|
this.state.my_vote == 1 ? 'text-info' : 'text-muted'
|
||||||
}`}
|
}`}
|
||||||
onClick={linkEvent(this, this.handlePostLike)}
|
onClick={linkEvent(this, this.handlePostLike)}
|
||||||
>
|
>
|
||||||
{this.state.upvoteLoading ? (
|
<svg class="icon upvote">
|
||||||
<svg class="icon icon-spinner spin upvote">
|
<use xlinkHref="#icon-arrow-up"></use>
|
||||||
<use xlinkHref="#icon-spinner"></use>
|
</svg>
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg class="icon upvote">
|
|
||||||
<use xlinkHref="#icon-arrow-up"></use>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
<div class={`font-weight-bold text-muted`}>{post.score}</div>
|
<div class={`font-weight-bold text-muted`}>{this.state.score}</div>
|
||||||
{WebSocketService.Instance.site.enable_downvotes && (
|
{WebSocketService.Instance.site.enable_downvotes && (
|
||||||
<button
|
<button
|
||||||
className={`btn btn-link p-0 ${
|
className={`vote-animate btn btn-link p-0 ${
|
||||||
post.my_vote == -1 ? 'text-danger' : 'text-muted'
|
this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
|
||||||
}`}
|
}`}
|
||||||
onClick={linkEvent(this, this.handlePostDisLike)}
|
onClick={linkEvent(this, this.handlePostDisLike)}
|
||||||
>
|
>
|
||||||
{this.state.downvoteLoading ? (
|
<svg class="icon downvote">
|
||||||
<svg class="icon icon-spinner spin downvote">
|
<use xlinkHref="#icon-arrow-down"></use>
|
||||||
<use xlinkHref="#icon-spinner"></use>
|
</svg>
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg class="icon downvote">
|
|
||||||
<use xlinkHref="#icon-arrow-down"></use>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{post.url && isImage(post.url) && !this.state.imageExpanded && (
|
{this.state.thumbnail && !this.state.imageExpanded && (
|
||||||
<span
|
<div class="mx-2 mt-1 float-left position-relative">
|
||||||
title={i18n.t('expand_here')}
|
{isImage(this.state.url) ? (
|
||||||
class="pointer"
|
<span
|
||||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
class="text-body pointer"
|
||||||
>
|
title={i18n.t('expand_here')}
|
||||||
<img
|
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||||
className={`mx-2 mt-1 float-left img-fluid thumbnail rounded ${(post.nsfw ||
|
>
|
||||||
post.community_nsfw) &&
|
{this.imgThumbnail()}
|
||||||
'img-blur'}`}
|
<svg class="icon rounded link-overlay hover-link">
|
||||||
src={imageThumbnailer(post.url)}
|
<use xlinkHref="#icon-image"></use>
|
||||||
/>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
className="text-body"
|
||||||
|
href={this.state.url}
|
||||||
|
target="_blank"
|
||||||
|
title={this.state.url}
|
||||||
|
>
|
||||||
|
{this.imgThumbnail()}
|
||||||
|
<svg class="icon rounded link-overlay hover-link">
|
||||||
|
<use xlinkHref="#icon-external-link"></use>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{post.url && isVideo(post.url) && (
|
{this.state.url && isVideo(this.state.url) && (
|
||||||
<video
|
<video
|
||||||
playsinline
|
playsinline
|
||||||
muted
|
muted
|
||||||
|
@ -177,18 +216,18 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
height="100"
|
height="100"
|
||||||
width="150"
|
width="150"
|
||||||
>
|
>
|
||||||
<source src={post.url} type="video/mp4" />
|
<source src={this.state.url} type="video/mp4" />
|
||||||
</video>
|
</video>
|
||||||
)}
|
)}
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<div className="post-title text-wrap-truncate">
|
<div className="post-title">
|
||||||
<h5 className="mb-0 d-inline">
|
<h5 className="mb-0 d-inline">
|
||||||
{post.url ? (
|
{this.props.showBody && this.state.url ? (
|
||||||
<a
|
<a
|
||||||
className="text-body"
|
className="text-body"
|
||||||
href={post.url}
|
href={this.state.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title={post.url}
|
title={this.state.url}
|
||||||
>
|
>
|
||||||
{post.name}
|
{post.name}
|
||||||
</a>
|
</a>
|
||||||
|
@ -202,19 +241,25 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</h5>
|
</h5>
|
||||||
{post.url && (
|
{this.state.url &&
|
||||||
<small>
|
!(
|
||||||
<a
|
new URL(this.state.url).hostname == window.location.hostname
|
||||||
className="ml-2 text-muted font-italic"
|
) && (
|
||||||
href={post.url}
|
<small class="d-inline-block">
|
||||||
target="_blank"
|
<a
|
||||||
title={post.url}
|
className="ml-2 text-muted font-italic"
|
||||||
>
|
href={this.state.url}
|
||||||
{new URL(post.url).hostname}
|
target="_blank"
|
||||||
</a>
|
title={this.state.url}
|
||||||
</small>
|
>
|
||||||
)}
|
{new URL(this.state.url).hostname}
|
||||||
{post.url && isImage(post.url) && (
|
<svg class="ml-1 icon">
|
||||||
|
<use xlinkHref="#icon-external-link"></use>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
{this.state.thumbnail && (
|
||||||
<>
|
<>
|
||||||
{!this.state.imageExpanded ? (
|
{!this.state.imageExpanded ? (
|
||||||
<span
|
<span
|
||||||
|
@ -237,7 +282,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||||
>
|
>
|
||||||
<img class="img-fluid" src={post.url} />
|
<object
|
||||||
|
class="img-fluid img-expanded"
|
||||||
|
data={this.state.thumbnail}
|
||||||
|
>
|
||||||
|
<svg class="icon rounded placeholder">
|
||||||
|
<use xlinkHref="#icon-external-link"></use>
|
||||||
|
</svg>
|
||||||
|
</object>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@ -315,9 +367,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span>
|
<span>
|
||||||
(<span className="text-info">+{post.upvotes}</span>
|
(<span className="text-info">+{this.state.upvotes}</span>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<span className="text-danger">-{post.downvotes}</span>
|
<span className="text-danger">-{this.state.downvotes}</span>
|
||||||
<span>) </span>
|
<span>) </span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
@ -596,6 +648,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
{this.state.url && this.props.showBody && this.state.iframely && (
|
||||||
|
<IFramelyCard iframely={this.state.iframely} />
|
||||||
|
)}
|
||||||
{this.state.showRemoveDialog && (
|
{this.state.showRemoveDialog && (
|
||||||
<form
|
<form
|
||||||
class="form-inline"
|
class="form-inline"
|
||||||
|
@ -746,29 +801,97 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePostLike(i: PostListing) {
|
fetchIframely() {
|
||||||
if (UserService.Instance.user) {
|
fetch(`/iframely/oembed?url=${this.state.url}`)
|
||||||
i.setState({ upvoteLoading: true });
|
.then(res => res.json())
|
||||||
|
.then(res => {
|
||||||
|
this.state.iframely = res;
|
||||||
|
this.setState(this.state);
|
||||||
|
|
||||||
|
// Store and fetch the image in pictshare
|
||||||
|
if (
|
||||||
|
this.state.iframely.thumbnail_url &&
|
||||||
|
isImage(this.state.iframely.thumbnail_url)
|
||||||
|
) {
|
||||||
|
fetch(
|
||||||
|
`/pictshare/api/geturl.php?url=${this.state.iframely.thumbnail_url}`
|
||||||
|
)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(res => {
|
||||||
|
let url = `${window.location.origin}/pictshare/${res.url}`;
|
||||||
|
if (res.filetype == 'mp4') {
|
||||||
|
url += '/raw';
|
||||||
|
}
|
||||||
|
this.state.thumbnail = url;
|
||||||
|
this.setState(this.state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(`Iframely service not set up properly. ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setThumbnail() {
|
||||||
|
let simpleImg = isImage(this.state.url);
|
||||||
|
if (simpleImg) {
|
||||||
|
this.state.thumbnail = this.state.url;
|
||||||
|
} else {
|
||||||
|
this.state.thumbnail = null;
|
||||||
}
|
}
|
||||||
|
this.setState(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePostLike(i: PostListing) {
|
||||||
|
let new_vote = i.state.my_vote == 1 ? 0 : 1;
|
||||||
|
|
||||||
|
if (i.state.my_vote == 1) {
|
||||||
|
i.state.score--;
|
||||||
|
i.state.upvotes--;
|
||||||
|
} else if (i.state.my_vote == -1) {
|
||||||
|
i.state.downvotes--;
|
||||||
|
i.state.upvotes++;
|
||||||
|
i.state.score += 2;
|
||||||
|
} else {
|
||||||
|
i.state.upvotes++;
|
||||||
|
i.state.score++;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.state.my_vote = new_vote;
|
||||||
|
|
||||||
let form: CreatePostLikeForm = {
|
let form: CreatePostLikeForm = {
|
||||||
post_id: i.props.post.id,
|
post_id: i.props.post.id,
|
||||||
score: i.props.post.my_vote == 1 ? 0 : 1,
|
score: i.state.my_vote,
|
||||||
};
|
};
|
||||||
|
|
||||||
WebSocketService.Instance.likePost(form);
|
WebSocketService.Instance.likePost(form);
|
||||||
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePostDisLike(i: PostListing) {
|
handlePostDisLike(i: PostListing) {
|
||||||
if (UserService.Instance.user) {
|
let new_vote = i.state.my_vote == -1 ? 0 : -1;
|
||||||
i.setState({ downvoteLoading: true });
|
|
||||||
|
if (i.state.my_vote == 1) {
|
||||||
|
i.state.score -= 2;
|
||||||
|
i.state.upvotes--;
|
||||||
|
i.state.downvotes++;
|
||||||
|
} else if (i.state.my_vote == -1) {
|
||||||
|
i.state.downvotes--;
|
||||||
|
i.state.score++;
|
||||||
|
} else {
|
||||||
|
i.state.downvotes++;
|
||||||
|
i.state.score--;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.state.my_vote = new_vote;
|
||||||
|
|
||||||
let form: CreatePostLikeForm = {
|
let form: CreatePostLikeForm = {
|
||||||
post_id: i.props.post.id,
|
post_id: i.props.post.id,
|
||||||
score: i.props.post.my_vote == -1 ? 0 : -1,
|
score: i.state.my_vote,
|
||||||
};
|
};
|
||||||
|
|
||||||
WebSocketService.Instance.likePost(form);
|
WebSocketService.Instance.likePost(form);
|
||||||
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditClick(i: PostListing) {
|
handleEditClick(i: PostListing) {
|
||||||
|
@ -814,8 +937,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
get crossPostParams(): string {
|
get crossPostParams(): string {
|
||||||
let params = `?title=${this.props.post.name}`;
|
let params = `?title=${this.props.post.name}`;
|
||||||
if (this.props.post.url) {
|
if (this.state.url) {
|
||||||
params += `&url=${this.props.post.url}`;
|
params += `&url=${this.state.url}`;
|
||||||
}
|
}
|
||||||
if (this.props.post.body) {
|
if (this.props.post.body) {
|
||||||
params += `&body=${this.props.post.body}`;
|
params += `&body=${this.props.post.body}`;
|
||||||
|
|
31
ui/src/components/post-listings.tsx
vendored
31
ui/src/components/post-listings.tsx
vendored
|
@ -1,13 +1,16 @@
|
||||||
import { Component } from 'inferno';
|
import { Component } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Post } from '../interfaces';
|
import { Post, SortType } from '../interfaces';
|
||||||
|
import { postSort } from '../utils';
|
||||||
import { PostListing } from './post-listing';
|
import { PostListing } from './post-listing';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
interface PostListingsProps {
|
interface PostListingsProps {
|
||||||
posts: Array<Post>;
|
posts: Array<Post>;
|
||||||
showCommunity?: boolean;
|
showCommunity?: boolean;
|
||||||
removeDuplicates?: boolean;
|
removeDuplicates?: boolean;
|
||||||
|
sort?: SortType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PostListings extends Component<PostListingsProps, any> {
|
export class PostListings extends Component<PostListingsProps, any> {
|
||||||
|
@ -19,10 +22,7 @@ export class PostListings extends Component<PostListingsProps, any> {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.props.posts.length > 0 ? (
|
{this.props.posts.length > 0 ? (
|
||||||
(this.props.removeDuplicates
|
this.outer().map(post => (
|
||||||
? this.removeDuplicates(this.props.posts)
|
|
||||||
: this.props.posts
|
|
||||||
).map(post => (
|
|
||||||
<>
|
<>
|
||||||
<PostListing
|
<PostListing
|
||||||
post={post}
|
post={post}
|
||||||
|
@ -36,11 +36,9 @@ export class PostListings extends Component<PostListingsProps, any> {
|
||||||
<>
|
<>
|
||||||
<div>{i18n.t('no_posts')}</div>
|
<div>{i18n.t('no_posts')}</div>
|
||||||
{this.props.showCommunity !== undefined && (
|
{this.props.showCommunity !== undefined && (
|
||||||
<div>
|
<T i18nKey="subscribe_to_communities">
|
||||||
<Link to="/communities">
|
#<Link to="/communities">#</Link>
|
||||||
{i18n.t('subscribe_to_communities')}
|
</T>
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -48,6 +46,19 @@ export class PostListings extends Component<PostListingsProps, any> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outer(): Array<Post> {
|
||||||
|
let out = this.props.posts;
|
||||||
|
if (this.props.removeDuplicates) {
|
||||||
|
out = this.removeDuplicates(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.sort !== undefined) {
|
||||||
|
postSort(out, this.props.sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
removeDuplicates(posts: Array<Post>): Array<Post> {
|
removeDuplicates(posts: Array<Post>): Array<Post> {
|
||||||
// A map from post url to list of posts (dupes)
|
// A map from post url to list of posts (dupes)
|
||||||
let urlMap = new Map<string, Array<Post>>();
|
let urlMap = new Map<string, Array<Post>>();
|
||||||
|
|
109
ui/src/components/post.tsx
vendored
109
ui/src/components/post.tsx
vendored
|
@ -29,7 +29,15 @@ import {
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { wsJsonToRes, hotRank, toast } from '../utils';
|
import {
|
||||||
|
wsJsonToRes,
|
||||||
|
toast,
|
||||||
|
editCommentRes,
|
||||||
|
saveCommentRes,
|
||||||
|
createCommentLikeRes,
|
||||||
|
createPostLikeRes,
|
||||||
|
commentsToFlatNodes,
|
||||||
|
} from '../utils';
|
||||||
import { PostListing } from './post-listing';
|
import { PostListing } from './post-listing';
|
||||||
import { PostListings } from './post-listings';
|
import { PostListings } from './post-listings';
|
||||||
import { Sidebar } from './sidebar';
|
import { Sidebar } from './sidebar';
|
||||||
|
@ -148,6 +156,10 @@ export class Post extends Component<any, PostState> {
|
||||||
auth: null,
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editComment(form);
|
WebSocketService.Instance.editComment(form);
|
||||||
|
UserService.Instance.user.unreadCount--;
|
||||||
|
UserService.Instance.sub.next({
|
||||||
|
user: UserService.Instance.user,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,16 +268,14 @@ export class Post extends Component<any, PostState> {
|
||||||
<div class="d-none d-md-block new-comments mb-3 card border-secondary">
|
<div class="d-none d-md-block new-comments mb-3 card border-secondary">
|
||||||
<div class="card-body small">
|
<div class="card-body small">
|
||||||
<h6>{i18n.t('recent_comments')}</h6>
|
<h6>{i18n.t('recent_comments')}</h6>
|
||||||
{this.state.comments.map(comment => (
|
<CommentNodes
|
||||||
<CommentNodes
|
nodes={commentsToFlatNodes(this.state.comments)}
|
||||||
nodes={[{ comment: comment }]}
|
noIndent
|
||||||
noIndent
|
locked={this.state.post.locked}
|
||||||
locked={this.state.post.locked}
|
moderators={this.state.moderators}
|
||||||
moderators={this.state.moderators}
|
admins={this.state.admins}
|
||||||
admins={this.state.admins}
|
postCreatorId={this.state.post.creator_id}
|
||||||
postCreatorId={this.state.post.creator_id}
|
/>
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -307,48 +317,9 @@ export class Post extends Component<any, PostState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sortTree(tree);
|
|
||||||
|
|
||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
sortTree(tree: Array<CommentNodeI>) {
|
|
||||||
// First, put removed and deleted comments at the bottom, then do your other sorts
|
|
||||||
if (this.state.commentSort == CommentSortType.Top) {
|
|
||||||
tree.sort(
|
|
||||||
(a, b) =>
|
|
||||||
+a.comment.removed - +b.comment.removed ||
|
|
||||||
+a.comment.deleted - +b.comment.deleted ||
|
|
||||||
b.comment.score - a.comment.score
|
|
||||||
);
|
|
||||||
} else if (this.state.commentSort == CommentSortType.New) {
|
|
||||||
tree.sort(
|
|
||||||
(a, b) =>
|
|
||||||
+a.comment.removed - +b.comment.removed ||
|
|
||||||
+a.comment.deleted - +b.comment.deleted ||
|
|
||||||
b.comment.published.localeCompare(a.comment.published)
|
|
||||||
);
|
|
||||||
} else if (this.state.commentSort == CommentSortType.Old) {
|
|
||||||
tree.sort(
|
|
||||||
(a, b) =>
|
|
||||||
+a.comment.removed - +b.comment.removed ||
|
|
||||||
+a.comment.deleted - +b.comment.deleted ||
|
|
||||||
a.comment.published.localeCompare(b.comment.published)
|
|
||||||
);
|
|
||||||
} else if (this.state.commentSort == CommentSortType.Hot) {
|
|
||||||
tree.sort(
|
|
||||||
(a, b) =>
|
|
||||||
+a.comment.removed - +b.comment.removed ||
|
|
||||||
+a.comment.deleted - +b.comment.deleted ||
|
|
||||||
hotRank(b.comment) - hotRank(a.comment)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let node of tree) {
|
|
||||||
this.sortTree(node.children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commentsTree() {
|
commentsTree() {
|
||||||
let nodes = this.buildCommentsTree();
|
let nodes = this.buildCommentsTree();
|
||||||
return (
|
return (
|
||||||
|
@ -359,6 +330,7 @@ export class Post extends Component<any, PostState> {
|
||||||
moderators={this.state.moderators}
|
moderators={this.state.moderators}
|
||||||
admins={this.state.admins}
|
admins={this.state.admins}
|
||||||
postCreatorId={this.state.post.creator_id}
|
postCreatorId={this.state.post.creator_id}
|
||||||
|
sort={this.state.commentSort}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -408,47 +380,19 @@ export class Post extends Component<any, PostState> {
|
||||||
}
|
}
|
||||||
} else if (res.op == UserOperation.EditComment) {
|
} else if (res.op == UserOperation.EditComment) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
let found = this.state.comments.find(c => c.id == data.comment.id);
|
editCommentRes(data, this.state.comments);
|
||||||
found.content = data.comment.content;
|
|
||||||
found.updated = data.comment.updated;
|
|
||||||
found.removed = data.comment.removed;
|
|
||||||
found.deleted = data.comment.deleted;
|
|
||||||
found.upvotes = data.comment.upvotes;
|
|
||||||
found.downvotes = data.comment.downvotes;
|
|
||||||
found.score = data.comment.score;
|
|
||||||
found.read = data.comment.read;
|
|
||||||
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.SaveComment) {
|
} else if (res.op == UserOperation.SaveComment) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
let found = this.state.comments.find(c => c.id == data.comment.id);
|
saveCommentRes(data, this.state.comments);
|
||||||
found.saved = data.comment.saved;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreateCommentLike) {
|
} else if (res.op == UserOperation.CreateCommentLike) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
let found: Comment = this.state.comments.find(
|
createCommentLikeRes(data, this.state.comments);
|
||||||
c => c.id === data.comment.id
|
|
||||||
);
|
|
||||||
found.score = data.comment.score;
|
|
||||||
found.upvotes = data.comment.upvotes;
|
|
||||||
found.downvotes = data.comment.downvotes;
|
|
||||||
if (data.comment.my_vote !== null) {
|
|
||||||
found.my_vote = data.comment.my_vote;
|
|
||||||
found.upvoteLoading = false;
|
|
||||||
found.downvoteLoading = false;
|
|
||||||
}
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreatePostLike) {
|
} else if (res.op == UserOperation.CreatePostLike) {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
this.state.post.score = data.post.score;
|
createPostLikeRes(data, this.state.post);
|
||||||
this.state.post.upvotes = data.post.upvotes;
|
|
||||||
this.state.post.downvotes = data.post.downvotes;
|
|
||||||
if (data.post.my_vote !== null) {
|
|
||||||
this.state.post.my_vote = data.post.my_vote;
|
|
||||||
this.state.post.upvoteLoading = false;
|
|
||||||
this.state.post.downvoteLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.EditPost) {
|
} else if (res.op == UserOperation.EditPost) {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
|
@ -504,7 +448,6 @@ export class Post extends Component<any, PostState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.TransferSite) {
|
} else if (res.op == UserOperation.TransferSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
|
|
||||||
this.state.admins = data.admins;
|
this.state.admins = data.admins;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.TransferCommunity) {
|
} else if (res.op == UserOperation.TransferCommunity) {
|
||||||
|
|
37
ui/src/components/search.tsx
vendored
37
ui/src/components/search.tsx
vendored
|
@ -25,6 +25,9 @@ import {
|
||||||
pictshareAvatarThumbnail,
|
pictshareAvatarThumbnail,
|
||||||
showAvatars,
|
showAvatars,
|
||||||
toast,
|
toast,
|
||||||
|
createCommentLikeRes,
|
||||||
|
createPostLikeFindRes,
|
||||||
|
commentsToFlatNodes,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { PostListing } from './post-listing';
|
import { PostListing } from './post-listing';
|
||||||
import { SortSelect } from './sort-select';
|
import { SortSelect } from './sort-select';
|
||||||
|
@ -294,15 +297,11 @@ export class Search extends Component<any, SearchState> {
|
||||||
|
|
||||||
comments() {
|
comments() {
|
||||||
return (
|
return (
|
||||||
<>
|
<CommentNodes
|
||||||
{this.state.searchResponse.comments.map(comment => (
|
nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
|
||||||
<div class="row">
|
locked
|
||||||
<div class="col-12">
|
noIndent
|
||||||
<CommentNodes nodes={[{ comment: comment }]} locked noIndent />
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,27 +473,11 @@ export class Search extends Component<any, SearchState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreateCommentLike) {
|
} else if (res.op == UserOperation.CreateCommentLike) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
let found: Comment = this.state.searchResponse.comments.find(
|
createCommentLikeRes(data, this.state.searchResponse.comments);
|
||||||
c => c.id === data.comment.id
|
|
||||||
);
|
|
||||||
found.score = data.comment.score;
|
|
||||||
found.upvotes = data.comment.upvotes;
|
|
||||||
found.downvotes = data.comment.downvotes;
|
|
||||||
if (data.comment.my_vote !== null) {
|
|
||||||
found.my_vote = data.comment.my_vote;
|
|
||||||
found.upvoteLoading = false;
|
|
||||||
found.downvoteLoading = false;
|
|
||||||
}
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreatePostLike) {
|
} else if (res.op == UserOperation.CreatePostLike) {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
let found = this.state.searchResponse.posts.find(
|
createPostLikeFindRes(data, this.state.searchResponse.posts);
|
||||||
c => c.id == data.post.id
|
|
||||||
);
|
|
||||||
found.my_vote = data.post.my_vote;
|
|
||||||
found.score = data.post.score;
|
|
||||||
found.upvotes = data.post.upvotes;
|
|
||||||
found.downvotes = data.post.downvotes;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
8
ui/src/components/sponsors.tsx
vendored
8
ui/src/components/sponsors.tsx
vendored
|
@ -47,7 +47,13 @@ export class Sponsors extends Component<any, any> {
|
||||||
#<a href="https://github.com/dessalines/lemmy">#</a>
|
#<a href="https://github.com/dessalines/lemmy">#</a>
|
||||||
</T>
|
</T>
|
||||||
</p>
|
</p>
|
||||||
<a class="btn btn-secondary" href="https://www.patreon.com/dessalines">
|
<a class="btn btn-secondary" href="https://liberapay.com/Lemmy/">
|
||||||
|
{i18n.t('support_on_liberapay')}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="btn btn-secondary ml-2"
|
||||||
|
href="https://www.patreon.com/dessalines"
|
||||||
|
>
|
||||||
{i18n.t('support_on_patreon')}
|
{i18n.t('support_on_patreon')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
10
ui/src/components/symbols.tsx
vendored
10
ui/src/components/symbols.tsx
vendored
|
@ -15,6 +15,16 @@ export class Symbols extends Component<any, any> {
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
|
<symbol id="icon-image" viewBox="0 0 32 32">
|
||||||
|
<title>image</title>
|
||||||
|
<path d="M29.996 4c0.001 0.001 0.003 0.002 0.004 0.004v23.993c-0.001 0.001-0.002 0.003-0.004 0.004h-27.993c-0.001-0.001-0.003-0.002-0.004-0.004v-23.993c0.001-0.001 0.002-0.003 0.004-0.004h27.993zM30 2h-28c-1.1 0-2 0.9-2 2v24c0 1.1 0.9 2 2 2h28c1.1 0 2-0.9 2-2v-24c0-1.1-0.9-2-2-2v0z"></path>
|
||||||
|
<path d="M26 9c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3z"></path>
|
||||||
|
<path d="M28 26h-24v-4l7-12 8 10h2l7-6z"></path>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-external-link" viewBox="0 0 24 24">
|
||||||
|
<title>external-link</title>
|
||||||
|
<path d="M17 13v6c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-11c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-11c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h6c0.552 0 1-0.448 1-1s-0.448-1-1-1h-6c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v11c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h11c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1zM10.707 14.707l9.293-9.293v3.586c0 0.552 0.448 1 1 1s1-0.448 1-1v-6c0-0.136-0.027-0.265-0.076-0.383s-0.121-0.228-0.216-0.323c-0.001-0.001-0.001-0.001-0.002-0.002-0.092-0.092-0.202-0.166-0.323-0.216-0.118-0.049-0.247-0.076-0.383-0.076h-6c-0.552 0-1 0.448-1 1s0.448 1 1 1h3.586l-9.293 9.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0z"></path>
|
||||||
|
</symbol>
|
||||||
<symbol id="icon-coffee" viewBox="0 0 24 24">
|
<symbol id="icon-coffee" viewBox="0 0 24 24">
|
||||||
<title>coffee1</title>
|
<title>coffee1</title>
|
||||||
<path d="M17 19h-12c-0.553 0-1-0.447-1-1s0.447-1 1-1h12c0.553 0 1 0.447 1 1s-0.447 1-1 1z"></path>
|
<path d="M17 19h-12c-0.553 0-1-0.447-1-1s0.447-1 1-1h12c0.553 0 1 0.447 1 1s-0.447 1-1 1z"></path>
|
||||||
|
|
56
ui/src/components/user.tsx
vendored
56
ui/src/components/user.tsx
vendored
|
@ -32,6 +32,11 @@ import {
|
||||||
languages,
|
languages,
|
||||||
showAvatars,
|
showAvatars,
|
||||||
toast,
|
toast,
|
||||||
|
editCommentRes,
|
||||||
|
saveCommentRes,
|
||||||
|
createCommentLikeRes,
|
||||||
|
createPostLikeFindRes,
|
||||||
|
commentsToFlatNodes,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { PostListing } from './post-listing';
|
import { PostListing } from './post-listing';
|
||||||
import { SortSelect } from './sort-select';
|
import { SortSelect } from './sort-select';
|
||||||
|
@ -316,13 +321,11 @@ export class User extends Component<any, UserState> {
|
||||||
comments() {
|
comments() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.comments.map(comment => (
|
<CommentNodes
|
||||||
<CommentNodes
|
nodes={commentsToFlatNodes(this.state.comments)}
|
||||||
nodes={[{ comment: comment }]}
|
admins={this.state.admins}
|
||||||
admins={this.state.admins}
|
noIndent
|
||||||
noIndent
|
/>
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1032,44 +1035,27 @@ export class User extends Component<any, UserState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.EditComment) {
|
} else if (res.op == UserOperation.EditComment) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
|
editCommentRes(data, this.state.comments);
|
||||||
let found = this.state.comments.find(c => c.id == data.comment.id);
|
|
||||||
found.content = data.comment.content;
|
|
||||||
found.updated = data.comment.updated;
|
|
||||||
found.removed = data.comment.removed;
|
|
||||||
found.deleted = data.comment.deleted;
|
|
||||||
found.upvotes = data.comment.upvotes;
|
|
||||||
found.downvotes = data.comment.downvotes;
|
|
||||||
found.score = data.comment.score;
|
|
||||||
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreateComment) {
|
} else if (res.op == UserOperation.CreateComment) {
|
||||||
// let res: CommentResponse = msg;
|
let data = res.data as CommentResponse;
|
||||||
toast(i18n.t('reply_sent'));
|
if (
|
||||||
// this.state.comments.unshift(res.comment); // TODO do this right
|
UserService.Instance.user &&
|
||||||
// this.setState(this.state);
|
data.comment.creator_id == UserService.Instance.user.id
|
||||||
|
) {
|
||||||
|
toast(i18n.t('reply_sent'));
|
||||||
|
}
|
||||||
} else if (res.op == UserOperation.SaveComment) {
|
} else if (res.op == UserOperation.SaveComment) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
let found = this.state.comments.find(c => c.id == data.comment.id);
|
saveCommentRes(data, this.state.comments);
|
||||||
found.saved = data.comment.saved;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreateCommentLike) {
|
} else if (res.op == UserOperation.CreateCommentLike) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
let found: Comment = this.state.comments.find(
|
createCommentLikeRes(data, this.state.comments);
|
||||||
c => c.id === data.comment.id
|
|
||||||
);
|
|
||||||
found.score = data.comment.score;
|
|
||||||
found.upvotes = data.comment.upvotes;
|
|
||||||
found.downvotes = data.comment.downvotes;
|
|
||||||
if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreatePostLike) {
|
} else if (res.op == UserOperation.CreatePostLike) {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
let found = this.state.posts.find(c => c.id == data.post.id);
|
createPostLikeFindRes(data, this.state.posts);
|
||||||
found.my_vote = data.post.my_vote;
|
|
||||||
found.score = data.post.score;
|
|
||||||
found.upvotes = data.post.upvotes;
|
|
||||||
found.downvotes = data.post.downvotes;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.BanUser) {
|
} else if (res.op == UserOperation.BanUser) {
|
||||||
let data = res.data as BanUserResponse;
|
let data = res.data as BanUserResponse;
|
||||||
|
|
2
ui/src/i18next.ts
vendored
2
ui/src/i18next.ts
vendored
|
@ -13,6 +13,7 @@ import { it } from './translations/it';
|
||||||
import { fi } from './translations/fi';
|
import { fi } from './translations/fi';
|
||||||
import { ca } from './translations/ca';
|
import { ca } from './translations/ca';
|
||||||
import { fa } from './translations/fa';
|
import { fa } from './translations/fa';
|
||||||
|
import { pt_BR } from './translations/pt_br';
|
||||||
|
|
||||||
// https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66
|
// https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66
|
||||||
const resources = {
|
const resources = {
|
||||||
|
@ -29,6 +30,7 @@ const resources = {
|
||||||
fi,
|
fi,
|
||||||
ca,
|
ca,
|
||||||
fa,
|
fa,
|
||||||
|
pt_BR,
|
||||||
};
|
};
|
||||||
|
|
||||||
function format(value: any, format: any, lng: any): any {
|
function format(value: any, format: any, lng: any): any {
|
||||||
|
|
1
ui/src/index.html
vendored
1
ui/src/index.html
vendored
|
@ -13,6 +13,7 @@
|
||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
<link rel="stylesheet" type="text/css" href="/static/assets/css/tribute.css" />
|
<link rel="stylesheet" type="text/css" href="/static/assets/css/tribute.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/static/assets/css/toastify.css" />
|
<link rel="stylesheet" type="text/css" href="/static/assets/css/toastify.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/assets/css/selectr.min.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/darkly.min.css" id="darkly" />
|
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/darkly.min.css" id="darkly" />
|
||||||
<link rel="stylesheet" type="text/css" href="/static/assets/css/main.css" />
|
<link rel="stylesheet" type="text/css" href="/static/assets/css/main.css" />
|
||||||
|
|
||||||
|
|
4
ui/src/index.tsx
vendored
4
ui/src/index.tsx
vendored
|
@ -41,7 +41,7 @@ class Index extends Component<any, any> {
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path={`/`} component={Main} />
|
<Route exact path={`/`} component={Main} />
|
||||||
<Route
|
<Route
|
||||||
path={`/home/type/:type/sort/:sort/page/:page`}
|
path={`/home/data_type/:data_type/listing_type/:listing_type/sort/:sort/page/:page`}
|
||||||
component={Main}
|
component={Main}
|
||||||
/>
|
/>
|
||||||
<Route path={`/login`} component={Login} />
|
<Route path={`/login`} component={Login} />
|
||||||
|
@ -56,7 +56,7 @@ class Index extends Component<any, any> {
|
||||||
<Route path={`/post/:id/comment/:comment_id`} component={Post} />
|
<Route path={`/post/:id/comment/:comment_id`} component={Post} />
|
||||||
<Route path={`/post/:id`} component={Post} />
|
<Route path={`/post/:id`} component={Post} />
|
||||||
<Route
|
<Route
|
||||||
path={`/c/:name/sort/:sort/page/:page`}
|
path={`/c/:name/data_type/:data_type/sort/:sort/page/:page`}
|
||||||
component={Community}
|
component={Community}
|
||||||
/>
|
/>
|
||||||
<Route path={`/community/:id`} component={Community} />
|
<Route path={`/community/:id`} component={Community} />
|
||||||
|
|
43
ui/src/interfaces.ts
vendored
43
ui/src/interfaces.ts
vendored
|
@ -42,6 +42,7 @@ export enum UserOperation {
|
||||||
EditPrivateMessage,
|
EditPrivateMessage,
|
||||||
GetPrivateMessages,
|
GetPrivateMessages,
|
||||||
UserJoin,
|
UserJoin,
|
||||||
|
GetComments,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CommentSortType {
|
export enum CommentSortType {
|
||||||
|
@ -57,6 +58,11 @@ export enum ListingType {
|
||||||
Community,
|
Community,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum DataType {
|
||||||
|
Post,
|
||||||
|
Comment,
|
||||||
|
}
|
||||||
|
|
||||||
export enum SortType {
|
export enum SortType {
|
||||||
Hot,
|
Hot,
|
||||||
New,
|
New,
|
||||||
|
@ -87,6 +93,7 @@ export interface User {
|
||||||
lang: string;
|
lang: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
show_avatars: boolean;
|
show_avatars: boolean;
|
||||||
|
unreadCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserView {
|
export interface UserView {
|
||||||
|
@ -165,13 +172,12 @@ export interface Post {
|
||||||
upvotes: number;
|
upvotes: number;
|
||||||
downvotes: number;
|
downvotes: number;
|
||||||
hot_rank: number;
|
hot_rank: number;
|
||||||
|
newest_activity_time: string;
|
||||||
user_id?: number;
|
user_id?: number;
|
||||||
my_vote?: number;
|
my_vote?: number;
|
||||||
subscribed?: boolean;
|
subscribed?: boolean;
|
||||||
read?: boolean;
|
read?: boolean;
|
||||||
saved?: boolean;
|
saved?: boolean;
|
||||||
upvoteLoading?: boolean;
|
|
||||||
downvoteLoading?: boolean;
|
|
||||||
duplicates?: Array<Post>;
|
duplicates?: Array<Post>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,6 +193,7 @@ export interface Comment {
|
||||||
published: string;
|
published: string;
|
||||||
updated?: string;
|
updated?: string;
|
||||||
community_id: number;
|
community_id: number;
|
||||||
|
community_name: string;
|
||||||
banned: boolean;
|
banned: boolean;
|
||||||
banned_from_community: boolean;
|
banned_from_community: boolean;
|
||||||
creator_name: string;
|
creator_name: string;
|
||||||
|
@ -194,13 +201,13 @@ export interface Comment {
|
||||||
score: number;
|
score: number;
|
||||||
upvotes: number;
|
upvotes: number;
|
||||||
downvotes: number;
|
downvotes: number;
|
||||||
|
hot_rank: number;
|
||||||
user_id?: number;
|
user_id?: number;
|
||||||
my_vote?: number;
|
my_vote?: number;
|
||||||
|
subscribed?: number;
|
||||||
saved?: boolean;
|
saved?: boolean;
|
||||||
user_mention_id?: number; // For mention type
|
user_mention_id?: number; // For mention type
|
||||||
recipient_id?: number;
|
recipient_id?: number;
|
||||||
upvoteLoading?: boolean;
|
|
||||||
downvoteLoading?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
|
@ -659,6 +666,19 @@ export interface GetPostsResponse {
|
||||||
posts: Array<Post>;
|
posts: Array<Post>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetCommentsForm {
|
||||||
|
type_: string;
|
||||||
|
sort: string;
|
||||||
|
page?: number;
|
||||||
|
limit: number;
|
||||||
|
community_id?: number;
|
||||||
|
auth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetCommentsResponse {
|
||||||
|
comments: Array<Comment>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreatePostLikeForm {
|
export interface CreatePostLikeForm {
|
||||||
post_id: number;
|
post_id: number;
|
||||||
score: number;
|
score: number;
|
||||||
|
@ -856,3 +876,18 @@ export interface WebSocketJsonResponse {
|
||||||
error?: string;
|
error?: string;
|
||||||
reconnect?: boolean;
|
reconnect?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FramelyData {
|
||||||
|
url: string;
|
||||||
|
type: string;
|
||||||
|
version?: string;
|
||||||
|
title: string;
|
||||||
|
author?: string;
|
||||||
|
author_url?: string;
|
||||||
|
provider_name?: string;
|
||||||
|
thumbnail_url?: string;
|
||||||
|
thumbnail_width?: number;
|
||||||
|
thumbnail_height?: number;
|
||||||
|
description?: string;
|
||||||
|
html?: string;
|
||||||
|
}
|
||||||
|
|
7
ui/src/services/UserService.ts
vendored
7
ui/src/services/UserService.ts
vendored
|
@ -7,9 +7,8 @@ import { Subject } from 'rxjs';
|
||||||
export class UserService {
|
export class UserService {
|
||||||
private static _instance: UserService;
|
private static _instance: UserService;
|
||||||
public user: User;
|
public user: User;
|
||||||
public sub: Subject<{ user: User; unreadCount: number }> = new Subject<{
|
public sub: Subject<{ user: User }> = new Subject<{
|
||||||
user: User;
|
user: User;
|
||||||
unreadCount: number;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
|
@ -32,7 +31,7 @@ export class UserService {
|
||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
Cookies.remove('jwt');
|
Cookies.remove('jwt');
|
||||||
setTheme();
|
setTheme();
|
||||||
this.sub.next({ user: undefined, unreadCount: 0 });
|
this.sub.next({ user: undefined });
|
||||||
console.log('Logged out.');
|
console.log('Logged out.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +44,7 @@ export class UserService {
|
||||||
if (this.user.theme != 'darkly') {
|
if (this.user.theme != 'darkly') {
|
||||||
setTheme(this.user.theme);
|
setTheme(this.user.theme);
|
||||||
}
|
}
|
||||||
this.sub.next({ user: this.user, unreadCount: 0 });
|
this.sub.next({ user: this.user });
|
||||||
console.log(this.user);
|
console.log(this.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
6
ui/src/services/WebSocketService.ts
vendored
6
ui/src/services/WebSocketService.ts
vendored
|
@ -38,6 +38,7 @@ import {
|
||||||
PrivateMessageForm,
|
PrivateMessageForm,
|
||||||
EditPrivateMessageForm,
|
EditPrivateMessageForm,
|
||||||
GetPrivateMessagesForm,
|
GetPrivateMessagesForm,
|
||||||
|
GetCommentsForm,
|
||||||
UserJoinForm,
|
UserJoinForm,
|
||||||
MessageType,
|
MessageType,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
|
@ -172,6 +173,11 @@ export class WebSocketService {
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.GetPosts, form));
|
this.ws.send(this.wsSendWrapper(UserOperation.GetPosts, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getComments(form: GetCommentsForm) {
|
||||||
|
this.setAuth(form, false);
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.GetComments, form));
|
||||||
|
}
|
||||||
|
|
||||||
public likePost(form: CreatePostLikeForm) {
|
public likePost(form: CreatePostLikeForm) {
|
||||||
this.setAuth(form);
|
this.setAuth(form);
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, form));
|
this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, form));
|
||||||
|
|
1
ui/src/translations/de.ts
vendored
1
ui/src/translations/de.ts
vendored
|
@ -150,6 +150,7 @@ export const de = {
|
||||||
sponsor_message:
|
sponsor_message:
|
||||||
'Lemmy ist freie <1>Open-Source</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:',
|
'Lemmy ist freie <1>Open-Source</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:',
|
||||||
support_on_patreon: 'Auf Patreon unterstützen',
|
support_on_patreon: 'Auf Patreon unterstützen',
|
||||||
|
support_on_liberapay: 'Auf Liberapay unterstützen',
|
||||||
general_sponsors:
|
general_sponsors:
|
||||||
'Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.',
|
'Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.',
|
||||||
crypto: 'Kryptowährung',
|
crypto: 'Kryptowährung',
|
||||||
|
|
3
ui/src/translations/en.ts
vendored
3
ui/src/translations/en.ts
vendored
|
@ -171,6 +171,7 @@ export const en = {
|
||||||
sponsor_message:
|
sponsor_message:
|
||||||
'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
|
'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
|
||||||
support_on_patreon: 'Support on Patreon',
|
support_on_patreon: 'Support on Patreon',
|
||||||
|
support_on_liberapay: 'Support on Liberapay',
|
||||||
donate_to_lemmy: 'Donate to Lemmy',
|
donate_to_lemmy: 'Donate to Lemmy',
|
||||||
donate: 'Donate',
|
donate: 'Donate',
|
||||||
general_sponsors:
|
general_sponsors:
|
||||||
|
@ -200,6 +201,7 @@ export const en = {
|
||||||
couldnt_like_comment: "Couldn't like comment.",
|
couldnt_like_comment: "Couldn't like comment.",
|
||||||
couldnt_update_comment: "Couldn't update comment.",
|
couldnt_update_comment: "Couldn't update comment.",
|
||||||
couldnt_save_comment: "Couldn't save comment.",
|
couldnt_save_comment: "Couldn't save comment.",
|
||||||
|
couldnt_get_comments: "Couldn't get comments.",
|
||||||
no_comment_edit_allowed: 'Not allowed to edit comment.',
|
no_comment_edit_allowed: 'Not allowed to edit comment.',
|
||||||
no_post_edit_allowed: 'Not allowed to edit post.',
|
no_post_edit_allowed: 'Not allowed to edit post.',
|
||||||
no_community_edit_allowed: 'Not allowed to edit community.',
|
no_community_edit_allowed: 'Not allowed to edit community.',
|
||||||
|
@ -210,6 +212,7 @@ export const en = {
|
||||||
community_follower_already_exists: 'Community follower already exists.',
|
community_follower_already_exists: 'Community follower already exists.',
|
||||||
community_user_already_banned: 'Community user already banned.',
|
community_user_already_banned: 'Community user already banned.',
|
||||||
couldnt_create_post: "Couldn't create post.",
|
couldnt_create_post: "Couldn't create post.",
|
||||||
|
post_title_too_long: 'Post title too long.',
|
||||||
couldnt_like_post: "Couldn't like post.",
|
couldnt_like_post: "Couldn't like post.",
|
||||||
couldnt_find_post: "Couldn't find post.",
|
couldnt_find_post: "Couldn't find post.",
|
||||||
couldnt_get_posts: "Couldn't get posts",
|
couldnt_get_posts: "Couldn't get posts",
|
||||||
|
|
1
ui/src/translations/es.ts
vendored
1
ui/src/translations/es.ts
vendored
|
@ -169,6 +169,7 @@ export const es = {
|
||||||
sponsor_message:
|
sponsor_message:
|
||||||
'Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:',
|
'Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:',
|
||||||
support_on_patreon: 'Apoyo en Patreon',
|
support_on_patreon: 'Apoyo en Patreon',
|
||||||
|
support_on_liberapay: 'Apoyo en Liberapay',
|
||||||
donate_to_lemmy: 'Donar a Lemmy',
|
donate_to_lemmy: 'Donar a Lemmy',
|
||||||
donate: 'Donar',
|
donate: 'Donar',
|
||||||
general_sponsors:
|
general_sponsors:
|
||||||
|
|
50
ui/src/translations/fr.ts
vendored
50
ui/src/translations/fr.ts
vendored
|
@ -9,7 +9,8 @@ export const fr = {
|
||||||
posts: 'Publications',
|
posts: 'Publications',
|
||||||
related_posts: 'Ces sujets peuvent être corrélés',
|
related_posts: 'Ces sujets peuvent être corrélés',
|
||||||
cross_posts: 'Ce sujet a également été posté sur :',
|
cross_posts: 'Ce sujet a également été posté sur :',
|
||||||
cross_post: 'crosspost',
|
cross_post: 'publication croisée',
|
||||||
|
cross_posted_to: 'publication croisée à',
|
||||||
comments: 'Commentaires',
|
comments: 'Commentaires',
|
||||||
number_of_comments: '{{count}} Commentaires',
|
number_of_comments: '{{count}} Commentaires',
|
||||||
remove_comment: 'Supprimer le commentaire',
|
remove_comment: 'Supprimer le commentaire',
|
||||||
|
@ -23,11 +24,18 @@ export const fr = {
|
||||||
list_of_communities: 'Liste des communautés',
|
list_of_communities: 'Liste des communautés',
|
||||||
number_of_communities: '{{count}} communautés',
|
number_of_communities: '{{count}} communautés',
|
||||||
community_reqs: 'en minuscule, sans espace et avec tiret du bas.',
|
community_reqs: 'en minuscule, sans espace et avec tiret du bas.',
|
||||||
|
create_private_message: 'Créer un message privé',
|
||||||
|
send_secure_message: 'Envoyer le message sécurisé',
|
||||||
|
send_message: 'Enovyer le message',
|
||||||
|
message: 'Message',
|
||||||
edit: 'éditer',
|
edit: 'éditer',
|
||||||
reply: 'répondre',
|
reply: 'répondre',
|
||||||
cancel: 'Annuler',
|
cancel: 'Annuler',
|
||||||
preview: 'prévisualiser',
|
preview: 'prévisualiser',
|
||||||
upload_image: 'envoyer une image',
|
upload_image: 'envoyer une image',
|
||||||
|
avatar: 'Avatar',
|
||||||
|
upload_avatar: 'Télécharger une avatar',
|
||||||
|
show_avatars: 'Afficher les avatars',
|
||||||
formatting_help: 'aide au formattage',
|
formatting_help: 'aide au formattage',
|
||||||
view_source: 'voir la source',
|
view_source: 'voir la source',
|
||||||
unlock: 'débloquer',
|
unlock: 'débloquer',
|
||||||
|
@ -35,6 +43,7 @@ export const fr = {
|
||||||
sticky: 'épingler',
|
sticky: 'épingler',
|
||||||
unsticky: 'décrocher',
|
unsticky: 'décrocher',
|
||||||
link: 'lien',
|
link: 'lien',
|
||||||
|
archive_link: 'archiver le lien',
|
||||||
mod: 'modérateur',
|
mod: 'modérateur',
|
||||||
mods: 'modérateurs',
|
mods: 'modérateurs',
|
||||||
moderates: 'Modérer',
|
moderates: 'Modérer',
|
||||||
|
@ -89,6 +98,7 @@ export const fr = {
|
||||||
sort_type: 'Trier',
|
sort_type: 'Trier',
|
||||||
hot: 'Tendances',
|
hot: 'Tendances',
|
||||||
new: 'Nouveaux',
|
new: 'Nouveaux',
|
||||||
|
old: 'Ancien',
|
||||||
top_day: 'Top du jour',
|
top_day: 'Top du jour',
|
||||||
week: 'Semaine',
|
week: 'Semaine',
|
||||||
month: 'Mois',
|
month: 'Mois',
|
||||||
|
@ -96,12 +106,16 @@ export const fr = {
|
||||||
all: 'Tout',
|
all: 'Tout',
|
||||||
top: 'Top',
|
top: 'Top',
|
||||||
api: 'API',
|
api: 'API',
|
||||||
|
docs: 'Documentations',
|
||||||
inbox: 'Boîte de réception',
|
inbox: 'Boîte de réception',
|
||||||
inbox_for: 'Boîte de réception de <1>{{user}}</1>',
|
inbox_for: 'Boîte de réception de <1>{{user}}</1>',
|
||||||
mark_all_as_read: 'Tout marquer comme lu',
|
mark_all_as_read: 'Tout marquer comme lu',
|
||||||
type: 'Type',
|
type: 'Type',
|
||||||
unread: 'Non-lu',
|
unread: 'Non-lu',
|
||||||
|
replies: 'Réponses',
|
||||||
|
mentions: 'Mentions',
|
||||||
reply_sent: 'Réponse envoyée',
|
reply_sent: 'Réponse envoyée',
|
||||||
|
message_sent: 'Message envoyé',
|
||||||
search: 'Rechercher',
|
search: 'Rechercher',
|
||||||
overview: 'Général',
|
overview: 'Général',
|
||||||
view: 'Voir',
|
view: 'Voir',
|
||||||
|
@ -112,11 +126,29 @@ export const fr = {
|
||||||
notifications_error:
|
notifications_error:
|
||||||
'Les notifications de bureau ne sont pas discponibles sur votre navigateur. Essayez Firefox ou Chrome.',
|
'Les notifications de bureau ne sont pas discponibles sur votre navigateur. Essayez Firefox ou Chrome.',
|
||||||
unread_messages: 'Messages non-lu',
|
unread_messages: 'Messages non-lu',
|
||||||
|
messages: 'Messages',
|
||||||
password: 'Mot de passe',
|
password: 'Mot de passe',
|
||||||
verify_password: 'Vérifiez le mot de passe',
|
verify_password: 'Vérifiez le mot de passe',
|
||||||
|
old_password: 'Ancien mot de passe',
|
||||||
|
forgot_password: 'Mot de passe oublié',
|
||||||
|
reset_password_mail_sent: 'Un email a été envoyé pour réinitialiser votre mot de passe.',
|
||||||
|
password_change: 'Changement de mot de passe',
|
||||||
|
new_password: 'Nouveau mot de passe',
|
||||||
|
no_email_setup: "Ce serveur n'a pas correctement configuré la messagerie de email.",
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
|
matrix_user_id: 'Utilisateur Matrix',
|
||||||
|
private_message_disclaimer:
|
||||||
|
"Attention: les messages privés en Matrix ne sont pas sécurisés. S'il vous plait, créer un compte de <1>Riot.im</1> pour des messages sécurisés.",
|
||||||
|
send_notifications_to_email: 'Envoyer des notifications par email',
|
||||||
optional: 'Optionnel',
|
optional: 'Optionnel',
|
||||||
expires: 'Expire',
|
expires: 'Expire',
|
||||||
|
language: 'Langue',
|
||||||
|
browser_default: 'Défaut pour le navigateur',
|
||||||
|
downvotes_disabled: 'Votes négatifs désactivés',
|
||||||
|
enable_downvotes: 'Votes négatifs activés',
|
||||||
|
open_registration: 'Ouvrir la regestration',
|
||||||
|
registration_closed: 'Régestration fermée',
|
||||||
|
enable_nsfw: 'Activer NSFW',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
body: 'Texte',
|
body: 'Texte',
|
||||||
copy_suggested_title: 'Ajouter le titre suggéré: {{title}}',
|
copy_suggested_title: 'Ajouter le titre suggéré: {{title}}',
|
||||||
|
@ -139,8 +171,10 @@ export const fr = {
|
||||||
sponsor_message:
|
sponsor_message:
|
||||||
"Lemmy est gratuit et <1>open-source</1>, c'est à dire sans publicité et sans monétisation. Pour toujours. Vos dons soutiennent directement le développement du projet. Merci à nos soutiens.",
|
"Lemmy est gratuit et <1>open-source</1>, c'est à dire sans publicité et sans monétisation. Pour toujours. Vos dons soutiennent directement le développement du projet. Merci à nos soutiens.",
|
||||||
support_on_patreon: 'Soutenir sur Patreon',
|
support_on_patreon: 'Soutenir sur Patreon',
|
||||||
general_sponsors:
|
support_on_liberapay: 'Soutenir sur Liberapay',
|
||||||
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
donate_to_lemmy: 'Faire un don à Lemmy',
|
||||||
|
donate: 'Faire un don',
|
||||||
|
general_sponsors: 'Les sponsors généraux sont ceux garantissant de 10 à 39$.',
|
||||||
crypto: 'Cryptomonnaies',
|
crypto: 'Cryptomonnaies',
|
||||||
bitcoin: 'Bitcoin',
|
bitcoin: 'Bitcoin',
|
||||||
ethereum: 'Ethereum',
|
ethereum: 'Ethereum',
|
||||||
|
@ -149,6 +183,7 @@ export const fr = {
|
||||||
joined: 'Membre depuis',
|
joined: 'Membre depuis',
|
||||||
by: 'par',
|
by: 'par',
|
||||||
to: 'vers',
|
to: 'vers',
|
||||||
|
from: 'de',
|
||||||
transfer_community: 'transférer la communauté',
|
transfer_community: 'transférer la communauté',
|
||||||
transfer_site: 'transférer le site',
|
transfer_site: 'transférer le site',
|
||||||
are_you_sure: 'Êtes-vous sûr ?',
|
are_you_sure: 'Êtes-vous sûr ?',
|
||||||
|
@ -158,12 +193,14 @@ export const fr = {
|
||||||
landing_0:
|
landing_0:
|
||||||
'Lemmy est un <1>aggrégateur de lien</1>, similaire à reddit et conçu pour fonctionner sur le <2>fédiverse</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via Activitypub est prévue. <5></5>Lemmy est une <6>version beta très précoce</6>, et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez rapporter des bugs et suggérez de nouvelles fonctionnalités <8>ici.</8><9></9>Crée avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
'Lemmy est un <1>aggrégateur de lien</1>, similaire à reddit et conçu pour fonctionner sur le <2>fédiverse</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via Activitypub est prévue. <5></5>Lemmy est une <6>version beta très précoce</6>, et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez rapporter des bugs et suggérez de nouvelles fonctionnalités <8>ici.</8><9></9>Crée avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||||
not_logged_in: "Vous n'êtes pas connecté.",
|
not_logged_in: "Vous n'êtes pas connecté.",
|
||||||
|
logged_in: 'Vous êtes connecté.',
|
||||||
community_ban: 'Vous avez été banni de cette communauté.',
|
community_ban: 'Vous avez été banni de cette communauté.',
|
||||||
site_ban: 'Vous avez été banni du site',
|
site_ban: 'Vous avez été banni du site',
|
||||||
couldnt_create_comment: 'Impossible de poster le commentaire.',
|
couldnt_create_comment: 'Impossible de poster le commentaire.',
|
||||||
couldnt_like_comment: "Impossible d'aimer le commentaire.",
|
couldnt_like_comment: "Impossible d'aimer le commentaire.",
|
||||||
couldnt_update_comment: 'Impossible de mettre à jour le commentaire.',
|
couldnt_update_comment: 'Impossible de mettre à jour le commentaire.',
|
||||||
couldnt_save_comment: 'Impossible de sauvegarder le commentaire.',
|
couldnt_save_comment: 'Impossible de sauvegarder le commentaire.',
|
||||||
|
couldnt_get_comments: 'Impossible de obtenir les commentaires.',
|
||||||
no_comment_edit_allowed:
|
no_comment_edit_allowed:
|
||||||
"Vous n'êtes pas autorisé à éditer ce commentaire.",
|
"Vous n'êtes pas autorisé à éditer ce commentaire.",
|
||||||
no_post_edit_allowed: "Vous n'êtes pas autorisé à éditer sujet.",
|
no_post_edit_allowed: "Vous n'êtes pas autorisé à éditer sujet.",
|
||||||
|
@ -176,6 +213,7 @@ export const fr = {
|
||||||
community_follower_already_exists: 'Ce membre est déjà abonné.',
|
community_follower_already_exists: 'Ce membre est déjà abonné.',
|
||||||
community_user_already_banned: 'Ce membre est déjà banni.',
|
community_user_already_banned: 'Ce membre est déjà banni.',
|
||||||
couldnt_create_post: 'Impossible de créer le sujet.',
|
couldnt_create_post: 'Impossible de créer le sujet.',
|
||||||
|
post_title_too_long: 'Sujet titre trop long.',
|
||||||
couldnt_like_post: "Impossible d'aimer le sujet.",
|
couldnt_like_post: "Impossible d'aimer le sujet.",
|
||||||
couldnt_find_post: 'Impossible de trouver le sujet.',
|
couldnt_find_post: 'Impossible de trouver le sujet.',
|
||||||
couldnt_get_posts: "Impossible d'obtenir les sujets",
|
couldnt_get_posts: "Impossible d'obtenir les sujets",
|
||||||
|
@ -191,8 +229,14 @@ export const fr = {
|
||||||
passwords_dont_match: 'Les mots de passes ne correspondent pas..',
|
passwords_dont_match: 'Les mots de passes ne correspondent pas..',
|
||||||
admin_already_created: 'Désolé, il y a déjà un admin.',
|
admin_already_created: 'Désolé, il y a déjà un admin.',
|
||||||
user_already_exists: "L'utilisateur existe déjà.",
|
user_already_exists: "L'utilisateur existe déjà.",
|
||||||
|
email_already_exists: "L'email existe déjà",
|
||||||
couldnt_update_user: "Impossible de mettre à jour l'utilisateur.",
|
couldnt_update_user: "Impossible de mettre à jour l'utilisateur.",
|
||||||
system_err_login:
|
system_err_login:
|
||||||
'Erreur système. Essayez de vous déconneter puis de vous reconnecter.',
|
'Erreur système. Essayez de vous déconneter puis de vous reconnecter.',
|
||||||
|
couldnt_create_private_message: 'Impossible de créer un message privé.',
|
||||||
|
no_private_message_edit_allowed: 'Pas autorisé à modifier un message privé.',
|
||||||
|
couldnt_update_private_message: 'Impossible de modifier un message privé.',
|
||||||
|
time: 'Temps',
|
||||||
|
action: 'Action',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
1
ui/src/translations/it.ts
vendored
1
ui/src/translations/it.ts
vendored
|
@ -138,6 +138,7 @@ export const it = {
|
||||||
sponsors_of_lemmy: 'Sponsors di Lemmy',
|
sponsors_of_lemmy: 'Sponsors di Lemmy',
|
||||||
sponsor_message: 'Lemmy è un software gratuito e <1>open-source</1>, il che significa nessuna pubblicità, monetizzazione o investitori esterni, per sempre. Le tue donazioni supportano direttamente lo sviluppo full-time del progetto. Si ringraziano le seguenti persone:',
|
sponsor_message: 'Lemmy è un software gratuito e <1>open-source</1>, il che significa nessuna pubblicità, monetizzazione o investitori esterni, per sempre. Le tue donazioni supportano direttamente lo sviluppo full-time del progetto. Si ringraziano le seguenti persone:',
|
||||||
support_on_patreon: 'Supporta su Patreon',
|
support_on_patreon: 'Supporta su Patreon',
|
||||||
|
support_on_liberapay: 'Supporta su Liberapay',
|
||||||
general_sponsors: 'I "General Sponsors" sono quelli che hanno investito dai 10$ ai 39$ su Lemmy.',
|
general_sponsors: 'I "General Sponsors" sono quelli che hanno investito dai 10$ ai 39$ su Lemmy.',
|
||||||
crypto: 'Crypto',
|
crypto: 'Crypto',
|
||||||
bitcoin: 'Bitcoin',
|
bitcoin: 'Bitcoin',
|
||||||
|
|
1
ui/src/translations/nl.ts
vendored
1
ui/src/translations/nl.ts
vendored
|
@ -125,6 +125,7 @@ export const nl = {
|
||||||
sponsor_message:
|
sponsor_message:
|
||||||
'Lemmy is vrije, <1>open-source</1> software, dus zonder reclame, winstoogmerk en durfkapitaal, punt. Jouw donaties gaan direct naar de full-time-ontwikkeling van het project. Met veel dank aan de volgende mensen:',
|
'Lemmy is vrije, <1>open-source</1> software, dus zonder reclame, winstoogmerk en durfkapitaal, punt. Jouw donaties gaan direct naar de full-time-ontwikkeling van het project. Met veel dank aan de volgende mensen:',
|
||||||
support_on_patreon: 'Ondersteun op Patreon',
|
support_on_patreon: 'Ondersteun op Patreon',
|
||||||
|
support_on_liberapay: 'Ondersteun op Liberapay',
|
||||||
general_sponsors:
|
general_sponsors:
|
||||||
'Algemene sponsors zijn sponsors die tussen de $10 en $39 hebben gegeven aan Lemmy.',
|
'Algemene sponsors zijn sponsors die tussen de $10 en $39 hebben gegeven aan Lemmy.',
|
||||||
crypto: 'Cryptovaluta',
|
crypto: 'Cryptovaluta',
|
||||||
|
|
241
ui/src/translations/pt_br.ts
vendored
Normal file
241
ui/src/translations/pt_br.ts
vendored
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
export const pt_BR = {
|
||||||
|
translation: {
|
||||||
|
post: 'publicação',
|
||||||
|
remove_post: 'Apagar publicação',
|
||||||
|
no_posts: 'Sem publicações.',
|
||||||
|
create_a_post: 'Criar uma publicação',
|
||||||
|
create_post: 'Criar publicação',
|
||||||
|
number_of_posts: '{{count}} publicações',
|
||||||
|
posts: 'Publicações',
|
||||||
|
related_posts: 'Essas publicações podem estar relacionadas',
|
||||||
|
cross_posts: 'Esse link também foi publicado em:',
|
||||||
|
cross_post: 're-publicar',
|
||||||
|
cross_posted_to: 'Publicado também em: ',
|
||||||
|
comments: 'Comentários',
|
||||||
|
number_of_comments: '{{count}} comentários',
|
||||||
|
remove_comment: 'Apagar comentário',
|
||||||
|
communities: 'Comunidades',
|
||||||
|
users: 'Usuários',
|
||||||
|
create_a_community: 'Criar uma comunidade',
|
||||||
|
create_community: 'Criar comunidade',
|
||||||
|
remove_community: 'Apagar comunidade',
|
||||||
|
subscribed_to_communities: 'Inscrito em <1>comunidades</1>',
|
||||||
|
trending_communities: '<1>Comunidades</1> em tendência',
|
||||||
|
list_of_communities: 'Lista de comunidades',
|
||||||
|
number_of_communities: '{{count}} comunidades',
|
||||||
|
community_reqs: 'minúsculas, sublinhados e sem espaços.',
|
||||||
|
create_private_message: 'Criar mensagem privada',
|
||||||
|
send_secure_message: 'Enviar mensagem segura',
|
||||||
|
send_message: 'Enviar mensagem',
|
||||||
|
message: 'Mensagem',
|
||||||
|
edit: 'editar',
|
||||||
|
reply: 'responder',
|
||||||
|
cancel: 'Cancelar',
|
||||||
|
preview: 'Pré-visualização',
|
||||||
|
upload_image: 'fazer upload de imagem',
|
||||||
|
avatar: 'Avatar',
|
||||||
|
upload_avatar: 'Fazer upload de avatar',
|
||||||
|
show_avatars: 'Mostrar Avatars',
|
||||||
|
formatting_help: 'ajuda de formatação',
|
||||||
|
view_source: 'ver fonte',
|
||||||
|
unlock: 'desbloquear',
|
||||||
|
lock: 'bloquear',
|
||||||
|
sticky: 'fixar',
|
||||||
|
unsticky: 'desafixar',
|
||||||
|
link: 'link',
|
||||||
|
archive_link: 'arquivar link',
|
||||||
|
mod: 'moderador',
|
||||||
|
mods: 'moderadores',
|
||||||
|
moderates: 'Modera',
|
||||||
|
settings: 'Configurações',
|
||||||
|
remove_as_mod: 'remover como moderador',
|
||||||
|
appoint_as_mod: 'designar como moderador',
|
||||||
|
modlog: 'Registro de moderação',
|
||||||
|
admin: 'administrador',
|
||||||
|
admins: 'administradores',
|
||||||
|
remove_as_admin: 'remover como administrador',
|
||||||
|
appoint_as_admin: 'designar como administrador',
|
||||||
|
remove: 'remover',
|
||||||
|
removed: 'removido',
|
||||||
|
locked: 'trancado',
|
||||||
|
stickied: 'fixado',
|
||||||
|
reason: 'Motivo',
|
||||||
|
mark_as_read: 'marcar como lido',
|
||||||
|
mark_as_unread: 'marcar como não lido',
|
||||||
|
delete: 'apagar',
|
||||||
|
deleted: 'apagado',
|
||||||
|
delete_account: 'Apagar conta',
|
||||||
|
delete_account_confirm:
|
||||||
|
'Aviso: isso vai apagar seus dados de forma permanente. Escreva sua senha para confirmar.',
|
||||||
|
restore: 'restaurar',
|
||||||
|
ban: 'banir',
|
||||||
|
ban_from_site: 'banido do site',
|
||||||
|
unban: 'readmitido',
|
||||||
|
unban_from_site: 'readmitido ao site',
|
||||||
|
banned: 'banido',
|
||||||
|
save: 'guardar',
|
||||||
|
unsave: 'descartar',
|
||||||
|
create: 'criar',
|
||||||
|
creator: 'criador',
|
||||||
|
username: 'nome de usuário',
|
||||||
|
email_or_username: 'E-mail ou nome de usuário',
|
||||||
|
number_of_users: '{{count}} usuários',
|
||||||
|
number_of_subscribers: '{{count}} inscritos',
|
||||||
|
number_of_points: '{{count}} pontos',
|
||||||
|
number_online: '{{count}} usuários online',
|
||||||
|
name: 'Nome',
|
||||||
|
title: 'Título',
|
||||||
|
category: 'Categoria',
|
||||||
|
subscribers: 'Inscritos',
|
||||||
|
both: 'Ambos',
|
||||||
|
saved: 'Guardado',
|
||||||
|
unsubscribe: 'Cancelar inscrição',
|
||||||
|
subscribe: 'Inscrever-se',
|
||||||
|
subscribed: 'Inscrito',
|
||||||
|
prev: 'Anterior',
|
||||||
|
next: 'Próximo',
|
||||||
|
sidebar: 'Barra lateral',
|
||||||
|
sort_type: 'Ordenação',
|
||||||
|
hot: 'Popular',
|
||||||
|
new: 'Novo',
|
||||||
|
old: 'Velho',
|
||||||
|
top_day: 'Top do dia',
|
||||||
|
week: 'Semana',
|
||||||
|
month: 'Mês',
|
||||||
|
year: 'Ano',
|
||||||
|
all: 'Tudo',
|
||||||
|
top: 'Top',
|
||||||
|
api: 'API',
|
||||||
|
docs: 'Docs',
|
||||||
|
inbox: 'Caixa de entrada',
|
||||||
|
inbox_for: 'Caixa de entrada de <1>{{user}}</1>',
|
||||||
|
mark_all_as_read: 'marcar tudo como lido',
|
||||||
|
type: 'Tipo',
|
||||||
|
unread: 'Não lido',
|
||||||
|
replies: 'Respostas',
|
||||||
|
mentions: 'Menções',
|
||||||
|
reply_sent: 'Resposta enviada',
|
||||||
|
message_sent: 'Mensagem enviada',
|
||||||
|
search: 'Busca',
|
||||||
|
overview: 'Visão geral',
|
||||||
|
view: 'Visualização',
|
||||||
|
logout: 'Sair',
|
||||||
|
login_sign_up: 'Entrar / Inscrever-se',
|
||||||
|
login: 'Entrar',
|
||||||
|
sign_up: 'Inscrever-se',
|
||||||
|
notifications_error:
|
||||||
|
'Seu navegador não oferece notificações para a área de trabalho. Tente o Firefox ou o Chrome.',
|
||||||
|
unread_messages: 'Mensagens não lidas',
|
||||||
|
messages: 'Mensagens',
|
||||||
|
password: 'Senha',
|
||||||
|
verify_password: 'Verifique a senha',
|
||||||
|
old_password: 'Senha antiga',
|
||||||
|
forgot_password: 'esqueci a senha',
|
||||||
|
reset_password_mail_sent: 'Enviado um e-mail para a alteração da senha.',
|
||||||
|
password_change: 'Alteração de senha',
|
||||||
|
new_password: 'Nova senha',
|
||||||
|
no_email_setup: 'Esse servidor não configurou corretamente o e-mail.',
|
||||||
|
email: 'E-mail',
|
||||||
|
matrix_user_id: 'Usuário Matrix',
|
||||||
|
private_message_disclaimer:
|
||||||
|
'Aviso: mensagens privadas no Lemmy não são seguras. Crie uma conta em <1>Riot.im</1> para troca segura de mensagens.',
|
||||||
|
send_notifications_to_email: 'Enviar notificações para o e-mail',
|
||||||
|
optional: 'Opcional',
|
||||||
|
expires: 'Expira',
|
||||||
|
language: 'Idioma',
|
||||||
|
browser_default: 'Padrão do navegador',
|
||||||
|
downvotes_disabled: 'Votos negativos desativados',
|
||||||
|
enable_downvotes: 'Permitir votos negativos',
|
||||||
|
open_registration: 'Permitir registro',
|
||||||
|
registration_closed: 'Registros desativados',
|
||||||
|
enable_nsfw: 'Permitir NSFW',
|
||||||
|
url: 'URL',
|
||||||
|
body: 'Conteúdo',
|
||||||
|
copy_suggested_title: 'copiar título sugerido: {{title}}',
|
||||||
|
community: 'Comunidade',
|
||||||
|
expand_here: 'Expandir aqui',
|
||||||
|
subscribe_to_communities: 'Inscreva-se em algumas <1>comunidades</1>.',
|
||||||
|
chat: 'Chat',
|
||||||
|
recent_comments: 'Últimos comentários',
|
||||||
|
no_results: 'Nenhum resultado.',
|
||||||
|
setup: 'Instalação',
|
||||||
|
lemmy_instance_setup: 'Criação de instância Lemmy',
|
||||||
|
setup_admin: 'Configurar administrador do site',
|
||||||
|
your_site: 'seu site',
|
||||||
|
modified: 'modificado',
|
||||||
|
nsfw: 'NSFW',
|
||||||
|
show_nsfw: 'Mostrar conteúdo NSFW',
|
||||||
|
theme: 'Tema',
|
||||||
|
sponsors: 'Patrocinadores',
|
||||||
|
sponsors_of_lemmy: 'Patrocinadores do Lemmy',
|
||||||
|
sponsor_message:
|
||||||
|
'Lemmy é um programa livre e de código aberto, o que significa que não haverá publicidade, monetização ou capital de risco, jamais. Suas doações apoiam de forma direta o desenvolvimento em tempo integral do projeto. Muitos agradecimentos às sequintes pessoas:',
|
||||||
|
support_on_patreon: 'Colabore no Patreon',
|
||||||
|
support_on_liberapay: 'Colabore no Liberapay',
|
||||||
|
donate_to_lemmy: 'Faça uma doação ao Lemmy',
|
||||||
|
donate: 'Doar',
|
||||||
|
general_sponsors:
|
||||||
|
'Patrocinadores são aqueles que doaram entre $10 e $39 ao Lemmy.',
|
||||||
|
crypto: 'Crypto',
|
||||||
|
bitcoin: 'Bitcoin',
|
||||||
|
ethereum: 'Ethereum',
|
||||||
|
monero: 'Monero',
|
||||||
|
code: 'Code',
|
||||||
|
joined: 'Entrou',
|
||||||
|
by: 'por',
|
||||||
|
to: 'para',
|
||||||
|
from: 'de',
|
||||||
|
transfer_community: 'transferir comunidade',
|
||||||
|
transfer_site: 'transferir site',
|
||||||
|
are_you_sure: 'tem certeza?',
|
||||||
|
yes: 'sim',
|
||||||
|
no: 'não',
|
||||||
|
powered_by: 'Powered by',
|
||||||
|
landing_0:
|
||||||
|
'Lemmy é um <1>agregador de links</1> / alternativa ao reddit, com a intenção de funcionar junto ao <2>fediverso</2>.<3></3>Pode ser hospedado em servidor próprio, tem atualização de comentários em tempo real e é minúsculo (<4>~80kB</4>). A federação com a rede ActivityPub está no roteiro do projeto. <5></5>Esta é uma <6>versão beta bastante antecipada</6>, e muitas funcionalidades ainda estão quebradas ou ausentes. <7></7>Sugira novas funcionalidades ou reporte erros <8>aqui.</8><9></9>Feito com <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||||
|
not_logged_in: 'Não autenticado.',
|
||||||
|
logged_in: 'Autenticado.',
|
||||||
|
community_ban: 'Você foi banido desta comunidade.',
|
||||||
|
site_ban: 'Você foi banido do site',
|
||||||
|
couldnt_create_comment: 'Não foi possível criar o comentário.',
|
||||||
|
couldnt_like_comment: 'Não foi possível curtir o comentário.',
|
||||||
|
couldnt_update_comment: 'Não foi possível atualizar o comentário.',
|
||||||
|
couldnt_save_comment: 'Não foi possível guardar o comentário.',
|
||||||
|
no_comment_edit_allowed: 'Sem permissão para editar de comentário.',
|
||||||
|
no_post_edit_allowed: 'Sem permissão para editar publicação.',
|
||||||
|
no_community_edit_allowed: 'Sem permissão para editar comunidade.',
|
||||||
|
couldnt_find_community: 'Não foi possível encontrar a comunidade.',
|
||||||
|
couldnt_update_community: 'Não foi possível atualizar a comunidade.',
|
||||||
|
community_already_exists: 'Esta comunidade já existe.',
|
||||||
|
community_moderator_already_exists:
|
||||||
|
'Este moderador da comunidade já existe.',
|
||||||
|
community_follower_already_exists: 'Este seguidor da comunidade já existe.',
|
||||||
|
community_user_already_banned: 'Este usuário da comunidade já foi banido.',
|
||||||
|
couldnt_create_post: 'Não foi possível criar a publicação.',
|
||||||
|
couldnt_like_post: 'Não foi possível curtir a publicação.',
|
||||||
|
couldnt_find_post: 'Não foi possível encontrar a publicação.',
|
||||||
|
couldnt_get_posts: 'Não foi possível obter as publicações',
|
||||||
|
couldnt_update_post: 'Não foi possível atualizar a publicação',
|
||||||
|
couldnt_save_post: 'Não foi possível guardar a publicação.',
|
||||||
|
no_slurs: 'Sem insultos.',
|
||||||
|
not_an_admin: 'Não é administrador.',
|
||||||
|
site_already_exists: 'O site já existe.',
|
||||||
|
couldnt_update_site: 'Não foi possível atualizar o site.',
|
||||||
|
couldnt_find_that_username_or_email:
|
||||||
|
'Não foi possível encontrar esse usuário ou e-mail.',
|
||||||
|
password_incorrect: 'Senha incorreta.',
|
||||||
|
passwords_dont_match: 'As senhas não são iguais.',
|
||||||
|
admin_already_created: 'Desculpe, já há um administrador.',
|
||||||
|
user_already_exists: 'Este usuário já existe.',
|
||||||
|
email_already_exists: 'Este e-mail já existe.',
|
||||||
|
couldnt_update_user: 'Não foi possível atualizar o usuário.',
|
||||||
|
system_err_login: 'Erro no sistema. Tente sair e autenticar-se outra vez.',
|
||||||
|
couldnt_create_private_message: 'Não foi possível criar mensagem privada.',
|
||||||
|
no_private_message_edit_allowed:
|
||||||
|
'Sem permissão para editar mensagem privada.',
|
||||||
|
couldnt_update_private_message:
|
||||||
|
'Não foi possível atualizar a mensagem privada.',
|
||||||
|
time: 'Tempo',
|
||||||
|
action: 'Ação',
|
||||||
|
},
|
||||||
|
};
|
1
ui/src/translations/zh.ts
vendored
1
ui/src/translations/zh.ts
vendored
|
@ -116,6 +116,7 @@ export const zh = {
|
||||||
sponsor_message:
|
sponsor_message:
|
||||||
'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
|
'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
|
||||||
support_on_patreon: '在 Patreon 赞助',
|
support_on_patreon: '在 Patreon 赞助',
|
||||||
|
support_on_liberapay: '在 on 赞助',
|
||||||
general_sponsors:
|
general_sponsors:
|
||||||
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
||||||
crypto: '加密',
|
crypto: '加密',
|
||||||
|
|
244
ui/src/utils.ts
vendored
244
ui/src/utils.ts
vendored
|
@ -10,19 +10,26 @@ import 'moment/locale/it';
|
||||||
import 'moment/locale/fi';
|
import 'moment/locale/fi';
|
||||||
import 'moment/locale/ca';
|
import 'moment/locale/ca';
|
||||||
import 'moment/locale/fa';
|
import 'moment/locale/fa';
|
||||||
|
import 'moment/locale/pt-br';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UserOperation,
|
UserOperation,
|
||||||
Comment,
|
Comment,
|
||||||
|
CommentNode,
|
||||||
|
Post,
|
||||||
PrivateMessage,
|
PrivateMessage,
|
||||||
User,
|
User,
|
||||||
SortType,
|
SortType,
|
||||||
|
CommentSortType,
|
||||||
ListingType,
|
ListingType,
|
||||||
|
DataType,
|
||||||
SearchType,
|
SearchType,
|
||||||
WebSocketResponse,
|
WebSocketResponse,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
SearchForm,
|
SearchForm,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
|
CommentResponse,
|
||||||
|
PostResponse,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
import { UserService, WebSocketService } from './services';
|
import { UserService, WebSocketService } from './services';
|
||||||
|
|
||||||
|
@ -35,7 +42,7 @@ import emojiShortName from 'emoji-short-name';
|
||||||
import Toastify from 'toastify-js';
|
import Toastify from 'toastify-js';
|
||||||
|
|
||||||
export const repoUrl = 'https://github.com/dessalines/lemmy';
|
export const repoUrl = 'https://github.com/dessalines/lemmy';
|
||||||
export const markdownHelpUrl = 'https://commonmark.org/help/';
|
export const markdownHelpUrl = '/docs/about_guide.html';
|
||||||
export const archiveUrl = 'https://archive.is';
|
export const archiveUrl = 'https://archive.is';
|
||||||
|
|
||||||
export const postRefetchSeconds: number = 60 * 1000;
|
export const postRefetchSeconds: number = 60 * 1000;
|
||||||
|
@ -87,15 +94,22 @@ md.renderer.rules.emoji = function(token, idx) {
|
||||||
return twemoji.parse(token[idx].content);
|
return twemoji.parse(token[idx].content);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function hotRank(comment: Comment): number {
|
export function hotRankComment(comment: Comment): number {
|
||||||
// Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
|
return hotRank(comment.score, comment.published);
|
||||||
|
}
|
||||||
|
|
||||||
let date: Date = new Date(comment.published + 'Z'); // Add Z to convert from UTC date
|
export function hotRankPost(post: Post): number {
|
||||||
|
return hotRank(post.score, post.newest_activity_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hotRank(score: number, timeStr: string): number {
|
||||||
|
// Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
|
||||||
|
let date: Date = new Date(timeStr + 'Z'); // Add Z to convert from UTC date
|
||||||
let now: Date = new Date();
|
let now: Date = new Date();
|
||||||
let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
|
let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
|
||||||
|
|
||||||
let rank =
|
let rank =
|
||||||
(10000 * Math.log10(Math.max(1, 3 + comment.score))) /
|
(10000 * Math.log10(Math.max(1, 3 + score))) /
|
||||||
Math.pow(hoursElapsed + 2, 1.8);
|
Math.pow(hoursElapsed + 2, 1.8);
|
||||||
|
|
||||||
// console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`);
|
// console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`);
|
||||||
|
@ -197,14 +211,18 @@ export function routeListingTypeToEnum(type: string): ListingType {
|
||||||
return ListingType[capitalizeFirstLetter(type)];
|
return ListingType[capitalizeFirstLetter(type)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function routeDataTypeToEnum(type: string): DataType {
|
||||||
|
return DataType[capitalizeFirstLetter(type)];
|
||||||
|
}
|
||||||
|
|
||||||
export function routeSearchTypeToEnum(type: string): SearchType {
|
export function routeSearchTypeToEnum(type: string): SearchType {
|
||||||
return SearchType[capitalizeFirstLetter(type)];
|
return SearchType[capitalizeFirstLetter(type)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPageTitle(url: string) {
|
export async function getPageTitle(url: string) {
|
||||||
let res = await fetch(`https://textance.herokuapp.com/title/${url}`);
|
let res = await fetch(`/iframely/oembed?url=${url}`).then(res => res.json());
|
||||||
let data = await res.text();
|
let title = await res.title;
|
||||||
return data;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function debounce(
|
export function debounce(
|
||||||
|
@ -260,6 +278,7 @@ export const languages = [
|
||||||
{ code: 'es', name: 'Español' },
|
{ code: 'es', name: 'Español' },
|
||||||
{ code: 'de', name: 'Deutsch' },
|
{ code: 'de', name: 'Deutsch' },
|
||||||
{ code: 'fa', name: 'فارسی' },
|
{ code: 'fa', name: 'فارسی' },
|
||||||
|
{ code: 'pt_BR', name: 'Português Brasileiro' },
|
||||||
{ code: 'zh', name: '中文' },
|
{ code: 'zh', name: '中文' },
|
||||||
{ code: 'fi', name: 'Suomi' },
|
{ code: 'fi', name: 'Suomi' },
|
||||||
{ code: 'fr', name: 'Français' },
|
{ code: 'fr', name: 'Français' },
|
||||||
|
@ -310,6 +329,8 @@ export function getMomentLanguage(): string {
|
||||||
lang = 'ca';
|
lang = 'ca';
|
||||||
} else if (lang.startsWith('fa')) {
|
} else if (lang.startsWith('fa')) {
|
||||||
lang = 'fa';
|
lang = 'fa';
|
||||||
|
} else if (lang.startsWith('pt')) {
|
||||||
|
lang = 'pt-br';
|
||||||
} else {
|
} else {
|
||||||
lang = 'en';
|
lang = 'en';
|
||||||
}
|
}
|
||||||
|
@ -365,7 +386,7 @@ export function objectFlip(obj: any) {
|
||||||
export function pictshareAvatarThumbnail(src: string): string {
|
export function pictshareAvatarThumbnail(src: string): string {
|
||||||
// sample url: http://localhost:8535/pictshare/gs7xuu.jpg
|
// sample url: http://localhost:8535/pictshare/gs7xuu.jpg
|
||||||
let split = src.split('pictshare');
|
let split = src.split('pictshare');
|
||||||
let out = `${split[0]}pictshare/96x96${split[1]}`;
|
let out = `${split[0]}pictshare/96${split[1]}`;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -380,7 +401,7 @@ export function showAvatars(): boolean {
|
||||||
export function imageThumbnailer(url: string): string {
|
export function imageThumbnailer(url: string): string {
|
||||||
let split = url.split('pictshare');
|
let split = url.split('pictshare');
|
||||||
if (split.length > 1) {
|
if (split.length > 1) {
|
||||||
let out = `${split[0]}pictshare/140x140${split[1]}`;
|
let out = `${split[0]}pictshare/192${split[1]}`;
|
||||||
return out;
|
return out;
|
||||||
} else {
|
} else {
|
||||||
return url;
|
return url;
|
||||||
|
@ -515,3 +536,206 @@ function communitySearch(text: string, cb: any) {
|
||||||
cb([]);
|
cb([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getListingTypeFromProps(props: any): ListingType {
|
||||||
|
return props.match.params.listing_type
|
||||||
|
? routeListingTypeToEnum(props.match.params.listing_type)
|
||||||
|
: UserService.Instance.user
|
||||||
|
? UserService.Instance.user.default_listing_type
|
||||||
|
: ListingType.All;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO might need to add a user setting for this too
|
||||||
|
export function getDataTypeFromProps(props: any): DataType {
|
||||||
|
return props.match.params.data_type
|
||||||
|
? routeDataTypeToEnum(props.match.params.data_type)
|
||||||
|
: DataType.Post;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSortTypeFromProps(props: any): SortType {
|
||||||
|
return props.match.params.sort
|
||||||
|
? routeSortTypeToEnum(props.match.params.sort)
|
||||||
|
: UserService.Instance.user
|
||||||
|
? UserService.Instance.user.default_sort_type
|
||||||
|
: SortType.Hot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPageFromProps(props: any): number {
|
||||||
|
return props.match.params.page ? Number(props.match.params.page) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editCommentRes(
|
||||||
|
data: CommentResponse,
|
||||||
|
comments: Array<Comment>
|
||||||
|
) {
|
||||||
|
let found = comments.find(c => c.id == data.comment.id);
|
||||||
|
if (found) {
|
||||||
|
found.content = data.comment.content;
|
||||||
|
found.updated = data.comment.updated;
|
||||||
|
found.removed = data.comment.removed;
|
||||||
|
found.deleted = data.comment.deleted;
|
||||||
|
found.upvotes = data.comment.upvotes;
|
||||||
|
found.downvotes = data.comment.downvotes;
|
||||||
|
found.score = data.comment.score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveCommentRes(
|
||||||
|
data: CommentResponse,
|
||||||
|
comments: Array<Comment>
|
||||||
|
) {
|
||||||
|
let found = comments.find(c => c.id == data.comment.id);
|
||||||
|
if (found) {
|
||||||
|
found.saved = data.comment.saved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCommentLikeRes(
|
||||||
|
data: CommentResponse,
|
||||||
|
comments: Array<Comment>
|
||||||
|
) {
|
||||||
|
let found: Comment = comments.find(c => c.id === data.comment.id);
|
||||||
|
if (found) {
|
||||||
|
found.score = data.comment.score;
|
||||||
|
found.upvotes = data.comment.upvotes;
|
||||||
|
found.downvotes = data.comment.downvotes;
|
||||||
|
if (data.comment.my_vote !== null) {
|
||||||
|
found.my_vote = data.comment.my_vote;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPostLikeFindRes(data: PostResponse, posts: Array<Post>) {
|
||||||
|
let found = posts.find(c => c.id == data.post.id);
|
||||||
|
if (found) {
|
||||||
|
createPostLikeRes(data, found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPostLikeRes(data: PostResponse, post: Post) {
|
||||||
|
if (post) {
|
||||||
|
post.score = data.post.score;
|
||||||
|
post.upvotes = data.post.upvotes;
|
||||||
|
post.downvotes = data.post.downvotes;
|
||||||
|
if (data.post.my_vote !== null) {
|
||||||
|
post.my_vote = data.post.my_vote;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editPostFindRes(data: PostResponse, posts: Array<Post>) {
|
||||||
|
let found = posts.find(c => c.id == data.post.id);
|
||||||
|
if (found) {
|
||||||
|
editPostRes(data, found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editPostRes(data: PostResponse, post: Post) {
|
||||||
|
if (post) {
|
||||||
|
post.url = data.post.url;
|
||||||
|
post.name = data.post.name;
|
||||||
|
post.nsfw = data.post.nsfw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function commentsToFlatNodes(
|
||||||
|
comments: Array<Comment>
|
||||||
|
): Array<CommentNode> {
|
||||||
|
let nodes: Array<CommentNode> = [];
|
||||||
|
for (let comment of comments) {
|
||||||
|
nodes.push({ comment: comment });
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function commentSort(tree: Array<CommentNode>, sort: CommentSortType) {
|
||||||
|
// First, put removed and deleted comments at the bottom, then do your other sorts
|
||||||
|
if (sort == CommentSortType.Top) {
|
||||||
|
tree.sort(
|
||||||
|
(a, b) =>
|
||||||
|
+a.comment.removed - +b.comment.removed ||
|
||||||
|
+a.comment.deleted - +b.comment.deleted ||
|
||||||
|
b.comment.score - a.comment.score
|
||||||
|
);
|
||||||
|
} else if (sort == CommentSortType.New) {
|
||||||
|
tree.sort(
|
||||||
|
(a, b) =>
|
||||||
|
+a.comment.removed - +b.comment.removed ||
|
||||||
|
+a.comment.deleted - +b.comment.deleted ||
|
||||||
|
b.comment.published.localeCompare(a.comment.published)
|
||||||
|
);
|
||||||
|
} else if (sort == CommentSortType.Old) {
|
||||||
|
tree.sort(
|
||||||
|
(a, b) =>
|
||||||
|
+a.comment.removed - +b.comment.removed ||
|
||||||
|
+a.comment.deleted - +b.comment.deleted ||
|
||||||
|
a.comment.published.localeCompare(b.comment.published)
|
||||||
|
);
|
||||||
|
} else if (sort == CommentSortType.Hot) {
|
||||||
|
tree.sort(
|
||||||
|
(a, b) =>
|
||||||
|
+a.comment.removed - +b.comment.removed ||
|
||||||
|
+a.comment.deleted - +b.comment.deleted ||
|
||||||
|
hotRankComment(b.comment) - hotRankComment(a.comment)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go through the children recursively
|
||||||
|
for (let node of tree) {
|
||||||
|
if (node.children) {
|
||||||
|
commentSort(node.children, sort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function commentSortSortType(tree: Array<CommentNode>, sort: SortType) {
|
||||||
|
commentSort(tree, convertCommentSortType(sort));
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertCommentSortType(sort: SortType): CommentSortType {
|
||||||
|
if (
|
||||||
|
sort == SortType.TopAll ||
|
||||||
|
sort == SortType.TopDay ||
|
||||||
|
sort == SortType.TopWeek ||
|
||||||
|
sort == SortType.TopMonth ||
|
||||||
|
sort == SortType.TopYear
|
||||||
|
) {
|
||||||
|
return CommentSortType.Top;
|
||||||
|
} else if (sort == SortType.New) {
|
||||||
|
return CommentSortType.New;
|
||||||
|
} else if (sort == SortType.Hot) {
|
||||||
|
return CommentSortType.Hot;
|
||||||
|
} else {
|
||||||
|
return CommentSortType.Hot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postSort(posts: Array<Post>, sort: SortType) {
|
||||||
|
// First, put removed and deleted comments at the bottom, then do your other sorts
|
||||||
|
if (
|
||||||
|
sort == SortType.TopAll ||
|
||||||
|
sort == SortType.TopDay ||
|
||||||
|
sort == SortType.TopWeek ||
|
||||||
|
sort == SortType.TopMonth ||
|
||||||
|
sort == SortType.TopYear
|
||||||
|
) {
|
||||||
|
posts.sort(
|
||||||
|
(a, b) =>
|
||||||
|
+a.removed - +b.removed || +a.deleted - +b.deleted || b.score - a.score
|
||||||
|
);
|
||||||
|
} else if (sort == SortType.New) {
|
||||||
|
posts.sort(
|
||||||
|
(a, b) =>
|
||||||
|
+a.removed - +b.removed ||
|
||||||
|
+a.deleted - +b.deleted ||
|
||||||
|
b.published.localeCompare(a.published)
|
||||||
|
);
|
||||||
|
} else if (sort == SortType.Hot) {
|
||||||
|
posts.sort(
|
||||||
|
(a, b) =>
|
||||||
|
+a.removed - +b.removed ||
|
||||||
|
+a.deleted - +b.deleted ||
|
||||||
|
hotRankPost(b) - hotRankPost(a)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
2
ui/src/version.ts
vendored
2
ui/src/version.ts
vendored
|
@ -1 +1 @@
|
||||||
export const version: string = 'v0.6.11';
|
export const version: string = 'v0.6.25';
|
||||||
|
|
2
ui/translation_report.ts
vendored
2
ui/translation_report.ts
vendored
|
@ -11,6 +11,7 @@ import { nl } from './src/translations/nl';
|
||||||
import { it } from './src/translations/it';
|
import { it } from './src/translations/it';
|
||||||
import { fi } from './src/translations/fi';
|
import { fi } from './src/translations/fi';
|
||||||
import { ca } from './src/translations/ca';
|
import { ca } from './src/translations/ca';
|
||||||
|
import { pt_BR } from './src/translations/pt_br';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
const files = [
|
const files = [
|
||||||
|
@ -23,6 +24,7 @@ const files = [
|
||||||
{ t: fr, n: 'fr' },
|
{ t: fr, n: 'fr' },
|
||||||
{ t: it, n: 'it' },
|
{ t: it, n: 'it' },
|
||||||
{ t: nl, n: 'nl' },
|
{ t: nl, n: 'nl' },
|
||||||
|
{ t: pt_BR, n: 'pt-br' },
|
||||||
{ t: ru, n: 'ru' },
|
{ t: ru, n: 'ru' },
|
||||||
{ t: sv, n: 'sv' },
|
{ t: sv, n: 'sv' },
|
||||||
{ t: zh, n: 'zh' },
|
{ t: zh, n: 'zh' },
|
||||||
|
|
21
ui/yarn.lock
vendored
21
ui/yarn.lock
vendored
|
@ -1079,10 +1079,10 @@ emoji-regex@^8.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||||
|
|
||||||
emoji-short-name@^0.1.0:
|
emoji-short-name@^1.0.0:
|
||||||
version "0.1.4"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-short-name/-/emoji-short-name-0.1.4.tgz#125a452adc22a399b089f802f9d8d46ecb6e5b08"
|
resolved "https://registry.yarnpkg.com/emoji-short-name/-/emoji-short-name-1.0.0.tgz#82e6f543b6c68984d69bdc80eac735104fdd4af8"
|
||||||
integrity sha512-VTjEKkhN1UARtHLqlK70N5K3SwxuZAkmdm5sXvSjkV677kr0jt/O7mvB5eQqM+3rKCa+w3Qb5G7wwU/fezonKQ==
|
integrity sha512-+tiniHvgRR7XMI1jAaGveumWg5LALE/nWkFD6CcOn6M5IDM9w4PkMs8UwzLTMoZtDLdTdQmzxGvLOxHVIjPzjg==
|
||||||
|
|
||||||
encodeurl@~1.0.2:
|
encodeurl@~1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
|
@ -3076,6 +3076,11 @@ mkdirp@^0.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist "0.0.8"
|
minimist "0.0.8"
|
||||||
|
|
||||||
|
mobius1-selectr@^2.4.13:
|
||||||
|
version "2.4.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/mobius1-selectr/-/mobius1-selectr-2.4.13.tgz#0019dfd9f984840d6e40f70683ab3ec78ce3b5df"
|
||||||
|
integrity sha512-Mk9qDrvU44UUL0EBhbAA1phfQZ7aMZPjwtL7wkpiBzGh8dETGqfsh50mWoX9EkjDlkONlErWXArHCKfoxVg0Bw==
|
||||||
|
|
||||||
moment@^2.24.0:
|
moment@^2.24.0:
|
||||||
version "2.24.0"
|
version "2.24.0"
|
||||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
||||||
|
@ -3731,10 +3736,10 @@ realm-utils@^1.0.9:
|
||||||
app-root-path "^1.3.0"
|
app-root-path "^1.3.0"
|
||||||
mkdirp "^0.5.1"
|
mkdirp "^0.5.1"
|
||||||
|
|
||||||
reconnecting-websocket@^4.3.0:
|
reconnecting-websocket@^4.4.0:
|
||||||
version "4.3.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.3.0.tgz#aaefbc7629a89450aa45324b89aec2276e728cc5"
|
resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
|
||||||
integrity sha512-3eaHIEVYB9Zb0GfYy1xdEHKJLA2JaawAegByZ1AZ8Npb3AiRgUN5l89cvE2H+pHTsFcoC88t32ky9qET6DJ75Q==
|
integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
|
||||||
|
|
||||||
regenerate-unicode-properties@^8.1.0:
|
regenerate-unicode-properties@^8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
|
|
Loading…
Reference in a new issue