mirror of https://github.com/LemmyNet/lemmy.git
Merge branch 'master' into master
commit
bab917f1be
|
@ -58,7 +58,7 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins
|
||||||
|
|
||||||
## Why's it called Lemmy?
|
## Why's it called Lemmy?
|
||||||
|
|
||||||
- Lead singer from [motorhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
- 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 old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
||||||
- 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/).
|
||||||
|
@ -69,7 +69,7 @@ Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Infern
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
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
|
||||||
mkdir lemmy/
|
mkdir lemmy/
|
||||||
|
@ -80,7 +80,7 @@ wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/.env
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
and goto 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), could be setup with:
|
||||||
|
|
||||||
|
@ -100,8 +100,7 @@ docker-compose up -d
|
||||||
|
|
||||||
### Ansible
|
### Ansible
|
||||||
|
|
||||||
First, you need to [install Ansible on your local computer](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html),
|
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.
|
||||||
eg 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:
|
||||||
|
|
||||||
|
@ -142,13 +141,15 @@ If you used a `LoadBalancer`, you should see it in your cloud provider's console
|
||||||
|
|
||||||
### Docker Development
|
### Docker Development
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/dessalines/lemmy
|
git clone https://github.com/dessalines/lemmy
|
||||||
cd lemmy/docker/dev
|
cd lemmy/docker/dev
|
||||||
./docker_update.sh # This builds and runs it, updating for your changes
|
./docker_update.sh # This builds and runs it, updating for your changes
|
||||||
```
|
```
|
||||||
|
|
||||||
and goto http://localhost:8536
|
and go to http://localhost:8536.
|
||||||
|
|
||||||
### Local Development
|
### Local Development
|
||||||
|
|
||||||
|
@ -195,21 +196,28 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
If you'd like to add translations, take a look a look at the [english translation file](ui/src/translations/en.ts).
|
If you'd like to add translations, take a look a look at the [English translation file](ui/src/translations/en.ts).
|
||||||
|
|
||||||
- Languages supported: English (`en`), Chinese (`zh`), Dutch (`nl`), Esperanto (`eo`), French (`fr`), Spanish (`es`), Swedish (`sv`), German (`de`), Russian (`ru`).
|
- Languages supported: English (`en`), Chinese (`zh`), Dutch (`nl`), Esperanto (`eo`), French (`fr`), Spanish (`es`), Swedish (`sv`), German (`de`), Russian (`ru`).
|
||||||
|
|
||||||
lang | done | missing
|
lang | done | missing
|
||||||
--- | --- | ---
|
--- | --- | ---
|
||||||
de | 82% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,subscribed,expires,recent_comments,nsfw,show_nsfw,theme,crypto,monero,joined,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
de | 81% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,subscribed,replies,mentions,expires,recent_comments,nsfw,show_nsfw,theme,crypto,monero,joined,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
||||||
eo | 91% | number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,stickied,delete_account,delete_account_confirm,banned,creator,number_online,theme,are_you_sure,yes,no
|
eo | 90% | number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,theme,are_you_sure,yes,no
|
||||||
es | 100% |
|
es | 99% | replies,mentions
|
||||||
fr | 95% | view_source,sticky,unsticky,stickied,delete_account,delete_account_confirm,creator,number_online,theme
|
fr | 99% | replies,mentions
|
||||||
nl | 93% | preview,upload_image,formatting_help,view_source,sticky,unsticky,stickied,delete_account,delete_account_confirm,banned,creator,number_online,theme
|
nl | 92% | preview,upload_image,formatting_help,view_source,sticky,unsticky,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,theme
|
||||||
ru | 86% | cross_posts,cross_post,number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,stickied,delete_account,delete_account_confirm,banned,creator,number_online,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
ru | 86% | cross_posts,cross_post,number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
||||||
sv | 100% |
|
sv | 99% | replies,mentions
|
||||||
zh | 84% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
zh | 83% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
||||||
|
|
||||||
|
If you'd like to update this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ui
|
||||||
|
ts-node translation_report.ts > tmp # And replace the text above.
|
||||||
|
```
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
|
@ -10,7 +10,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- lemmy_db:/var/lib/postgresql/data
|
- lemmy_db:/var/lib/postgresql/data
|
||||||
lemmy:
|
lemmy:
|
||||||
image: dessalines/lemmy:v0.3.0.2
|
image: dessalines/lemmy:v0.3.0.7
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8536:8536"
|
- "127.0.0.1:8536:8536"
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -210,6 +210,7 @@ Only the first user will be able to be the admin.
|
||||||
{
|
{
|
||||||
op: "DeleteAccount",
|
op: "DeleteAccount",
|
||||||
data: {
|
data: {
|
||||||
|
password: String,
|
||||||
auth: String
|
auth: String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
drop view user_mention_view;
|
||||||
|
drop table user_mention;
|
|
@ -0,0 +1,35 @@
|
||||||
|
create table user_mention (
|
||||||
|
id serial primary key,
|
||||||
|
recipient_id int references user_ on update cascade on delete cascade not null,
|
||||||
|
comment_id int references comment on update cascade on delete cascade not null,
|
||||||
|
read boolean default false not null,
|
||||||
|
published timestamp not null default now(),
|
||||||
|
unique(recipient_id, comment_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
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.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;
|
|
@ -0,0 +1,2 @@
|
||||||
|
alter table user_ drop column default_sort_type;
|
||||||
|
alter table user_ drop column default_listing_type;
|
|
@ -0,0 +1,2 @@
|
||||||
|
alter table user_ add column default_sort_type smallint default 0 not null;
|
||||||
|
alter table user_ add column default_listing_type smallint default 1 not null;
|
|
@ -85,6 +85,35 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment"))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Scan the comment for user mentions, add those rows
|
||||||
|
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||||
|
|
||||||
|
for username_mention in &extracted_usernames {
|
||||||
|
let mention_user = User_::read_from_name(&conn, username_mention.to_string());
|
||||||
|
|
||||||
|
if mention_user.is_ok() {
|
||||||
|
let mention_user_id = mention_user?.id;
|
||||||
|
|
||||||
|
// You can't mention yourself
|
||||||
|
// At some point, make it so you can't tag the parent creator either
|
||||||
|
// This can cause two notifications, one for reply and the other for mention
|
||||||
|
if mention_user_id != user_id {
|
||||||
|
let user_mention_form = UserMentionForm {
|
||||||
|
recipient_id: mention_user_id,
|
||||||
|
comment_id: inserted_comment.id,
|
||||||
|
read: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow this to fail softly, since comment edits might re-update or replace it
|
||||||
|
// Let the uniqueness handle this fail
|
||||||
|
match UserMention::create(&conn, &user_mention_form) {
|
||||||
|
Ok(_mention) => (),
|
||||||
|
Err(_e) => eprintln!("{}", &_e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// You like your own comment by default
|
// You like your own comment by default
|
||||||
let like_form = CommentLikeForm {
|
let like_form = CommentLikeForm {
|
||||||
comment_id: inserted_comment.id,
|
comment_id: inserted_comment.id,
|
||||||
|
@ -170,6 +199,35 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Scan the comment for user mentions, add those rows
|
||||||
|
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||||
|
|
||||||
|
for username_mention in &extracted_usernames {
|
||||||
|
let mention_user = User_::read_from_name(&conn, username_mention.to_string());
|
||||||
|
|
||||||
|
if mention_user.is_ok() {
|
||||||
|
let mention_user_id = mention_user?.id;
|
||||||
|
|
||||||
|
// You can't mention yourself
|
||||||
|
// At some point, make it so you can't tag the parent creator either
|
||||||
|
// This can cause two notifications, one for reply and the other for mention
|
||||||
|
if mention_user_id != user_id {
|
||||||
|
let user_mention_form = UserMentionForm {
|
||||||
|
recipient_id: mention_user_id,
|
||||||
|
comment_id: data.edit_id,
|
||||||
|
read: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow this to fail softly, since comment edits might re-update or replace it
|
||||||
|
// Let the uniqueness handle this fail
|
||||||
|
match UserMention::create(&conn, &user_mention_form) {
|
||||||
|
Ok(_mention) => (),
|
||||||
|
Err(_e) => eprintln!("{}", &_e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
if let Some(removed) = data.removed.to_owned() {
|
if let Some(removed) = data.removed.to_owned() {
|
||||||
let form = ModRemoveCommentForm {
|
let form = ModRemoveCommentForm {
|
||||||
|
|
|
@ -8,9 +8,11 @@ use crate::db::moderator_views::*;
|
||||||
use crate::db::post::*;
|
use crate::db::post::*;
|
||||||
use crate::db::post_view::*;
|
use crate::db::post_view::*;
|
||||||
use crate::db::user::*;
|
use crate::db::user::*;
|
||||||
|
use crate::db::user_mention::*;
|
||||||
|
use crate::db::user_mention_view::*;
|
||||||
use crate::db::user_view::*;
|
use crate::db::user_view::*;
|
||||||
use crate::db::*;
|
use crate::db::*;
|
||||||
use crate::{has_slurs, naive_from_unix, naive_now, remove_slurs, Settings};
|
use crate::{extract_usernames, has_slurs, naive_from_unix, naive_now, remove_slurs, Settings};
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -43,6 +45,8 @@ pub enum UserOperation {
|
||||||
GetFollowedCommunities,
|
GetFollowedCommunities,
|
||||||
GetUserDetails,
|
GetUserDetails,
|
||||||
GetReplies,
|
GetReplies,
|
||||||
|
GetUserMentions,
|
||||||
|
EditUserMention,
|
||||||
GetModlog,
|
GetModlog,
|
||||||
BanFromCommunity,
|
BanFromCommunity,
|
||||||
AddModToCommunity,
|
AddModToCommunity,
|
||||||
|
|
|
@ -235,7 +235,7 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
|
||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let type_ = PostListingType::from_str(&data.type_)?;
|
let type_ = ListingType::from_str(&data.type_)?;
|
||||||
let sort = SortType::from_str(&data.sort)?;
|
let sort = SortType::from_str(&data.sort)?;
|
||||||
|
|
||||||
let posts = match PostView::list(
|
let posts = match PostView::list(
|
||||||
|
|
|
@ -321,7 +321,7 @@ impl Perform<SearchResponse> for Oper<Search> {
|
||||||
SearchType::Posts => {
|
SearchType::Posts => {
|
||||||
posts = PostView::list(
|
posts = PostView::list(
|
||||||
&conn,
|
&conn,
|
||||||
PostListingType::All,
|
ListingType::All,
|
||||||
&sort,
|
&sort,
|
||||||
data.community_id,
|
data.community_id,
|
||||||
None,
|
None,
|
||||||
|
@ -365,7 +365,7 @@ impl Perform<SearchResponse> for Oper<Search> {
|
||||||
SearchType::All => {
|
SearchType::All => {
|
||||||
posts = PostView::list(
|
posts = PostView::list(
|
||||||
&conn,
|
&conn,
|
||||||
PostListingType::All,
|
ListingType::All,
|
||||||
&sort,
|
&sort,
|
||||||
data.community_id,
|
data.community_id,
|
||||||
None,
|
None,
|
||||||
|
@ -403,7 +403,7 @@ impl Perform<SearchResponse> for Oper<Search> {
|
||||||
SearchType::Url => {
|
SearchType::Url => {
|
||||||
posts = PostView::list(
|
posts = PostView::list(
|
||||||
&conn,
|
&conn,
|
||||||
PostListingType::All,
|
ListingType::All,
|
||||||
&sort,
|
&sort,
|
||||||
data.community_id,
|
data.community_id,
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -22,6 +22,8 @@ pub struct Register {
|
||||||
pub struct SaveUserSettings {
|
pub struct SaveUserSettings {
|
||||||
show_nsfw: bool,
|
show_nsfw: bool,
|
||||||
theme: String,
|
theme: String,
|
||||||
|
default_sort_type: i16,
|
||||||
|
default_listing_type: i16,
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +62,12 @@ pub struct GetRepliesResponse {
|
||||||
replies: Vec<ReplyView>,
|
replies: Vec<ReplyView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GetUserMentionsResponse {
|
||||||
|
op: String,
|
||||||
|
mentions: Vec<UserMentionView>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct MarkAllAsRead {
|
pub struct MarkAllAsRead {
|
||||||
auth: String,
|
auth: String,
|
||||||
|
@ -103,6 +111,28 @@ pub struct GetReplies {
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GetUserMentions {
|
||||||
|
sort: String,
|
||||||
|
page: Option<i64>,
|
||||||
|
limit: Option<i64>,
|
||||||
|
unread_only: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct EditUserMention {
|
||||||
|
user_mention_id: i32,
|
||||||
|
read: Option<bool>,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct UserMentionResponse {
|
||||||
|
op: String,
|
||||||
|
mention: UserMentionView,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct DeleteAccount {
|
pub struct DeleteAccount {
|
||||||
password: String,
|
password: String,
|
||||||
|
@ -170,6 +200,8 @@ impl Perform<LoginResponse> for Oper<Register> {
|
||||||
banned: false,
|
banned: false,
|
||||||
show_nsfw: data.show_nsfw,
|
show_nsfw: data.show_nsfw,
|
||||||
theme: "darkly".into(),
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the user
|
// Create the user
|
||||||
|
@ -261,6 +293,8 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
||||||
banned: read_user.banned,
|
banned: read_user.banned,
|
||||||
show_nsfw: data.show_nsfw,
|
show_nsfw: data.show_nsfw,
|
||||||
theme: data.theme.to_owned(),
|
theme: data.theme.to_owned(),
|
||||||
|
default_sort_type: data.default_sort_type,
|
||||||
|
default_listing_type: data.default_listing_type,
|
||||||
};
|
};
|
||||||
|
|
||||||
let updated_user = match User_::update(&conn, user_id, &user_form) {
|
let updated_user = match User_::update(&conn, user_id, &user_form) {
|
||||||
|
@ -299,7 +333,6 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
//TODO add save
|
|
||||||
let sort = SortType::from_str(&data.sort)?;
|
let sort = SortType::from_str(&data.sort)?;
|
||||||
|
|
||||||
let user_details_id = match data.user_id {
|
let user_details_id = match data.user_id {
|
||||||
|
@ -319,7 +352,7 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
||||||
let posts = if data.saved_only {
|
let posts = if data.saved_only {
|
||||||
PostView::list(
|
PostView::list(
|
||||||
&conn,
|
&conn,
|
||||||
PostListingType::All,
|
ListingType::All,
|
||||||
&sort,
|
&sort,
|
||||||
data.community_id,
|
data.community_id,
|
||||||
None,
|
None,
|
||||||
|
@ -335,7 +368,7 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
||||||
} else {
|
} else {
|
||||||
PostView::list(
|
PostView::list(
|
||||||
&conn,
|
&conn,
|
||||||
PostListingType::All,
|
ListingType::All,
|
||||||
&sort,
|
&sort,
|
||||||
data.community_id,
|
data.community_id,
|
||||||
Some(user_details_id),
|
Some(user_details_id),
|
||||||
|
@ -426,6 +459,8 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
||||||
banned: read_user.banned,
|
banned: read_user.banned,
|
||||||
show_nsfw: read_user.show_nsfw,
|
show_nsfw: read_user.show_nsfw,
|
||||||
theme: read_user.theme,
|
theme: read_user.theme,
|
||||||
|
default_sort_type: read_user.default_sort_type,
|
||||||
|
default_listing_type: read_user.default_listing_type,
|
||||||
};
|
};
|
||||||
|
|
||||||
match User_::update(&conn, data.user_id, &user_form) {
|
match User_::update(&conn, data.user_id, &user_form) {
|
||||||
|
@ -485,6 +520,8 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
||||||
banned: data.ban,
|
banned: data.ban,
|
||||||
show_nsfw: read_user.show_nsfw,
|
show_nsfw: read_user.show_nsfw,
|
||||||
theme: read_user.theme,
|
theme: read_user.theme,
|
||||||
|
default_sort_type: read_user.default_sort_type,
|
||||||
|
default_listing_type: read_user.default_listing_type,
|
||||||
};
|
};
|
||||||
|
|
||||||
match User_::update(&conn, data.user_id, &user_form) {
|
match User_::update(&conn, data.user_id, &user_form) {
|
||||||
|
@ -541,7 +578,6 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
||||||
data.limit,
|
data.limit,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Return the jwt
|
|
||||||
Ok(GetRepliesResponse {
|
Ok(GetRepliesResponse {
|
||||||
op: self.op.to_string(),
|
op: self.op.to_string(),
|
||||||
replies: replies,
|
replies: replies,
|
||||||
|
@ -549,6 +585,71 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
|
||||||
|
fn perform(&self) -> Result<GetUserMentionsResponse, Error> {
|
||||||
|
let data: &GetUserMentions = &self.data;
|
||||||
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let sort = SortType::from_str(&data.sort)?;
|
||||||
|
|
||||||
|
let mentions = UserMentionView::get_mentions(
|
||||||
|
&conn,
|
||||||
|
user_id,
|
||||||
|
&sort,
|
||||||
|
data.unread_only,
|
||||||
|
data.page,
|
||||||
|
data.limit,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(GetUserMentionsResponse {
|
||||||
|
op: self.op.to_string(),
|
||||||
|
mentions: mentions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Perform<UserMentionResponse> for Oper<EditUserMention> {
|
||||||
|
fn perform(&self) -> Result<UserMentionResponse, Error> {
|
||||||
|
let data: &EditUserMention = &self.data;
|
||||||
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let user_mention = UserMention::read(&conn, data.user_mention_id)?;
|
||||||
|
|
||||||
|
let user_mention_form = UserMentionForm {
|
||||||
|
recipient_id: user_id,
|
||||||
|
comment_id: user_mention.comment_id,
|
||||||
|
read: data.read.to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _updated_user_mention =
|
||||||
|
match UserMention::update(&conn, user_mention.id, &user_mention_form) {
|
||||||
|
Ok(comment) => comment,
|
||||||
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?;
|
||||||
|
|
||||||
|
Ok(UserMentionResponse {
|
||||||
|
op: self.op.to_string(),
|
||||||
|
mention: user_mention_view,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||||
fn perform(&self) -> Result<GetRepliesResponse, Error> {
|
fn perform(&self) -> Result<GetRepliesResponse, Error> {
|
||||||
let data: &MarkAllAsRead = &self.data;
|
let data: &MarkAllAsRead = &self.data;
|
||||||
|
@ -581,11 +682,27 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let replies = ReplyView::get_replies(&conn, user_id, &SortType::New, true, Some(1), Some(999))?;
|
// Mentions
|
||||||
|
let mentions =
|
||||||
|
UserMentionView::get_mentions(&conn, user_id, &SortType::New, true, Some(1), Some(999))?;
|
||||||
|
|
||||||
|
for mention in &mentions {
|
||||||
|
let mention_form = UserMentionForm {
|
||||||
|
recipient_id: mention.to_owned().recipient_id,
|
||||||
|
comment_id: mention.to_owned().id,
|
||||||
|
read: Some(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _updated_mention =
|
||||||
|
match UserMention::update(&conn, mention.user_mention_id, &mention_form) {
|
||||||
|
Ok(mention) => mention,
|
||||||
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Ok(GetRepliesResponse {
|
Ok(GetRepliesResponse {
|
||||||
op: self.op.to_string(),
|
op: self.op.to_string(),
|
||||||
replies: replies,
|
replies: vec![],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -644,7 +761,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||||
// Posts
|
// Posts
|
||||||
let posts = PostView::list(
|
let posts = PostView::list(
|
||||||
&conn,
|
&conn,
|
||||||
PostListingType::All,
|
ListingType::All,
|
||||||
&SortType::New,
|
&SortType::New,
|
||||||
None,
|
None,
|
||||||
Some(user_id),
|
Some(user_id),
|
||||||
|
|
|
@ -55,6 +55,7 @@ impl User_ {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::User_;
|
use super::User_;
|
||||||
|
use crate::db::{ListingType, SortType};
|
||||||
use crate::naive_now;
|
use crate::naive_now;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -73,6 +74,8 @@ mod tests {
|
||||||
updated: None,
|
updated: None,
|
||||||
show_nsfw: false,
|
show_nsfw: false,
|
||||||
theme: "darkly".into(),
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
};
|
};
|
||||||
|
|
||||||
let person = expected_user.person();
|
let person = expected_user.person();
|
||||||
|
|
|
@ -179,6 +179,8 @@ mod tests {
|
||||||
updated: None,
|
updated: None,
|
||||||
show_nsfw: false,
|
show_nsfw: false,
|
||||||
theme: "darkly".into(),
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
|
@ -69,7 +69,6 @@ impl CommentView {
|
||||||
|
|
||||||
let (limit, offset) = limit_and_offset(page, limit);
|
let (limit, offset) = limit_and_offset(page, limit);
|
||||||
|
|
||||||
// TODO no limits here?
|
|
||||||
let mut query = comment_view.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
|
||||||
|
@ -265,6 +264,8 @@ mod tests {
|
||||||
updated: None,
|
updated: None,
|
||||||
show_nsfw: false,
|
show_nsfw: false,
|
||||||
theme: "darkly".into(),
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
|
@ -265,6 +265,8 @@ mod tests {
|
||||||
updated: None,
|
updated: None,
|
||||||
show_nsfw: false,
|
show_nsfw: false,
|
||||||
theme: "darkly".into(),
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
|
@ -14,6 +14,8 @@ pub mod moderator_views;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod post_view;
|
pub mod post_view;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
pub mod user_mention;
|
||||||
|
pub mod user_mention_view;
|
||||||
pub mod user_view;
|
pub mod user_view;
|
||||||
|
|
||||||
pub trait Crud<T> {
|
pub trait Crud<T> {
|
||||||
|
@ -104,6 +106,13 @@ pub enum SortType {
|
||||||
TopAll,
|
TopAll,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum ListingType {
|
||||||
|
All,
|
||||||
|
Subscribed,
|
||||||
|
Community,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
||||||
pub enum SearchType {
|
pub enum SearchType {
|
||||||
All,
|
All,
|
||||||
|
|
|
@ -447,6 +447,8 @@ mod tests {
|
||||||
updated: None,
|
updated: None,
|
||||||
show_nsfw: false,
|
show_nsfw: false,
|
||||||
theme: "darkly".into(),
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_mod = User_::create(&conn, &new_mod).unwrap();
|
let inserted_mod = User_::create(&conn, &new_mod).unwrap();
|
||||||
|
@ -462,6 +464,8 @@ mod tests {
|
||||||
updated: None,
|
updated: None,
|
||||||
show_nsfw: false,
|
show_nsfw: false,
|
||||||
theme: "darkly".into(),
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
|
@ -192,6 +192,8 @@ mod tests {
|
||||||
updated: None,
|
updated: None,
|
||||||
show_nsfw: false,
|
show_nsfw: false,
|
||||||
theme: "darkly".into(),
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
|
||||||
pub enum PostListingType {
|
|
||||||
All,
|
|
||||||
Subscribed,
|
|
||||||
Community,
|
|
||||||
}
|
|
||||||
|
|
||||||
// The faked schema since diesel doesn't do views
|
// The faked schema since diesel doesn't do views
|
||||||
table! {
|
table! {
|
||||||
post_view (id) {
|
post_view (id) {
|
||||||
|
@ -83,7 +76,7 @@ pub struct PostView {
|
||||||
impl PostView {
|
impl PostView {
|
||||||
pub fn list(
|
pub fn list(
|
||||||
conn: &PgConnection,
|
conn: &PgConnection,
|
||||||
type_: PostListingType,
|
type_: ListingType,
|
||||||
sort: &SortType,
|
sort: &SortType,
|
||||||
for_community_id: Option<i32>,
|
for_community_id: Option<i32>,
|
||||||
for_creator_id: Option<i32>,
|
for_creator_id: Option<i32>,
|
||||||
|
@ -129,7 +122,7 @@ impl PostView {
|
||||||
};
|
};
|
||||||
|
|
||||||
match type_ {
|
match type_ {
|
||||||
PostListingType::Subscribed => {
|
ListingType::Subscribed => {
|
||||||
query = query.filter(subscribed.eq(true));
|
query = query.filter(subscribed.eq(true));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -226,6 +219,8 @@ mod tests {
|
||||||
banned: false,
|
banned: false,
|
||||||
show_nsfw: false,
|
show_nsfw: false,
|
||||||
theme: "darkly".into(),
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
@ -351,7 +346,7 @@ mod tests {
|
||||||
|
|
||||||
let read_post_listings_with_user = PostView::list(
|
let read_post_listings_with_user = PostView::list(
|
||||||
&conn,
|
&conn,
|
||||||
PostListingType::Community,
|
ListingType::Community,
|
||||||
&SortType::New,
|
&SortType::New,
|
||||||
Some(inserted_community.id),
|
Some(inserted_community.id),
|
||||||
None,
|
None,
|
||||||
|
@ -367,7 +362,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let read_post_listings_no_user = PostView::list(
|
let read_post_listings_no_user = PostView::list(
|
||||||
&conn,
|
&conn,
|
||||||
PostListingType::Community,
|
ListingType::Community,
|
||||||
&SortType::New,
|
&SortType::New,
|
||||||
Some(inserted_community.id),
|
Some(inserted_community.id),
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -0,0 +1,345 @@
|
||||||
|
table! {
|
||||||
|
category (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Varchar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
comment (id) {
|
||||||
|
id -> Int4,
|
||||||
|
creator_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
parent_id -> Nullable<Int4>,
|
||||||
|
content -> Text,
|
||||||
|
removed -> Bool,
|
||||||
|
read -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
comment_like (id) {
|
||||||
|
id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
comment_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
score -> Int2,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
comment_saved (id) {
|
||||||
|
id -> Int4,
|
||||||
|
comment_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
community (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Varchar,
|
||||||
|
title -> Varchar,
|
||||||
|
description -> Nullable<Text>,
|
||||||
|
category_id -> Int4,
|
||||||
|
creator_id -> Int4,
|
||||||
|
removed -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
|
nsfw -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
community_follower (id) {
|
||||||
|
id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
community_moderator (id) {
|
||||||
|
id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
community_user_ban (id) {
|
||||||
|
id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_add (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
other_user_id -> Int4,
|
||||||
|
removed -> Nullable<Bool>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_add_community (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
other_user_id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
removed -> Nullable<Bool>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_ban (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
other_user_id -> Int4,
|
||||||
|
reason -> Nullable<Text>,
|
||||||
|
banned -> Nullable<Bool>,
|
||||||
|
expires -> Nullable<Timestamp>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_ban_from_community (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
other_user_id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
reason -> Nullable<Text>,
|
||||||
|
banned -> Nullable<Bool>,
|
||||||
|
expires -> Nullable<Timestamp>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_lock_post (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
locked -> Nullable<Bool>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_remove_comment (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
comment_id -> Int4,
|
||||||
|
reason -> Nullable<Text>,
|
||||||
|
removed -> Nullable<Bool>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_remove_community (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
reason -> Nullable<Text>,
|
||||||
|
removed -> Nullable<Bool>,
|
||||||
|
expires -> Nullable<Timestamp>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_remove_post (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
reason -> Nullable<Text>,
|
||||||
|
removed -> Nullable<Bool>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_sticky_post (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
stickied -> Nullable<Bool>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
post (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Varchar,
|
||||||
|
url -> Nullable<Text>,
|
||||||
|
body -> Nullable<Text>,
|
||||||
|
creator_id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
removed -> Bool,
|
||||||
|
locked -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
|
nsfw -> Bool,
|
||||||
|
stickied -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
post_like (id) {
|
||||||
|
id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
score -> Int2,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
post_read (id) {
|
||||||
|
id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
post_saved (id) {
|
||||||
|
id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
site (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Varchar,
|
||||||
|
description -> Nullable<Text>,
|
||||||
|
creator_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
user_ (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Varchar,
|
||||||
|
fedi_name -> Varchar,
|
||||||
|
preferred_username -> Nullable<Varchar>,
|
||||||
|
password_encrypted -> Text,
|
||||||
|
email -> Nullable<Text>,
|
||||||
|
icon -> Nullable<Bytea>,
|
||||||
|
admin -> Bool,
|
||||||
|
banned -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
show_nsfw -> Bool,
|
||||||
|
theme -> Varchar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
user_ban (id) {
|
||||||
|
id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
user_mention (id) {
|
||||||
|
id -> Int4,
|
||||||
|
recipient_id -> Int4,
|
||||||
|
comment_id -> Int4,
|
||||||
|
read -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
joinable!(comment -> post (post_id));
|
||||||
|
joinable!(comment -> user_ (creator_id));
|
||||||
|
joinable!(comment_like -> comment (comment_id));
|
||||||
|
joinable!(comment_like -> post (post_id));
|
||||||
|
joinable!(comment_like -> user_ (user_id));
|
||||||
|
joinable!(comment_saved -> comment (comment_id));
|
||||||
|
joinable!(comment_saved -> user_ (user_id));
|
||||||
|
joinable!(community -> category (category_id));
|
||||||
|
joinable!(community -> user_ (creator_id));
|
||||||
|
joinable!(community_follower -> community (community_id));
|
||||||
|
joinable!(community_follower -> user_ (user_id));
|
||||||
|
joinable!(community_moderator -> community (community_id));
|
||||||
|
joinable!(community_moderator -> user_ (user_id));
|
||||||
|
joinable!(community_user_ban -> community (community_id));
|
||||||
|
joinable!(community_user_ban -> user_ (user_id));
|
||||||
|
joinable!(mod_add_community -> community (community_id));
|
||||||
|
joinable!(mod_ban_from_community -> community (community_id));
|
||||||
|
joinable!(mod_lock_post -> post (post_id));
|
||||||
|
joinable!(mod_lock_post -> user_ (mod_user_id));
|
||||||
|
joinable!(mod_remove_comment -> comment (comment_id));
|
||||||
|
joinable!(mod_remove_comment -> user_ (mod_user_id));
|
||||||
|
joinable!(mod_remove_community -> community (community_id));
|
||||||
|
joinable!(mod_remove_community -> user_ (mod_user_id));
|
||||||
|
joinable!(mod_remove_post -> post (post_id));
|
||||||
|
joinable!(mod_remove_post -> user_ (mod_user_id));
|
||||||
|
joinable!(mod_sticky_post -> post (post_id));
|
||||||
|
joinable!(mod_sticky_post -> user_ (mod_user_id));
|
||||||
|
joinable!(post -> community (community_id));
|
||||||
|
joinable!(post -> user_ (creator_id));
|
||||||
|
joinable!(post_like -> post (post_id));
|
||||||
|
joinable!(post_like -> user_ (user_id));
|
||||||
|
joinable!(post_read -> post (post_id));
|
||||||
|
joinable!(post_read -> user_ (user_id));
|
||||||
|
joinable!(post_saved -> post (post_id));
|
||||||
|
joinable!(post_saved -> user_ (user_id));
|
||||||
|
joinable!(site -> user_ (creator_id));
|
||||||
|
joinable!(user_ban -> user_ (user_id));
|
||||||
|
joinable!(user_mention -> comment (comment_id));
|
||||||
|
joinable!(user_mention -> user_ (recipient_id));
|
||||||
|
|
||||||
|
allow_tables_to_appear_in_same_query!(
|
||||||
|
category,
|
||||||
|
comment,
|
||||||
|
comment_like,
|
||||||
|
comment_saved,
|
||||||
|
community,
|
||||||
|
community_follower,
|
||||||
|
community_moderator,
|
||||||
|
community_user_ban,
|
||||||
|
mod_add,
|
||||||
|
mod_add_community,
|
||||||
|
mod_ban,
|
||||||
|
mod_ban_from_community,
|
||||||
|
mod_lock_post,
|
||||||
|
mod_remove_comment,
|
||||||
|
mod_remove_community,
|
||||||
|
mod_remove_post,
|
||||||
|
mod_sticky_post,
|
||||||
|
post,
|
||||||
|
post_like,
|
||||||
|
post_read,
|
||||||
|
post_saved,
|
||||||
|
site,
|
||||||
|
user_,
|
||||||
|
user_ban,
|
||||||
|
user_mention,
|
||||||
|
);
|
|
@ -21,6 +21,8 @@ pub struct User_ {
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub show_nsfw: bool,
|
pub show_nsfw: bool,
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
|
pub default_sort_type: i16,
|
||||||
|
pub default_listing_type: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset, Clone)]
|
#[derive(Insertable, AsChangeset, Clone)]
|
||||||
|
@ -36,6 +38,8 @@ pub struct UserForm {
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub show_nsfw: bool,
|
pub show_nsfw: bool,
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
|
pub default_sort_type: i16,
|
||||||
|
pub default_listing_type: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Crud<UserForm> for User_ {
|
impl Crud<UserForm> for User_ {
|
||||||
|
@ -77,6 +81,8 @@ pub struct Claims {
|
||||||
pub iss: String,
|
pub iss: String,
|
||||||
pub show_nsfw: bool,
|
pub show_nsfw: bool,
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
|
pub default_sort_type: i16,
|
||||||
|
pub default_listing_type: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Claims {
|
impl Claims {
|
||||||
|
@ -98,6 +104,8 @@ impl User_ {
|
||||||
iss: self.fedi_name.to_owned(),
|
iss: self.fedi_name.to_owned(),
|
||||||
show_nsfw: self.show_nsfw,
|
show_nsfw: self.show_nsfw,
|
||||||
theme: self.theme.to_owned(),
|
theme: self.theme.to_owned(),
|
||||||
|
default_sort_type: self.default_sort_type,
|
||||||
|
default_listing_type: self.default_listing_type,
|
||||||
};
|
};
|
||||||
encode(
|
encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
|
@ -146,6 +154,8 @@ mod tests {
|
||||||
updated: None,
|
updated: None,
|
||||||
show_nsfw: false,
|
show_nsfw: false,
|
||||||
theme: "darkly".into(),
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
@ -164,6 +174,8 @@ mod tests {
|
||||||
updated: None,
|
updated: None,
|
||||||
show_nsfw: false,
|
show_nsfw: false,
|
||||||
theme: "darkly".into(),
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
};
|
};
|
||||||
|
|
||||||
let read_user = User_::read(&conn, inserted_user.id).unwrap();
|
let read_user = User_::read(&conn, inserted_user.id).unwrap();
|
||||||
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
use super::comment::Comment;
|
||||||
|
use super::*;
|
||||||
|
use crate::schema::user_mention;
|
||||||
|
|
||||||
|
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
|
#[belongs_to(Comment)]
|
||||||
|
#[table_name = "user_mention"]
|
||||||
|
pub struct UserMention {
|
||||||
|
pub id: i32,
|
||||||
|
pub recipient_id: i32,
|
||||||
|
pub comment_id: i32,
|
||||||
|
pub read: bool,
|
||||||
|
pub published: chrono::NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, AsChangeset, Clone)]
|
||||||
|
#[table_name = "user_mention"]
|
||||||
|
pub struct UserMentionForm {
|
||||||
|
pub recipient_id: i32,
|
||||||
|
pub comment_id: i32,
|
||||||
|
pub read: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Crud<UserMentionForm> for UserMention {
|
||||||
|
fn read(conn: &PgConnection, user_mention_id: i32) -> Result<Self, Error> {
|
||||||
|
use crate::schema::user_mention::dsl::*;
|
||||||
|
user_mention.find(user_mention_id).first::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(conn: &PgConnection, user_mention_id: i32) -> Result<usize, Error> {
|
||||||
|
use crate::schema::user_mention::dsl::*;
|
||||||
|
diesel::delete(user_mention.find(user_mention_id)).execute(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create(conn: &PgConnection, user_mention_form: &UserMentionForm) -> Result<Self, Error> {
|
||||||
|
use crate::schema::user_mention::dsl::*;
|
||||||
|
insert_into(user_mention)
|
||||||
|
.values(user_mention_form)
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
conn: &PgConnection,
|
||||||
|
user_mention_id: i32,
|
||||||
|
user_mention_form: &UserMentionForm,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::user_mention::dsl::*;
|
||||||
|
diesel::update(user_mention.find(user_mention_id))
|
||||||
|
.set(user_mention_form)
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::super::comment::*;
|
||||||
|
use super::super::community::*;
|
||||||
|
use super::super::post::*;
|
||||||
|
use super::super::user::*;
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn test_crud() {
|
||||||
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
let new_user = UserForm {
|
||||||
|
name: "terrylake".into(),
|
||||||
|
fedi_name: "rrf".into(),
|
||||||
|
preferred_username: None,
|
||||||
|
password_encrypted: "nope".into(),
|
||||||
|
email: None,
|
||||||
|
admin: false,
|
||||||
|
banned: false,
|
||||||
|
updated: None,
|
||||||
|
show_nsfw: false,
|
||||||
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
||||||
|
let recipient_form = UserForm {
|
||||||
|
name: "terrylakes recipient".into(),
|
||||||
|
fedi_name: "rrf".into(),
|
||||||
|
preferred_username: None,
|
||||||
|
password_encrypted: "nope".into(),
|
||||||
|
email: None,
|
||||||
|
admin: false,
|
||||||
|
banned: false,
|
||||||
|
updated: None,
|
||||||
|
show_nsfw: false,
|
||||||
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
|
||||||
|
|
||||||
|
let new_community = CommunityForm {
|
||||||
|
name: "test community lake".to_string(),
|
||||||
|
title: "nada".to_owned(),
|
||||||
|
description: None,
|
||||||
|
category_id: 1,
|
||||||
|
creator_id: inserted_user.id,
|
||||||
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
|
updated: None,
|
||||||
|
nsfw: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||||
|
|
||||||
|
let new_post = PostForm {
|
||||||
|
name: "A test post".into(),
|
||||||
|
creator_id: inserted_user.id,
|
||||||
|
url: None,
|
||||||
|
body: None,
|
||||||
|
community_id: inserted_community.id,
|
||||||
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
|
locked: None,
|
||||||
|
stickied: None,
|
||||||
|
updated: None,
|
||||||
|
nsfw: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_post = Post::create(&conn, &new_post).unwrap();
|
||||||
|
|
||||||
|
let comment_form = CommentForm {
|
||||||
|
content: "A test comment".into(),
|
||||||
|
creator_id: inserted_user.id,
|
||||||
|
post_id: inserted_post.id,
|
||||||
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
|
read: None,
|
||||||
|
parent_id: None,
|
||||||
|
updated: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
|
||||||
|
|
||||||
|
let user_mention_form = UserMentionForm {
|
||||||
|
recipient_id: inserted_recipient.id,
|
||||||
|
comment_id: inserted_comment.id,
|
||||||
|
read: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_mention = UserMention::create(&conn, &user_mention_form).unwrap();
|
||||||
|
|
||||||
|
let expected_mention = UserMention {
|
||||||
|
id: inserted_mention.id,
|
||||||
|
recipient_id: inserted_mention.recipient_id,
|
||||||
|
comment_id: inserted_mention.comment_id,
|
||||||
|
read: false,
|
||||||
|
published: inserted_mention.published,
|
||||||
|
};
|
||||||
|
|
||||||
|
let read_mention = UserMention::read(&conn, inserted_mention.id).unwrap();
|
||||||
|
let updated_mention =
|
||||||
|
UserMention::update(&conn, inserted_mention.id, &user_mention_form).unwrap();
|
||||||
|
let num_deleted = UserMention::delete(&conn, inserted_mention.id).unwrap();
|
||||||
|
Comment::delete(&conn, inserted_comment.id).unwrap();
|
||||||
|
Post::delete(&conn, inserted_post.id).unwrap();
|
||||||
|
Community::delete(&conn, inserted_community.id).unwrap();
|
||||||
|
User_::delete(&conn, inserted_user.id).unwrap();
|
||||||
|
User_::delete(&conn, inserted_recipient.id).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected_mention, read_mention);
|
||||||
|
assert_eq!(expected_mention, inserted_mention);
|
||||||
|
assert_eq!(expected_mention, updated_mention);
|
||||||
|
assert_eq!(1, num_deleted);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// The faked schema since diesel doesn't do views
|
||||||
|
table! {
|
||||||
|
user_mention_view (id) {
|
||||||
|
id -> Int4,
|
||||||
|
user_mention_id -> Int4,
|
||||||
|
creator_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
parent_id -> Nullable<Int4>,
|
||||||
|
content -> Text,
|
||||||
|
removed -> Bool,
|
||||||
|
read -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
|
community_id -> Int4,
|
||||||
|
banned -> Bool,
|
||||||
|
banned_from_community -> Bool,
|
||||||
|
creator_name -> Varchar,
|
||||||
|
score -> BigInt,
|
||||||
|
upvotes -> BigInt,
|
||||||
|
downvotes -> BigInt,
|
||||||
|
user_id -> Nullable<Int4>,
|
||||||
|
my_vote -> Nullable<Int4>,
|
||||||
|
saved -> Nullable<Bool>,
|
||||||
|
recipient_id -> Int4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
|
||||||
|
)]
|
||||||
|
#[table_name = "user_mention_view"]
|
||||||
|
pub struct UserMentionView {
|
||||||
|
pub id: i32,
|
||||||
|
pub user_mention_id: i32,
|
||||||
|
pub creator_id: i32,
|
||||||
|
pub post_id: i32,
|
||||||
|
pub parent_id: Option<i32>,
|
||||||
|
pub content: String,
|
||||||
|
pub removed: bool,
|
||||||
|
pub read: bool,
|
||||||
|
pub published: chrono::NaiveDateTime,
|
||||||
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub deleted: bool,
|
||||||
|
pub community_id: i32,
|
||||||
|
pub banned: bool,
|
||||||
|
pub banned_from_community: bool,
|
||||||
|
pub creator_name: String,
|
||||||
|
pub score: i64,
|
||||||
|
pub upvotes: i64,
|
||||||
|
pub downvotes: i64,
|
||||||
|
pub user_id: Option<i32>,
|
||||||
|
pub my_vote: Option<i32>,
|
||||||
|
pub saved: Option<bool>,
|
||||||
|
pub recipient_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserMentionView {
|
||||||
|
pub fn get_mentions(
|
||||||
|
conn: &PgConnection,
|
||||||
|
for_user_id: i32,
|
||||||
|
sort: &SortType,
|
||||||
|
unread_only: bool,
|
||||||
|
page: Option<i64>,
|
||||||
|
limit: Option<i64>,
|
||||||
|
) -> Result<Vec<Self>, Error> {
|
||||||
|
use super::user_mention_view::user_mention_view::dsl::*;
|
||||||
|
|
||||||
|
let (limit, offset) = limit_and_offset(page, limit);
|
||||||
|
|
||||||
|
let mut query = user_mention_view.into_boxed();
|
||||||
|
|
||||||
|
query = query
|
||||||
|
.filter(user_id.eq(for_user_id))
|
||||||
|
.filter(recipient_id.eq(for_user_id));
|
||||||
|
|
||||||
|
if unread_only {
|
||||||
|
query = query.filter(read.eq(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
query = match sort {
|
||||||
|
// SortType::Hot => query.order_by(hot_rank.desc()),
|
||||||
|
SortType::New => query.order_by(published.desc()),
|
||||||
|
SortType::TopAll => query.order_by(score.desc()),
|
||||||
|
SortType::TopYear => query
|
||||||
|
.filter(published.gt(now - 1.years()))
|
||||||
|
.order_by(score.desc()),
|
||||||
|
SortType::TopMonth => query
|
||||||
|
.filter(published.gt(now - 1.months()))
|
||||||
|
.order_by(score.desc()),
|
||||||
|
SortType::TopWeek => query
|
||||||
|
.filter(published.gt(now - 1.weeks()))
|
||||||
|
.order_by(score.desc()),
|
||||||
|
SortType::TopDay => query
|
||||||
|
.filter(published.gt(now - 1.days()))
|
||||||
|
.order_by(score.desc()),
|
||||||
|
_ => query.order_by(published.desc()),
|
||||||
|
};
|
||||||
|
|
||||||
|
query.limit(limit).offset(offset).load::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(
|
||||||
|
conn: &PgConnection,
|
||||||
|
from_user_mention_id: i32,
|
||||||
|
from_recipient_id: i32,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use super::user_mention_view::user_mention_view::dsl::*;
|
||||||
|
|
||||||
|
user_mention_view
|
||||||
|
.filter(user_mention_id.eq(from_user_mention_id))
|
||||||
|
.filter(user_id.eq(from_recipient_id))
|
||||||
|
.first::<Self>(conn)
|
||||||
|
}
|
||||||
|
}
|
|
@ -104,9 +104,23 @@ pub fn has_slurs(test: &str) -> bool {
|
||||||
SLUR_REGEX.is_match(test)
|
SLUR_REGEX.is_match(test)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn extract_usernames(test: &str) -> Vec<&str> {
|
||||||
|
let mut matches: Vec<&str> = USERNAME_MATCHES_REGEX
|
||||||
|
.find_iter(test)
|
||||||
|
.map(|mat| mat.as_str())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Unique
|
||||||
|
matches.sort_unstable();
|
||||||
|
matches.dedup();
|
||||||
|
|
||||||
|
// Remove /u/
|
||||||
|
matches.iter().map(|t| &t[3..]).collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{has_slurs, is_email_regex, remove_slurs, Settings};
|
use crate::{extract_usernames, has_slurs, is_email_regex, remove_slurs, Settings};
|
||||||
#[test]
|
#[test]
|
||||||
fn test_api() {
|
fn test_api() {
|
||||||
assert_eq!(Settings::get().api_endpoint(), "rrr/api/v1");
|
assert_eq!(Settings::get().api_endpoint(), "rrr/api/v1");
|
||||||
|
@ -131,9 +145,17 @@ mod tests {
|
||||||
assert!(has_slurs(&test));
|
assert!(has_slurs(&test));
|
||||||
assert!(!has_slurs(slur_free));
|
assert!(!has_slurs(slur_free));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_usernames() {
|
||||||
|
let usernames = extract_usernames("this is a user mention for [/u/testme](/u/testme) and thats all. Oh [/u/another](/u/another) user. And the first again [/u/testme](/u/testme) okay");
|
||||||
|
let expected = vec!["another", "testme"];
|
||||||
|
assert_eq!(usernames, expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
||||||
static ref SLUR_REGEX: Regex = Regex::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?s?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").unwrap();
|
static ref SLUR_REGEX: Regex = Regex::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?s?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").unwrap();
|
||||||
|
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -255,6 +255,8 @@ table! {
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
show_nsfw -> Bool,
|
show_nsfw -> Bool,
|
||||||
theme -> Varchar,
|
theme -> Varchar,
|
||||||
|
default_sort_type -> Int2,
|
||||||
|
default_listing_type -> Int2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,6 +268,16 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
user_mention (id) {
|
||||||
|
id -> Int4,
|
||||||
|
recipient_id -> Int4,
|
||||||
|
comment_id -> Int4,
|
||||||
|
read -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
joinable!(comment -> post (post_id));
|
joinable!(comment -> post (post_id));
|
||||||
joinable!(comment -> user_ (creator_id));
|
joinable!(comment -> user_ (creator_id));
|
||||||
joinable!(comment_like -> comment (comment_id));
|
joinable!(comment_like -> comment (comment_id));
|
||||||
|
@ -303,6 +315,8 @@ joinable!(post_saved -> post (post_id));
|
||||||
joinable!(post_saved -> user_ (user_id));
|
joinable!(post_saved -> user_ (user_id));
|
||||||
joinable!(site -> user_ (creator_id));
|
joinable!(site -> user_ (creator_id));
|
||||||
joinable!(user_ban -> user_ (user_id));
|
joinable!(user_ban -> user_ (user_id));
|
||||||
|
joinable!(user_mention -> comment (comment_id));
|
||||||
|
joinable!(user_mention -> user_ (recipient_id));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
allow_tables_to_appear_in_same_query!(
|
||||||
category,
|
category,
|
||||||
|
@ -329,4 +343,5 @@ allow_tables_to_appear_in_same_query!(
|
||||||
site,
|
site,
|
||||||
user_,
|
user_,
|
||||||
user_ban,
|
user_ban,
|
||||||
|
user_mention,
|
||||||
);
|
);
|
||||||
|
|
|
@ -136,7 +136,7 @@ impl ChatServer {
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
let posts = PostView::list(
|
let posts = PostView::list(
|
||||||
&conn,
|
&conn,
|
||||||
PostListingType::Community,
|
ListingType::Community,
|
||||||
&SortType::New,
|
&SortType::New,
|
||||||
Some(*community_id),
|
Some(*community_id),
|
||||||
None,
|
None,
|
||||||
|
@ -343,6 +343,16 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
let res = Oper::new(user_operation, get_replies).perform()?;
|
let res = Oper::new(user_operation, get_replies).perform()?;
|
||||||
Ok(serde_json::to_string(&res)?)
|
Ok(serde_json::to_string(&res)?)
|
||||||
}
|
}
|
||||||
|
UserOperation::GetUserMentions => {
|
||||||
|
let get_user_mentions: GetUserMentions = serde_json::from_str(data)?;
|
||||||
|
let res = Oper::new(user_operation, get_user_mentions).perform()?;
|
||||||
|
Ok(serde_json::to_string(&res)?)
|
||||||
|
}
|
||||||
|
UserOperation::EditUserMention => {
|
||||||
|
let edit_user_mention: EditUserMention = serde_json::from_str(data)?;
|
||||||
|
let res = Oper::new(user_operation, edit_user_mention).perform()?;
|
||||||
|
Ok(serde_json::to_string(&res)?)
|
||||||
|
}
|
||||||
UserOperation::MarkAllAsRead => {
|
UserOperation::MarkAllAsRead => {
|
||||||
let mark_all_as_read: MarkAllAsRead = serde_json::from_str(data)?;
|
let mark_all_as_read: MarkAllAsRead = serde_json::from_str(data)?;
|
||||||
let res = Oper::new(user_operation, mark_all_as_read).perform()?;
|
let res = Oper::new(user_operation, mark_all_as_read).perform()?;
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"browser": true
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"jane",
|
||||||
|
"inferno"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:jane/recommended",
|
||||||
|
"plugin:jane/typescript",
|
||||||
|
"plugin:inferno/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./tsconfig.json",
|
||||||
|
"warnOnUnsupportedTypeScriptVersion": false
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/camelcase": 0,
|
||||||
|
"@typescript-eslint/member-delimiter-style": 0,
|
||||||
|
"@typescript-eslint/no-empty-interface": 0,
|
||||||
|
"@typescript-eslint/no-explicit-any": 0,
|
||||||
|
"@typescript-eslint/no-this-alias": 0,
|
||||||
|
"@typescript-eslint/no-unused-vars": 0,
|
||||||
|
"@typescript-eslint/no-use-before-define": 0,
|
||||||
|
"@typescript-eslint/no-useless-constructor": 0,
|
||||||
|
"arrow-body-style": 0,
|
||||||
|
"curly": 0,
|
||||||
|
"eol-last": 0,
|
||||||
|
"eqeqeq": 0,
|
||||||
|
"func-style": 0,
|
||||||
|
"import/no-duplicates": 0,
|
||||||
|
"inferno/jsx-key": 0,
|
||||||
|
"inferno/jsx-no-target-blank": 0,
|
||||||
|
"inferno/jsx-props-class-name": 0,
|
||||||
|
"inferno/no-direct-mutation-state": 0,
|
||||||
|
"inferno/no-unknown-property": 0,
|
||||||
|
"max-statements": 0,
|
||||||
|
"new-cap": 0,
|
||||||
|
"no-console": 0,
|
||||||
|
"no-duplicate-imports": 0,
|
||||||
|
"no-extra-parens": 0,
|
||||||
|
"no-return-assign": 0,
|
||||||
|
"no-throw-literal": 0,
|
||||||
|
"no-trailing-spaces": 0,
|
||||||
|
"no-unused-expressions": 0,
|
||||||
|
"no-useless-constructor": 0,
|
||||||
|
"no-useless-escape": 0,
|
||||||
|
"no-var": 0,
|
||||||
|
"prefer-const": 0,
|
||||||
|
"prefer-rest-params": 0,
|
||||||
|
"quote-props": 0,
|
||||||
|
"unicorn/filename-case": 0
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = Object.assign(require('eslint-plugin-jane/prettier-ts'), {
|
||||||
|
arrowParens: 'avoid',
|
||||||
|
semi: true,
|
||||||
|
});
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,19 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "lemmy",
|
"name": "lemmy",
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "A simple UI for lemmy",
|
"description": "A simple UI for lemmy",
|
||||||
"main": "index.js",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
|
||||||
"start": "node fuse dev",
|
|
||||||
"build": "node fuse prod"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "Dessalines",
|
"author": "Dessalines",
|
||||||
"license": "GPL-2.0-or-later",
|
"license": "GPL-2.0-or-later",
|
||||||
"engines": {
|
"main": "index.js",
|
||||||
"node": ">=8.9.0"
|
"scripts": {
|
||||||
|
"build": "node fuse prod",
|
||||||
|
"lint": "eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
|
||||||
|
"start": "node fuse dev"
|
||||||
},
|
},
|
||||||
"engineStrict": true,
|
"keywords": [],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/autosize": "^3.0.6",
|
"@types/autosize": "^3.0.6",
|
||||||
"@types/js-cookie": "^2.2.1",
|
"@types/js-cookie": "^2.2.1",
|
||||||
|
@ -25,6 +22,7 @@
|
||||||
"classcat": "^1.1.3",
|
"classcat": "^1.1.3",
|
||||||
"dotenv": "^6.1.0",
|
"dotenv": "^6.1.0",
|
||||||
"emoji-short-name": "^0.1.0",
|
"emoji-short-name": "^0.1.0",
|
||||||
|
"husky": "^3.0.9",
|
||||||
"i18next": "^17.0.9",
|
"i18next": "^17.0.9",
|
||||||
"inferno": "^7.0.1",
|
"inferno": "^7.0.1",
|
||||||
"inferno-i18next": "nimbusec-oss/inferno-i18next",
|
"inferno-i18next": "nimbusec-oss/inferno-i18next",
|
||||||
|
@ -35,6 +33,7 @@
|
||||||
"markdown-it-container": "^2.0.0",
|
"markdown-it-container": "^2.0.0",
|
||||||
"markdown-it-emoji": "^1.4.0",
|
"markdown-it-emoji": "^1.4.0",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
|
"prettier": "^1.18.2",
|
||||||
"rxjs": "^6.4.0",
|
"rxjs": "^6.4.0",
|
||||||
"terser": "^3.17.0",
|
"terser": "^3.17.0",
|
||||||
"tributejs": "3.7.2",
|
"tributejs": "3.7.2",
|
||||||
|
@ -43,9 +42,34 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/i18next": "^12.1.0",
|
"@types/i18next": "^12.1.0",
|
||||||
|
"eslint": "^6.5.1",
|
||||||
|
"eslint-plugin-inferno": "^7.14.3",
|
||||||
|
"eslint-plugin-jane": "^7.0.0",
|
||||||
"fuse-box": "^3.1.3",
|
"fuse-box": "^3.1.3",
|
||||||
|
"lint-staged": "^9.4.2",
|
||||||
|
"sortpack": "^2.0.1",
|
||||||
"ts-transform-classcat": "^0.0.2",
|
"ts-transform-classcat": "^0.0.2",
|
||||||
"ts-transform-inferno": "^4.0.2",
|
"ts-transform-inferno": "^4.0.2",
|
||||||
"typescript": "^3.5.3"
|
"typescript": "^3.5.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.9.0"
|
||||||
|
},
|
||||||
|
"engineStrict": true,
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{ts,tsx,js}": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix",
|
||||||
|
"git add"
|
||||||
|
],
|
||||||
|
"package.json": [
|
||||||
|
"sortpack",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,22 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { CommentNode as CommentNodeI, CommentForm as CommentFormI, SearchForm, SearchType, SortType, UserOperation, SearchResponse } from '../interfaces';
|
import {
|
||||||
import { Subscription } from "rxjs";
|
CommentNode as CommentNodeI,
|
||||||
import { capitalizeFirstLetter, mentionDropdownFetchLimit, msgOp, mdToHtml, randomStr, markdownHelpUrl } from '../utils';
|
CommentForm as CommentFormI,
|
||||||
|
SearchForm,
|
||||||
|
SearchType,
|
||||||
|
SortType,
|
||||||
|
UserOperation,
|
||||||
|
SearchResponse,
|
||||||
|
} from '../interfaces';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import {
|
||||||
|
capitalizeFirstLetter,
|
||||||
|
mentionDropdownFetchLimit,
|
||||||
|
msgOp,
|
||||||
|
mdToHtml,
|
||||||
|
randomStr,
|
||||||
|
markdownHelpUrl,
|
||||||
|
} from '../utils';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import * as autosize from 'autosize';
|
import * as autosize from 'autosize';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
@ -25,7 +40,6 @@ interface CommentFormState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
|
|
||||||
private id = `comment-form-${randomStr()}`;
|
private id = `comment-form-${randomStr()}`;
|
||||||
private userSub: Subscription;
|
private userSub: Subscription;
|
||||||
private communitySub: Subscription;
|
private communitySub: Subscription;
|
||||||
|
@ -34,13 +48,21 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
commentForm: {
|
commentForm: {
|
||||||
auth: null,
|
auth: null,
|
||||||
content: null,
|
content: null,
|
||||||
post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId,
|
post_id: this.props.node
|
||||||
creator_id: UserService.Instance.user ? UserService.Instance.user.id : null,
|
? this.props.node.comment.post_id
|
||||||
|
: this.props.postId,
|
||||||
|
creator_id: UserService.Instance.user
|
||||||
|
? UserService.Instance.user.id
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
buttonTitle: !this.props.node ? capitalizeFirstLetter(i18n.t('post')) : this.props.edit ? capitalizeFirstLetter(i18n.t('edit')) : capitalizeFirstLetter(i18n.t('reply')),
|
buttonTitle: !this.props.node
|
||||||
|
? capitalizeFirstLetter(i18n.t('post'))
|
||||||
|
: this.props.edit
|
||||||
|
? capitalizeFirstLetter(i18n.t('edit'))
|
||||||
|
: capitalizeFirstLetter(i18n.t('reply')),
|
||||||
previewMode: false,
|
previewMode: false,
|
||||||
imageLoading: false,
|
imageLoading: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -57,7 +79,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
selectTemplate: (item: any) => {
|
selectTemplate: (item: any) => {
|
||||||
return `:${item.original.key}:`;
|
return `:${item.original.key}:`;
|
||||||
},
|
},
|
||||||
values: Object.entries(emojiShortName).map(e => {return {'key': e[1], 'val': e[0]}}),
|
values: Object.entries(emojiShortName).map(e => {
|
||||||
|
return { key: e[1], val: e[0] };
|
||||||
|
}),
|
||||||
allowSpaces: false,
|
allowSpaces: false,
|
||||||
autocompleteMode: true,
|
autocompleteMode: true,
|
||||||
menuItemLimit: mentionDropdownFetchLimit,
|
menuItemLimit: mentionDropdownFetchLimit,
|
||||||
|
@ -88,8 +112,8 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
allowSpaces: false,
|
allowSpaces: false,
|
||||||
autocompleteMode: true,
|
autocompleteMode: true,
|
||||||
menuItemLimit: mentionDropdownFetchLimit,
|
menuItemLimit: mentionDropdownFetchLimit,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
|
@ -104,7 +128,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
// A reply gets a new parent id
|
// A reply gets a new parent id
|
||||||
this.state.commentForm.parent_id = this.props.node.comment.id;
|
this.state.commentForm.parent_id = this.props.node.comment.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -124,27 +148,82 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div className={`col-sm-12`}>
|
<div className={`col-sm-12`}>
|
||||||
<textarea id={this.id} className={`form-control ${this.state.previewMode && 'd-none'}`} value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} required disabled={this.props.disabled} rows={2} maxLength={10000} />
|
<textarea
|
||||||
{this.state.previewMode &&
|
id={this.id}
|
||||||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.commentForm.content)} />
|
className={`form-control ${this.state.previewMode && 'd-none'}`}
|
||||||
}
|
value={this.state.commentForm.content}
|
||||||
|
onInput={linkEvent(this, this.handleCommentContentChange)}
|
||||||
|
required
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
rows={2}
|
||||||
|
maxLength={10000}
|
||||||
|
/>
|
||||||
|
{this.state.previewMode && (
|
||||||
|
<div
|
||||||
|
className="md-div"
|
||||||
|
dangerouslySetInnerHTML={mdToHtml(
|
||||||
|
this.state.commentForm.content
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-sm btn-secondary mr-2" disabled={this.props.disabled}>{this.state.buttonTitle}</button>
|
<button
|
||||||
{this.state.commentForm.content &&
|
type="submit"
|
||||||
<button className={`btn btn-sm mr-2 btn-secondary ${this.state.previewMode && 'active'}`} onClick={linkEvent(this, this.handlePreviewToggle)}><T i18nKey="preview">#</T></button>
|
class="btn btn-sm btn-secondary mr-2"
|
||||||
}
|
disabled={this.props.disabled}
|
||||||
{this.props.node && <button type="button" class="btn btn-sm btn-secondary mr-2" onClick={linkEvent(this, this.handleReplyCancel)}><T i18nKey="cancel">#</T></button>}
|
>
|
||||||
<a href={markdownHelpUrl} target="_blank" class="d-inline-block float-right text-muted small font-weight-bold"><T i18nKey="formatting_help">#</T></a>
|
{this.state.buttonTitle}
|
||||||
|
</button>
|
||||||
|
{this.state.commentForm.content && (
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm mr-2 btn-secondary ${this.state
|
||||||
|
.previewMode && 'active'}`}
|
||||||
|
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||||
|
>
|
||||||
|
<T i18nKey="preview">#</T>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{this.props.node && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-secondary mr-2"
|
||||||
|
onClick={linkEvent(this, this.handleReplyCancel)}
|
||||||
|
>
|
||||||
|
<T i18nKey="cancel">#</T>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href={markdownHelpUrl}
|
||||||
|
target="_blank"
|
||||||
|
class="d-inline-block float-right text-muted small font-weight-bold"
|
||||||
|
>
|
||||||
|
<T i18nKey="formatting_help">#</T>
|
||||||
|
</a>
|
||||||
<form class="d-inline-block mr-2 float-right text-muted small font-weight-bold">
|
<form class="d-inline-block mr-2 float-right text-muted small font-weight-bold">
|
||||||
<label htmlFor={`file-upload-${this.id}`} className={`${UserService.Instance.user && 'pointer'}`}><T i18nKey="upload_image">#</T></label>
|
<label
|
||||||
<input id={`file-upload-${this.id}`} type="file" accept="image/*,video/*" name="file" class="d-none" disabled={!UserService.Instance.user} onChange={linkEvent(this, this.handleImageUpload)} />
|
htmlFor={`file-upload-${this.id}`}
|
||||||
|
className={`${UserService.Instance.user && 'pointer'}`}
|
||||||
|
>
|
||||||
|
<T i18nKey="upload_image">#</T>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={`file-upload-${this.id}`}
|
||||||
|
type="file"
|
||||||
|
accept="image/*,video/*"
|
||||||
|
name="file"
|
||||||
|
class="d-none"
|
||||||
|
disabled={!UserService.Instance.user}
|
||||||
|
onChange={linkEvent(this, this.handleImageUpload)}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
{this.state.imageLoading &&
|
{this.state.imageLoading && (
|
||||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg>
|
<svg class="icon icon-spinner spin">
|
||||||
}
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -200,23 +279,24 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(res => {
|
.then(res => {
|
||||||
let url = `${window.location.origin}/pictshare/${res.url}`;
|
let url = `${window.location.origin}/pictshare/${res.url}`;
|
||||||
let markdown = (res.filetype == 'mp4') ? `[vid](${url}/raw)` : `![](${url})`;
|
let markdown =
|
||||||
let content = i.state.commentForm.content;
|
res.filetype == 'mp4' ? `[vid](${url}/raw)` : `![](${url})`;
|
||||||
content = (content) ? `${content} ${markdown}` : markdown;
|
let content = i.state.commentForm.content;
|
||||||
i.state.commentForm.content = content;
|
content = content ? `${content} ${markdown}` : markdown;
|
||||||
i.state.imageLoading = false;
|
i.state.commentForm.content = content;
|
||||||
i.setState(i.state);
|
i.state.imageLoading = false;
|
||||||
})
|
i.setState(i.state);
|
||||||
.catch((error) => {
|
})
|
||||||
i.state.imageLoading = false;
|
.catch(error => {
|
||||||
i.setState(i.state);
|
i.state.imageLoading = false;
|
||||||
alert(error);
|
i.setState(i.state);
|
||||||
})
|
alert(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
userSearch(text: string, cb: any) {
|
userSearch(text: string, cb: any) {
|
||||||
if (text) {
|
if (text) {
|
||||||
let form: SearchForm = {
|
let form: SearchForm = {
|
||||||
|
@ -229,18 +309,19 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
|
|
||||||
WebSocketService.Instance.search(form);
|
WebSocketService.Instance.search(form);
|
||||||
|
|
||||||
this.userSub = WebSocketService.Instance.subject
|
this.userSub = WebSocketService.Instance.subject.subscribe(
|
||||||
.subscribe(
|
msg => {
|
||||||
(msg) => {
|
|
||||||
let op: UserOperation = msgOp(msg);
|
let op: UserOperation = msgOp(msg);
|
||||||
if (op == UserOperation.Search) {
|
if (op == UserOperation.Search) {
|
||||||
let res: SearchResponse = msg;
|
let res: SearchResponse = msg;
|
||||||
let users = res.users.map(u => {return {key: u.name}});
|
let users = res.users.map(u => {
|
||||||
|
return { key: u.name };
|
||||||
|
});
|
||||||
cb(users);
|
cb(users);
|
||||||
this.userSub.unsubscribe();
|
this.userSub.unsubscribe();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err) => console.error(err),
|
err => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -260,18 +341,19 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||||
|
|
||||||
WebSocketService.Instance.search(form);
|
WebSocketService.Instance.search(form);
|
||||||
|
|
||||||
this.communitySub = WebSocketService.Instance.subject
|
this.communitySub = WebSocketService.Instance.subject.subscribe(
|
||||||
.subscribe(
|
msg => {
|
||||||
(msg) => {
|
|
||||||
let op: UserOperation = msgOp(msg);
|
let op: UserOperation = msgOp(msg);
|
||||||
if (op == UserOperation.Search) {
|
if (op == UserOperation.Search) {
|
||||||
let res: SearchResponse = msg;
|
let res: SearchResponse = msg;
|
||||||
let communities = res.communities.map(u => {return {key: u.name}});
|
let communities = res.communities.map(u => {
|
||||||
|
return { key: u.name };
|
||||||
|
});
|
||||||
cb(communities);
|
cb(communities);
|
||||||
this.communitySub.unsubscribe();
|
this.communitySub.unsubscribe();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err) => console.error(err),
|
err => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,6 +1,21 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, SaveCommentForm, BanFromCommunityForm, BanUserForm, CommunityUser, UserView, AddModToCommunityForm, AddAdminForm, TransferCommunityForm, TransferSiteForm, BanType } from '../interfaces';
|
import {
|
||||||
|
CommentNode as CommentNodeI,
|
||||||
|
CommentLikeForm,
|
||||||
|
CommentForm as CommentFormI,
|
||||||
|
EditUserMentionForm,
|
||||||
|
SaveCommentForm,
|
||||||
|
BanFromCommunityForm,
|
||||||
|
BanUserForm,
|
||||||
|
CommunityUser,
|
||||||
|
UserView,
|
||||||
|
AddModToCommunityForm,
|
||||||
|
AddAdminForm,
|
||||||
|
TransferCommunityForm,
|
||||||
|
TransferSiteForm,
|
||||||
|
BanType,
|
||||||
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { mdToHtml, getUnixTime, canMod, isMod } from '../utils';
|
import { mdToHtml, getUnixTime, canMod, isMod } from '../utils';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
|
@ -37,7 +52,6 @@ interface CommentNodeProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
|
|
||||||
private emptyState: CommentNodeState = {
|
private emptyState: CommentNodeState = {
|
||||||
showReply: false,
|
showReply: false,
|
||||||
showEdit: false,
|
showEdit: false,
|
||||||
|
@ -51,7 +65,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
viewSource: false,
|
viewSource: false,
|
||||||
showConfirmTransferSite: false,
|
showConfirmTransferSite: false,
|
||||||
showConfirmTransferCommunity: false,
|
showConfirmTransferCommunity: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -65,176 +79,405 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
render() {
|
render() {
|
||||||
let node = this.props.node;
|
let node = this.props.node;
|
||||||
return (
|
return (
|
||||||
<div className={`comment ${node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''}`}>
|
<div
|
||||||
{!this.state.collapsed &&
|
className={`comment ${
|
||||||
<div className={`vote-bar mr-2 float-left small text-center ${this.props.viewOnly && 'no-click'}`}>
|
node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''
|
||||||
<button className={`btn p-0 ${node.comment.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentLike)}>
|
}`}
|
||||||
<svg class="icon upvote"><use xlinkHref="#icon-arrow-up"></use></svg>
|
>
|
||||||
|
{!this.state.collapsed && (
|
||||||
|
<div
|
||||||
|
className={`vote-bar mr-2 float-left small text-center ${this.props
|
||||||
|
.viewOnly && 'no-click'}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={`btn p-0 ${
|
||||||
|
node.comment.my_vote == 1 ? 'text-info' : 'text-muted'
|
||||||
|
}`}
|
||||||
|
onClick={linkEvent(node, this.handleCommentLike)}
|
||||||
|
>
|
||||||
|
<svg class="icon upvote">
|
||||||
|
<use xlinkHref="#icon-arrow-up"></use>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class={`font-weight-bold text-muted`}>{node.comment.score}</div>
|
<div class={`font-weight-bold text-muted`}>
|
||||||
<button className={`btn p-0 ${node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentDisLike)}>
|
{node.comment.score}
|
||||||
<svg class="icon downvote"><use xlinkHref="#icon-arrow-down"></use></svg>
|
</div>
|
||||||
|
<button
|
||||||
|
className={`btn p-0 ${
|
||||||
|
node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'
|
||||||
|
}`}
|
||||||
|
onClick={linkEvent(node, this.handleCommentDisLike)}
|
||||||
|
>
|
||||||
|
<svg class="icon downvote">
|
||||||
|
<use xlinkHref="#icon-arrow-down"></use>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
<div id={`comment-${node.comment.id}`} className={`details comment-node ml-4 ${this.isCommentNew ? 'mark' : ''}`}>
|
<div
|
||||||
|
id={`comment-${node.comment.id}`}
|
||||||
|
className={`details comment-node ml-4 ${
|
||||||
|
this.isCommentNew ? 'mark' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<ul class="list-inline mb-0 text-muted small">
|
<ul class="list-inline mb-0 text-muted small">
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<Link className="text-info" to={`/u/${node.comment.creator_name}`}>{node.comment.creator_name}</Link>
|
<Link
|
||||||
|
className="text-info"
|
||||||
|
to={`/u/${node.comment.creator_name}`}
|
||||||
|
>
|
||||||
|
{node.comment.creator_name}
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{this.isMod &&
|
{this.isMod && (
|
||||||
<li className="list-inline-item badge badge-light"><T i18nKey="mod">#</T></li>
|
<li className="list-inline-item badge badge-light">
|
||||||
}
|
<T i18nKey="mod">#</T>
|
||||||
{this.isAdmin &&
|
</li>
|
||||||
<li className="list-inline-item badge badge-light"><T i18nKey="admin">#</T></li>
|
)}
|
||||||
}
|
{this.isAdmin && (
|
||||||
{this.isPostCreator &&
|
<li className="list-inline-item badge badge-light">
|
||||||
<li className="list-inline-item badge badge-light"><T i18nKey="creator">#</T></li>
|
<T i18nKey="admin">#</T>
|
||||||
}
|
</li>
|
||||||
{(node.comment.banned_from_community || node.comment.banned) &&
|
)}
|
||||||
<li className="list-inline-item badge badge-danger"><T i18nKey="banned">#</T></li>
|
{this.isPostCreator && (
|
||||||
}
|
<li className="list-inline-item badge badge-light">
|
||||||
|
<T i18nKey="creator">#</T>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{(node.comment.banned_from_community || node.comment.banned) && (
|
||||||
|
<li className="list-inline-item badge badge-danger">
|
||||||
|
<T i18nKey="banned">#</T>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span>(
|
<span>
|
||||||
<span className="text-info">+{node.comment.upvotes}</span>
|
(<span className="text-info">+{node.comment.upvotes}</span>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<span className="text-danger">-{node.comment.downvotes}</span>
|
<span className="text-danger">-{node.comment.downvotes}</span>
|
||||||
<span>) </span>
|
<span>) </span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span><MomentTime data={node.comment} /></span>
|
<span>
|
||||||
|
<MomentTime data={node.comment} />
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<div className="pointer text-monospace" onClick={linkEvent(this, this.handleCommentCollapse)}>{this.state.collapsed ? '[+]' : '[-]'}</div>
|
<div
|
||||||
|
className="pointer text-monospace"
|
||||||
|
onClick={linkEvent(this, this.handleCommentCollapse)}
|
||||||
|
>
|
||||||
|
{this.state.collapsed ? '[+]' : '[-]'}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
|
{this.state.showEdit && (
|
||||||
{!this.state.showEdit && !this.state.collapsed &&
|
<CommentForm
|
||||||
|
node={node}
|
||||||
|
edit
|
||||||
|
onReplyCancel={this.handleReplyCancel}
|
||||||
|
disabled={this.props.locked}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!this.state.showEdit && !this.state.collapsed && (
|
||||||
<div>
|
<div>
|
||||||
{this.state.viewSource ? <pre>{this.commentUnlessRemoved}</pre> :
|
{this.state.viewSource ? (
|
||||||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.commentUnlessRemoved)} />
|
<pre>{this.commentUnlessRemoved}</pre>
|
||||||
}
|
) : (
|
||||||
|
<div
|
||||||
|
className="md-div"
|
||||||
|
dangerouslySetInnerHTML={mdToHtml(this.commentUnlessRemoved)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||||
{UserService.Instance.user && !this.props.viewOnly &&
|
{UserService.Instance.user && !this.props.viewOnly && (
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}><T i18nKey="reply">#</T></span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleReplyClick)}
|
||||||
|
>
|
||||||
|
<T i18nKey="reply">#</T>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item mr-2">
|
<li className="list-inline-item mr-2">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleSaveCommentClick)}>{node.comment.saved ? i18n.t('unsave') : i18n.t('save')}</span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleSaveCommentClick)}
|
||||||
|
>
|
||||||
|
{node.comment.saved ? i18n.t('unsave') : i18n.t('save')}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
{this.myComment &&
|
{this.myComment && (
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleEditClick)}
|
||||||
|
>
|
||||||
|
<T i18nKey="edit">#</T>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
|
<span
|
||||||
{!node.comment.deleted ? i18n.t('delete') : i18n.t('restore')}
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||||
|
>
|
||||||
|
{!node.comment.deleted
|
||||||
|
? i18n.t('delete')
|
||||||
|
: i18n.t('restore')}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{/* Admins and mods can remove comments */}
|
{/* Admins and mods can remove comments */}
|
||||||
{(this.canMod || this.canAdmin) &&
|
{(this.canMod || this.canAdmin) && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!node.comment.removed ?
|
{!node.comment.removed ? (
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
|
<span
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
|
class="pointer"
|
||||||
}
|
onClick={linkEvent(this, this.handleModRemoveShow)}
|
||||||
|
>
|
||||||
|
<T i18nKey="remove">#</T>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleModRemoveSubmit
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="restore">#</T>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
{/* Mods can ban from community, and appoint as mods to community */}
|
{/* Mods can ban from community, and appoint as mods to community */}
|
||||||
{this.canMod &&
|
{this.canMod && (
|
||||||
<>
|
<>
|
||||||
{!this.isMod &&
|
{!this.isMod && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!node.comment.banned_from_community ?
|
{!node.comment.banned_from_community ? (
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}><T i18nKey="ban">#</T></span> :
|
<span
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}><T i18nKey="unban">#</T></span>
|
class="pointer"
|
||||||
}
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleModBanFromCommunityShow
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="ban">#</T>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleModBanFromCommunitySubmit
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="unban">#</T>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
{!node.comment.banned_from_community &&
|
{!node.comment.banned_from_community && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{this.isMod ? i18n.t('remove_as_mod') : i18n.t('appoint_as_mod')}</span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleAddModToCommunity
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{this.isMod
|
||||||
|
? i18n.t('remove_as_mod')
|
||||||
|
: i18n.t('appoint_as_mod')}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{/* Community creators and admins can transfer community to another mod */}
|
{/* Community creators and admins can transfer community to another mod */}
|
||||||
{(this.amCommunityCreator || this.canAdmin) && this.isMod &&
|
{(this.amCommunityCreator || this.canAdmin) && this.isMod && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!this.state.showConfirmTransferCommunity ?
|
{!this.state.showConfirmTransferCommunity ? (
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferCommunity)}><T i18nKey="transfer_community">#</T>
|
<span
|
||||||
</span> : <>
|
class="pointer"
|
||||||
<span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
|
onClick={linkEvent(
|
||||||
<span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferCommunity)}><T i18nKey="yes">#</T></span>
|
this,
|
||||||
<span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferCommunity)}><T i18nKey="no">#</T></span>
|
this.handleShowConfirmTransferCommunity
|
||||||
</>
|
)}
|
||||||
}
|
>
|
||||||
|
<T i18nKey="transfer_community">#</T>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span class="d-inline-block mr-1">
|
||||||
|
<T i18nKey="are_you_sure">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block mr-1"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleTransferCommunity
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="yes">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleCancelShowConfirmTransferCommunity
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="no">#</T>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
{/* Admins can ban from all, and appoint other admins */}
|
{/* Admins can ban from all, and appoint other admins */}
|
||||||
{this.canAdmin &&
|
{this.canAdmin && (
|
||||||
<>
|
<>
|
||||||
{!this.isAdmin &&
|
{!this.isAdmin && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!node.comment.banned ?
|
{!node.comment.banned ? (
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}><T i18nKey="ban_from_site">#</T></span> :
|
<span
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}><T i18nKey="unban_from_site">#</T></span>
|
class="pointer"
|
||||||
}
|
onClick={linkEvent(this, this.handleModBanShow)}
|
||||||
|
>
|
||||||
|
<T i18nKey="ban_from_site">#</T>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleModBanSubmit
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="unban_from_site">#</T>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
{!node.comment.banned &&
|
{!node.comment.banned && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleAddAdmin)}>{this.isAdmin ? i18n.t('remove_as_admin') : i18n.t('appoint_as_admin')}</span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleAddAdmin)}
|
||||||
|
>
|
||||||
|
{this.isAdmin
|
||||||
|
? i18n.t('remove_as_admin')
|
||||||
|
: i18n.t('appoint_as_admin')}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{/* Site Creator can transfer to another admin */}
|
{/* Site Creator can transfer to another admin */}
|
||||||
{this.amSiteCreator && this.isAdmin &&
|
{this.amSiteCreator && this.isAdmin && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!this.state.showConfirmTransferSite ?
|
{!this.state.showConfirmTransferSite ? (
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferSite)}><T i18nKey="transfer_site">#</T>
|
<span
|
||||||
</span> : <>
|
class="pointer"
|
||||||
<span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
|
onClick={linkEvent(
|
||||||
<span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferSite)}><T i18nKey="yes">#</T></span>
|
this,
|
||||||
<span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferSite)}><T i18nKey="no">#</T></span>
|
this.handleShowConfirmTransferSite
|
||||||
</>
|
)}
|
||||||
}
|
>
|
||||||
|
<T i18nKey="transfer_site">#</T>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span class="d-inline-block mr-1">
|
||||||
|
<T i18nKey="are_you_sure">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block mr-1"
|
||||||
|
onClick={linkEvent(this, this.handleTransferSite)}
|
||||||
|
>
|
||||||
|
<T i18nKey="yes">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleCancelShowConfirmTransferSite
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="no">#</T>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span className="pointer" onClick={linkEvent(this, this.handleViewSource)}><T i18nKey="view_source">#</T></span>
|
<span
|
||||||
|
className="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleViewSource)}
|
||||||
|
>
|
||||||
|
<T i18nKey="view_source">#</T>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}><T i18nKey="link">#</T></Link>
|
<Link
|
||||||
|
className="text-muted"
|
||||||
|
to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
|
||||||
|
>
|
||||||
|
<T i18nKey="link">#</T>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{this.props.markable &&
|
{this.props.markable && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleMarkRead)}>{node.comment.read ? i18n.t('mark_as_unread') : i18n.t('mark_as_read')}</span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleMarkRead)}
|
||||||
|
>
|
||||||
|
{node.comment.read
|
||||||
|
? i18n.t('mark_as_unread')
|
||||||
|
: i18n.t('mark_as_read')}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{this.state.showRemoveDialog &&
|
{this.state.showRemoveDialog && (
|
||||||
<form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
|
<form
|
||||||
<input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
|
class="form-inline"
|
||||||
<button type="submit" class="btn btn-secondary"><T i18nKey="remove_comment">#</T></button>
|
onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control mr-2"
|
||||||
|
placeholder={i18n.t('reason')}
|
||||||
|
value={this.state.removeReason}
|
||||||
|
onInput={linkEvent(this, this.handleModRemoveReasonChange)}
|
||||||
|
/>
|
||||||
|
<button type="submit" class="btn btn-secondary">
|
||||||
|
<T i18nKey="remove_comment">#</T>
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
}
|
)}
|
||||||
{this.state.showBanDialog &&
|
{this.state.showBanDialog && (
|
||||||
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-form-label"><T i18nKey="reason">#</T></label>
|
<label class="col-form-label">
|
||||||
<input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
|
<T i18nKey="reason">#</T>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control mr-2"
|
||||||
|
placeholder={i18n.t('reason')}
|
||||||
|
value={this.state.banReason}
|
||||||
|
onInput={linkEvent(this, this.handleModBanReasonChange)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* TODO hold off on expires until later */}
|
{/* TODO hold off on expires until later */}
|
||||||
{/* <div class="form-group row"> */}
|
{/* <div class="form-group row"> */}
|
||||||
|
@ -242,42 +485,59 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
{/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
|
{/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<button type="submit" class="btn btn-secondary">{i18n.t('ban')} {node.comment.creator_name}</button>
|
<button type="submit" class="btn btn-secondary">
|
||||||
|
{i18n.t('ban')} {node.comment.creator_name}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
}
|
)}
|
||||||
{this.state.showReply &&
|
{this.state.showReply && (
|
||||||
<CommentForm
|
<CommentForm
|
||||||
node={node}
|
node={node}
|
||||||
onReplyCancel={this.handleReplyCancel}
|
onReplyCancel={this.handleReplyCancel}
|
||||||
disabled={this.props.locked}
|
disabled={this.props.locked}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
{node.children && !this.state.collapsed &&
|
{node.children && !this.state.collapsed && (
|
||||||
<CommentNodes
|
<CommentNodes
|
||||||
nodes={node.children}
|
nodes={node.children}
|
||||||
locked={this.props.locked}
|
locked={this.props.locked}
|
||||||
moderators={this.props.moderators}
|
moderators={this.props.moderators}
|
||||||
admins={this.props.admins}
|
admins={this.props.admins}
|
||||||
postCreatorId={this.props.postCreatorId}
|
postCreatorId={this.props.postCreatorId}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
{/* A collapsed clearfix */}
|
{/* A collapsed clearfix */}
|
||||||
{this.state.collapsed && <div class="row col-12"></div>}
|
{this.state.collapsed && <div class="row col-12"></div>}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get myComment(): boolean {
|
get myComment(): boolean {
|
||||||
return UserService.Instance.user && this.props.node.comment.creator_id == UserService.Instance.user.id;
|
return (
|
||||||
|
UserService.Instance.user &&
|
||||||
|
this.props.node.comment.creator_id == UserService.Instance.user.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isMod(): boolean {
|
get isMod(): boolean {
|
||||||
return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.node.comment.creator_id);
|
return (
|
||||||
|
this.props.moderators &&
|
||||||
|
isMod(
|
||||||
|
this.props.moderators.map(m => m.user_id),
|
||||||
|
this.props.node.comment.creator_id
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isAdmin(): boolean {
|
get isAdmin(): boolean {
|
||||||
return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
|
return (
|
||||||
|
this.props.admins &&
|
||||||
|
isMod(
|
||||||
|
this.props.admins.map(a => a.id),
|
||||||
|
this.props.node.comment.creator_id
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isPostCreator(): boolean {
|
get isPostCreator(): boolean {
|
||||||
|
@ -285,38 +545,57 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get canMod(): boolean {
|
get canMod(): boolean {
|
||||||
|
|
||||||
if (this.props.admins && this.props.moderators) {
|
if (this.props.admins && this.props.moderators) {
|
||||||
let adminsThenMods = this.props.admins.map(a => a.id)
|
let adminsThenMods = this.props.admins
|
||||||
.concat(this.props.moderators.map(m => m.user_id));
|
.map(a => a.id)
|
||||||
|
.concat(this.props.moderators.map(m => m.user_id));
|
||||||
|
|
||||||
return canMod(UserService.Instance.user, adminsThenMods, this.props.node.comment.creator_id);
|
return canMod(
|
||||||
} else {
|
UserService.Instance.user,
|
||||||
|
adminsThenMods,
|
||||||
|
this.props.node.comment.creator_id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get canAdmin(): boolean {
|
get canAdmin(): boolean {
|
||||||
return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
|
return (
|
||||||
|
this.props.admins &&
|
||||||
|
canMod(
|
||||||
|
UserService.Instance.user,
|
||||||
|
this.props.admins.map(a => a.id),
|
||||||
|
this.props.node.comment.creator_id
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get amCommunityCreator(): boolean {
|
get amCommunityCreator(): boolean {
|
||||||
return this.props.moderators &&
|
return (
|
||||||
UserService.Instance.user &&
|
this.props.moderators &&
|
||||||
(this.props.node.comment.creator_id != UserService.Instance.user.id) &&
|
UserService.Instance.user &&
|
||||||
(UserService.Instance.user.id == this.props.moderators[0].user_id);
|
this.props.node.comment.creator_id != UserService.Instance.user.id &&
|
||||||
|
UserService.Instance.user.id == this.props.moderators[0].user_id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get amSiteCreator(): boolean {
|
get amSiteCreator(): boolean {
|
||||||
return this.props.admins &&
|
return (
|
||||||
UserService.Instance.user &&
|
this.props.admins &&
|
||||||
(this.props.node.comment.creator_id != UserService.Instance.user.id) &&
|
UserService.Instance.user &&
|
||||||
(UserService.Instance.user.id == this.props.admins[0].id);
|
this.props.node.comment.creator_id != UserService.Instance.user.id &&
|
||||||
|
UserService.Instance.user.id == this.props.admins[0].id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get commentUnlessRemoved(): string {
|
get commentUnlessRemoved(): string {
|
||||||
let node = this.props.node;
|
let node = this.props.node;
|
||||||
return node.comment.removed ? `*${i18n.t('removed')}*` : node.comment.deleted ? `*${i18n.t('deleted')}*` : node.comment.content;
|
return node.comment.removed
|
||||||
|
? `*${i18n.t('removed')}*`
|
||||||
|
: node.comment.deleted
|
||||||
|
? `*${i18n.t('deleted')}*`
|
||||||
|
: node.comment.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReplyClick(i: CommentNode) {
|
handleReplyClick(i: CommentNode) {
|
||||||
|
@ -337,16 +616,19 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
post_id: i.props.node.comment.post_id,
|
post_id: i.props.node.comment.post_id,
|
||||||
parent_id: i.props.node.comment.parent_id,
|
parent_id: i.props.node.comment.parent_id,
|
||||||
deleted: !i.props.node.comment.deleted,
|
deleted: !i.props.node.comment.deleted,
|
||||||
auth: null
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editComment(deleteForm);
|
WebSocketService.Instance.editComment(deleteForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSaveCommentClick(i: CommentNode) {
|
handleSaveCommentClick(i: CommentNode) {
|
||||||
let saved = (i.props.node.comment.saved == undefined) ? true : !i.props.node.comment.saved;
|
let saved =
|
||||||
|
i.props.node.comment.saved == undefined
|
||||||
|
? true
|
||||||
|
: !i.props.node.comment.saved;
|
||||||
let form: SaveCommentForm = {
|
let form: SaveCommentForm = {
|
||||||
comment_id: i.props.node.comment.id,
|
comment_id: i.props.node.comment.id,
|
||||||
save: saved
|
save: saved,
|
||||||
};
|
};
|
||||||
|
|
||||||
WebSocketService.Instance.saveComment(form);
|
WebSocketService.Instance.saveComment(form);
|
||||||
|
@ -358,13 +640,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleCommentLike(i: CommentNodeI) {
|
handleCommentLike(i: CommentNodeI) {
|
||||||
|
|
||||||
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: i.comment.my_vote == 1 ? 0 : 1,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.likeComment(form);
|
WebSocketService.Instance.likeComment(form);
|
||||||
}
|
}
|
||||||
|
@ -373,7 +653,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
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: i.comment.my_vote == -1 ? 0 : -1,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.likeComment(form);
|
WebSocketService.Instance.likeComment(form);
|
||||||
}
|
}
|
||||||
|
@ -398,7 +678,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
parent_id: i.props.node.comment.parent_id,
|
parent_id: i.props.node.comment.parent_id,
|
||||||
removed: !i.props.node.comment.removed,
|
removed: !i.props.node.comment.removed,
|
||||||
reason: i.state.removeReason,
|
reason: i.state.removeReason,
|
||||||
auth: null
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editComment(form);
|
WebSocketService.Instance.editComment(form);
|
||||||
|
|
||||||
|
@ -407,19 +687,27 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMarkRead(i: CommentNode) {
|
handleMarkRead(i: CommentNode) {
|
||||||
let form: CommentFormI = {
|
// if it has a user_mention_id field, then its a mention
|
||||||
content: i.props.node.comment.content,
|
if (i.props.node.comment.user_mention_id) {
|
||||||
edit_id: i.props.node.comment.id,
|
let form: EditUserMentionForm = {
|
||||||
creator_id: i.props.node.comment.creator_id,
|
user_mention_id: i.props.node.comment.user_mention_id,
|
||||||
post_id: i.props.node.comment.post_id,
|
read: !i.props.node.comment.read,
|
||||||
parent_id: i.props.node.comment.parent_id,
|
};
|
||||||
read: !i.props.node.comment.read,
|
WebSocketService.Instance.editUserMention(form);
|
||||||
auth: null
|
} else {
|
||||||
};
|
let form: CommentFormI = {
|
||||||
WebSocketService.Instance.editComment(form);
|
content: i.props.node.comment.content,
|
||||||
|
edit_id: i.props.node.comment.id,
|
||||||
|
creator_id: i.props.node.comment.creator_id,
|
||||||
|
post_id: i.props.node.comment.post_id,
|
||||||
|
parent_id: i.props.node.comment.parent_id,
|
||||||
|
read: !i.props.node.comment.read,
|
||||||
|
auth: null,
|
||||||
|
};
|
||||||
|
WebSocketService.Instance.editComment(form);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleModBanFromCommunityShow(i: CommentNode) {
|
handleModBanFromCommunityShow(i: CommentNode) {
|
||||||
i.state.showBanDialog = true;
|
i.state.showBanDialog = true;
|
||||||
i.state.banType = BanType.Community;
|
i.state.banType = BanType.Community;
|
||||||
|
@ -499,12 +787,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowConfirmTransferCommunity(i: CommentNode) {
|
handleShowConfirmTransferCommunity(i: CommentNode) {
|
||||||
i.state.showConfirmTransferCommunity = true;
|
i.state.showConfirmTransferCommunity = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCancelShowConfirmTransferCommunity(i: CommentNode) {
|
handleCancelShowConfirmTransferCommunity(i: CommentNode) {
|
||||||
i.state.showConfirmTransferCommunity = false;
|
i.state.showConfirmTransferCommunity = false;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
@ -519,12 +807,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowConfirmTransferSite(i: CommentNode) {
|
handleShowConfirmTransferSite(i: CommentNode) {
|
||||||
i.state.showConfirmTransferSite = true;
|
i.state.showConfirmTransferSite = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCancelShowConfirmTransferSite(i: CommentNode) {
|
handleCancelShowConfirmTransferSite(i: CommentNode) {
|
||||||
i.state.showConfirmTransferSite = false;
|
i.state.showConfirmTransferSite = false;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import { Component } from 'inferno';
|
import { Component } from 'inferno';
|
||||||
import { CommentNode as CommentNodeI, CommunityUser, UserView } from '../interfaces';
|
import {
|
||||||
|
CommentNode as CommentNodeI,
|
||||||
|
CommunityUser,
|
||||||
|
UserView,
|
||||||
|
} from '../interfaces';
|
||||||
import { CommentNode } from './comment-node';
|
import { CommentNode } from './comment-node';
|
||||||
|
|
||||||
interface CommentNodesState {
|
interface CommentNodesState {}
|
||||||
}
|
|
||||||
|
|
||||||
interface CommentNodesProps {
|
interface CommentNodesProps {
|
||||||
nodes: Array<CommentNodeI>;
|
nodes: Array<CommentNodeI>;
|
||||||
|
@ -16,8 +19,10 @@ interface CommentNodesProps {
|
||||||
markable?: boolean;
|
markable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> {
|
export class CommentNodes extends Component<
|
||||||
|
CommentNodesProps,
|
||||||
|
CommentNodesState
|
||||||
|
> {
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
}
|
}
|
||||||
|
@ -25,20 +30,19 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="comments">
|
<div className="comments">
|
||||||
{this.props.nodes.map(node =>
|
{this.props.nodes.map(node => (
|
||||||
<CommentNode node={node}
|
<CommentNode
|
||||||
noIndent={this.props.noIndent}
|
node={node}
|
||||||
viewOnly={this.props.viewOnly}
|
noIndent={this.props.noIndent}
|
||||||
locked={this.props.locked}
|
viewOnly={this.props.viewOnly}
|
||||||
|
locked={this.props.locked}
|
||||||
moderators={this.props.moderators}
|
moderators={this.props.moderators}
|
||||||
admins={this.props.admins}
|
admins={this.props.admins}
|
||||||
postCreatorId={this.props.postCreatorId}
|
postCreatorId={this.props.postCreatorId}
|
||||||
markable={this.props.markable}
|
markable={this.props.markable}
|
||||||
/>
|
/>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { UserOperation, Community, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm, ListCommunitiesForm, SortType } from '../interfaces';
|
import {
|
||||||
|
UserOperation,
|
||||||
|
Community,
|
||||||
|
ListCommunitiesResponse,
|
||||||
|
CommunityResponse,
|
||||||
|
FollowCommunityForm,
|
||||||
|
ListCommunitiesForm,
|
||||||
|
SortType,
|
||||||
|
} from '../interfaces';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { msgOp } from '../utils';
|
import { msgOp } from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
@ -22,25 +30,31 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
communities: [],
|
communities: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
page: this.getPageFromProps(this.props),
|
page: this.getPageFromProps(this.props),
|
||||||
}
|
};
|
||||||
|
|
||||||
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.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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
(msg) => this.parseMessage(msg),
|
msg => this.parseMessage(msg),
|
||||||
(err) => console.error(err),
|
err => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
this.refetch();
|
this.refetch();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPageFromProps(props: any): number {
|
getPageFromProps(props: any): number {
|
||||||
return (props.match.params.page) ? Number(props.match.params.page) : 1;
|
return props.match.params.page ? Number(props.match.params.page) : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -48,7 +62,9 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.title = `${i18n.t('communities')} - ${WebSocketService.Instance.site.name}`;
|
document.title = `${i18n.t('communities')} - ${
|
||||||
|
WebSocketService.Instance.site.name
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Necessary for back button for some reason
|
// Necessary for back button for some reason
|
||||||
|
@ -63,46 +79,92 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{this.state.loading ?
|
{this.state.loading ? (
|
||||||
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
<h5 class="">
|
||||||
<div>
|
<svg class="icon icon-spinner spin">
|
||||||
<h5><T i18nKey="list_of_communities">#</T></h5>
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
<div class="table-responsive">
|
</svg>
|
||||||
<table id="community_table" class="table table-sm table-hover">
|
</h5>
|
||||||
<thead class="pointer">
|
) : (
|
||||||
<tr>
|
<div>
|
||||||
<th><T i18nKey="name">#</T></th>
|
<h5>
|
||||||
<th class="d-none d-lg-table-cell"><T i18nKey="title">#</T></th>
|
<T i18nKey="list_of_communities">#</T>
|
||||||
<th><T i18nKey="category">#</T></th>
|
</h5>
|
||||||
<th class="text-right"><T i18nKey="subscribers">#</T></th>
|
<div class="table-responsive">
|
||||||
<th class="text-right d-none d-lg-table-cell"><T i18nKey="posts">#</T></th>
|
<table id="community_table" class="table table-sm table-hover">
|
||||||
<th class="text-right d-none d-lg-table-cell"><T i18nKey="comments">#</T></th>
|
<thead class="pointer">
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{this.state.communities.map(community =>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><Link to={`/c/${community.name}`}>{community.name}</Link></td>
|
<th>
|
||||||
<td class="d-none d-lg-table-cell">{community.title}</td>
|
<T i18nKey="name">#</T>
|
||||||
<td>{community.category_name}</td>
|
</th>
|
||||||
<td class="text-right">{community.number_of_subscribers}</td>
|
<th class="d-none d-lg-table-cell">
|
||||||
<td class="text-right d-none d-lg-table-cell">{community.number_of_posts}</td>
|
<T i18nKey="title">#</T>
|
||||||
<td class="text-right d-none d-lg-table-cell">{community.number_of_comments}</td>
|
</th>
|
||||||
<td class="text-right">
|
<th>
|
||||||
{community.subscribed ?
|
<T i18nKey="category">#</T>
|
||||||
<span class="pointer btn-link" onClick={linkEvent(community.id, this.handleUnsubscribe)}><T i18nKey="unsubscribe">#</T></span> :
|
</th>
|
||||||
<span class="pointer btn-link" onClick={linkEvent(community.id, this.handleSubscribe)}><T i18nKey="subscribe">#</T></span>
|
<th class="text-right">
|
||||||
}
|
<T i18nKey="subscribers">#</T>
|
||||||
</td>
|
</th>
|
||||||
|
<th class="text-right d-none d-lg-table-cell">
|
||||||
|
<T i18nKey="posts">#</T>
|
||||||
|
</th>
|
||||||
|
<th class="text-right d-none d-lg-table-cell">
|
||||||
|
<T i18nKey="comments">#</T>
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{this.state.communities.map(community => (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Link to={`/c/${community.name}`}>
|
||||||
|
{community.name}
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-lg-table-cell">{community.title}</td>
|
||||||
|
<td>{community.category_name}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{community.number_of_subscribers}
|
||||||
|
</td>
|
||||||
|
<td class="text-right d-none d-lg-table-cell">
|
||||||
|
{community.number_of_posts}
|
||||||
|
</td>
|
||||||
|
<td class="text-right d-none d-lg-table-cell">
|
||||||
|
{community.number_of_comments}
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{community.subscribed ? (
|
||||||
|
<span
|
||||||
|
class="pointer btn-link"
|
||||||
|
onClick={linkEvent(
|
||||||
|
community.id,
|
||||||
|
this.handleUnsubscribe
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="unsubscribe">#</T>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
class="pointer btn-link"
|
||||||
|
onClick={linkEvent(
|
||||||
|
community.id,
|
||||||
|
this.handleSubscribe
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="subscribe">#</T>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{this.paginator()}
|
||||||
</div>
|
</div>
|
||||||
{this.paginator()}
|
)}
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -110,10 +172,20 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
paginator() {
|
paginator() {
|
||||||
return (
|
return (
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{this.state.page > 1 &&
|
{this.state.page > 1 && (
|
||||||
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
|
<button
|
||||||
}
|
class="btn btn-sm btn-secondary mr-1"
|
||||||
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
|
>
|
||||||
|
<T i18nKey="prev">#</T>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
|
>
|
||||||
|
<T i18nKey="next">#</T>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -122,14 +194,14 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
this.props.history.push(`/communities/page/${this.state.page}`);
|
this.props.history.push(`/communities/page/${this.state.page}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPage(i: Communities) {
|
nextPage(i: Communities) {
|
||||||
i.state.page++;
|
i.state.page++;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.refetch();
|
i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPage(i: Communities) {
|
prevPage(i: Communities) {
|
||||||
i.state.page--;
|
i.state.page--;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
|
@ -139,7 +211,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
handleUnsubscribe(communityId: number) {
|
handleUnsubscribe(communityId: number) {
|
||||||
let form: FollowCommunityForm = {
|
let form: FollowCommunityForm = {
|
||||||
community_id: communityId,
|
community_id: communityId,
|
||||||
follow: false
|
follow: false,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.followCommunity(form);
|
WebSocketService.Instance.followCommunity(form);
|
||||||
}
|
}
|
||||||
|
@ -147,7 +219,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
handleSubscribe(communityId: number) {
|
handleSubscribe(communityId: number) {
|
||||||
let form: FollowCommunityForm = {
|
let form: FollowCommunityForm = {
|
||||||
community_id: communityId,
|
community_id: communityId,
|
||||||
follow: true
|
follow: true,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.followCommunity(form);
|
WebSocketService.Instance.followCommunity(form);
|
||||||
}
|
}
|
||||||
|
@ -157,10 +229,9 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
sort: SortType[SortType.TopAll],
|
sort: SortType[SortType.TopAll],
|
||||||
limit: 100,
|
limit: 100,
|
||||||
page: this.state.page,
|
page: this.state.page,
|
||||||
}
|
};
|
||||||
|
|
||||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
|
@ -172,9 +243,11 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
} else if (op == UserOperation.ListCommunities) {
|
} else if (op == UserOperation.ListCommunities) {
|
||||||
let res: ListCommunitiesResponse = msg;
|
let res: ListCommunitiesResponse = msg;
|
||||||
this.state.communities = res.communities;
|
this.state.communities = res.communities;
|
||||||
this.state.communities.sort((a, b) => b.number_of_subscribers - a.number_of_subscribers);
|
this.state.communities.sort(
|
||||||
|
(a, b) => b.number_of_subscribers - a.number_of_subscribers
|
||||||
|
);
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
let table = document.querySelector('#community_table');
|
let table = document.querySelector('#community_table');
|
||||||
Sortable.initTable(table);
|
Sortable.initTable(table);
|
||||||
|
@ -184,6 +257,6 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
found.subscribed = res.community.subscribed;
|
found.subscribed = res.community.subscribed;
|
||||||
found.number_of_subscribers = res.community.number_of_subscribers;
|
found.number_of_subscribers = res.community.number_of_subscribers;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { CommunityForm as CommunityFormI, UserOperation, Category, ListCategoriesResponse, CommunityResponse } from '../interfaces';
|
import {
|
||||||
|
CommunityForm as CommunityFormI,
|
||||||
|
UserOperation,
|
||||||
|
Category,
|
||||||
|
ListCategoriesResponse,
|
||||||
|
CommunityResponse,
|
||||||
|
} from '../interfaces';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { msgOp, capitalizeFirstLetter } from '../utils';
|
import { msgOp, capitalizeFirstLetter } from '../utils';
|
||||||
import * as autosize from 'autosize';
|
import * as autosize from 'autosize';
|
||||||
|
@ -23,7 +29,10 @@ interface CommunityFormState {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommunityForm extends Component<CommunityFormProps, CommunityFormState> {
|
export class CommunityForm extends Component<
|
||||||
|
CommunityFormProps,
|
||||||
|
CommunityFormState
|
||||||
|
> {
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
|
|
||||||
private emptyState: CommunityFormState = {
|
private emptyState: CommunityFormState = {
|
||||||
|
@ -34,8 +43,8 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
},
|
},
|
||||||
categories: [],
|
categories: [],
|
||||||
loading: false
|
loading: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -50,16 +59,23 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||||
description: this.props.community.description,
|
description: this.props.community.description,
|
||||||
edit_id: this.props.community.id,
|
edit_id: this.props.community.id,
|
||||||
nsfw: this.props.community.nsfw,
|
nsfw: this.props.community.nsfw,
|
||||||
auth: null
|
auth: null,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
(msg) => this.parseMessage(msg),
|
msg => this.parseMessage(msg),
|
||||||
(err) => console.error(err),
|
err => console.error(err),
|
||||||
() => console.log("complete")
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
WebSocketService.Instance.listCategories();
|
WebSocketService.Instance.listCategories();
|
||||||
|
@ -73,53 +89,110 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
|
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-12 col-form-label"><T i18nKey="name">#</T></label>
|
<label class="col-12 col-form-label">
|
||||||
|
<T i18nKey="name">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<input type="text" class="form-control" value={this.state.communityForm.name} onInput={linkEvent(this, this.handleCommunityNameChange)} required minLength={3} maxLength={20} pattern="[a-z0-9_]+" title={i18n.t('community_reqs')}/>
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
value={this.state.communityForm.name}
|
||||||
|
onInput={linkEvent(this, this.handleCommunityNameChange)}
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
maxLength={20}
|
||||||
|
pattern="[a-z0-9_]+"
|
||||||
|
title={i18n.t('community_reqs')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-12 col-form-label"><T i18nKey="title">#</T></label>
|
<label class="col-12 col-form-label">
|
||||||
|
<T i18nKey="title">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<input type="text" value={this.state.communityForm.title} onInput={linkEvent(this, this.handleCommunityTitleChange)} class="form-control" required minLength={3} maxLength={100} />
|
<input
|
||||||
|
type="text"
|
||||||
|
value={this.state.communityForm.title}
|
||||||
|
onInput={linkEvent(this, this.handleCommunityTitleChange)}
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
maxLength={100}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-12 col-form-label"><T i18nKey="sidebar">#</T></label>
|
<label class="col-12 col-form-label">
|
||||||
|
<T i18nKey="sidebar">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={3} maxLength={10000} />
|
<textarea
|
||||||
|
value={this.state.communityForm.description}
|
||||||
|
onInput={linkEvent(this, this.handleCommunityDescriptionChange)}
|
||||||
|
class="form-control"
|
||||||
|
rows={3}
|
||||||
|
maxLength={10000}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-12 col-form-label"><T i18nKey="category">#</T></label>
|
<label class="col-12 col-form-label">
|
||||||
|
<T i18nKey="category">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<select class="form-control" value={this.state.communityForm.category_id} onInput={linkEvent(this, this.handleCommunityCategoryChange)}>
|
<select
|
||||||
{this.state.categories.map(category =>
|
class="form-control"
|
||||||
|
value={this.state.communityForm.category_id}
|
||||||
|
onInput={linkEvent(this, this.handleCommunityCategoryChange)}
|
||||||
|
>
|
||||||
|
{this.state.categories.map(category => (
|
||||||
<option value={category.id}>{category.name}</option>
|
<option value={category.id}>{category.name}</option>
|
||||||
)}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" checked={this.state.communityForm.nsfw} onChange={linkEvent(this, this.handleCommunityNsfwChange)}/>
|
<input
|
||||||
<label class="form-check-label"><T i18nKey="nsfw">#</T></label>
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={this.state.communityForm.nsfw}
|
||||||
|
onChange={linkEvent(this, this.handleCommunityNsfwChange)}
|
||||||
|
/>
|
||||||
|
<label class="form-check-label">
|
||||||
|
<T i18nKey="nsfw">#</T>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<button type="submit" class="btn btn-secondary mr-2">
|
<button type="submit" class="btn btn-secondary mr-2">
|
||||||
{this.state.loading ?
|
{this.state.loading ? (
|
||||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
|
<svg class="icon icon-spinner spin">
|
||||||
this.props.community ? capitalizeFirstLetter(i18n.t('save')) : capitalizeFirstLetter(i18n.t('create'))}</button>
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
{this.props.community && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}><T i18nKey="cancel">#</T></button>}
|
</svg>
|
||||||
|
) : this.props.community ? (
|
||||||
|
capitalizeFirstLetter(i18n.t('save'))
|
||||||
|
) : (
|
||||||
|
capitalizeFirstLetter(i18n.t('create'))
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{this.props.community && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.handleCancel)}
|
||||||
|
>
|
||||||
|
<T i18nKey="cancel">#</T>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -174,7 +247,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
return;
|
return;
|
||||||
} else if (op == UserOperation.ListCategories){
|
} else if (op == UserOperation.ListCategories) {
|
||||||
let res: ListCategoriesResponse = msg;
|
let res: ListCategoriesResponse = msg;
|
||||||
this.state.categories = res.categories;
|
this.state.categories = res.categories;
|
||||||
if (!this.props.community) {
|
if (!this.props.community) {
|
||||||
|
@ -185,7 +258,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||||
let res: CommunityResponse = msg;
|
let res: CommunityResponse = msg;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
this.props.onCreate(res.community);
|
this.props.onCreate(res.community);
|
||||||
}
|
}
|
||||||
// TODO is ths necessary
|
// TODO is ths necessary
|
||||||
else if (op == UserOperation.EditCommunity) {
|
else if (op == UserOperation.EditCommunity) {
|
||||||
let res: CommunityResponse = msg;
|
let res: CommunityResponse = msg;
|
||||||
|
@ -193,5 +266,4 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||||
this.props.onEdit(res.community);
|
this.props.onEdit(res.community);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,30 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, CommunityUser, UserView, SortType, Post, GetPostsForm, ListingType, GetPostsResponse, CreatePostLikeResponse } from '../interfaces';
|
import {
|
||||||
import { WebSocketService } from '../services';
|
UserOperation,
|
||||||
|
Community as CommunityI,
|
||||||
|
GetCommunityResponse,
|
||||||
|
CommunityResponse,
|
||||||
|
CommunityUser,
|
||||||
|
UserView,
|
||||||
|
SortType,
|
||||||
|
Post,
|
||||||
|
GetPostsForm,
|
||||||
|
ListingType,
|
||||||
|
GetPostsResponse,
|
||||||
|
CreatePostLikeResponse,
|
||||||
|
} from '../interfaces';
|
||||||
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { PostListings } from './post-listings';
|
import { PostListings } from './post-listings';
|
||||||
|
import { SortSelect } from './sort-select';
|
||||||
import { Sidebar } from './sidebar';
|
import { Sidebar } from './sidebar';
|
||||||
import { msgOp, routeSortTypeToEnum, fetchLimit, postRefetchSeconds } from '../utils';
|
import {
|
||||||
|
msgOp,
|
||||||
|
routeSortTypeToEnum,
|
||||||
|
fetchLimit,
|
||||||
|
postRefetchSeconds,
|
||||||
|
} from '../utils';
|
||||||
import { T, i18n } from 'inferno-i18next';
|
import { T, i18n } from 'inferno-i18next';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -21,7 +40,6 @@ interface State {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Community extends Component<any, State> {
|
export class Community extends Component<any, State> {
|
||||||
|
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
private postFetcher: any;
|
private postFetcher: any;
|
||||||
private emptyState: State = {
|
private emptyState: State = {
|
||||||
|
@ -49,38 +67,46 @@ export class Community extends Component<any, State> {
|
||||||
posts: [],
|
posts: [],
|
||||||
sort: this.getSortTypeFromProps(this.props),
|
sort: this.getSortTypeFromProps(this.props),
|
||||||
page: this.getPageFromProps(this.props),
|
page: this.getPageFromProps(this.props),
|
||||||
}
|
};
|
||||||
|
|
||||||
getSortTypeFromProps(props: any): SortType {
|
getSortTypeFromProps(props: any): SortType {
|
||||||
return (props.match.params.sort) ?
|
return props.match.params.sort
|
||||||
routeSortTypeToEnum(props.match.params.sort) :
|
? routeSortTypeToEnum(props.match.params.sort)
|
||||||
SortType.Hot;
|
: UserService.Instance.user
|
||||||
|
? UserService.Instance.user.default_sort_type
|
||||||
|
: SortType.Hot;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPageFromProps(props: any): number {
|
getPageFromProps(props: any): number {
|
||||||
return (props.match.params.page) ? Number(props.match.params.page) : 1;
|
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.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(
|
||||||
.subscribe(
|
retryWhen(errors =>
|
||||||
(msg) => this.parseMessage(msg),
|
errors.pipe(
|
||||||
(err) => console.error(err),
|
delay(3000),
|
||||||
|
take(10)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
msg => this.parseMessage(msg),
|
||||||
|
err => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.state.communityId) {
|
if (this.state.communityId) {
|
||||||
WebSocketService.Instance.getCommunity(this.state.communityId);
|
WebSocketService.Instance.getCommunity(this.state.communityId);
|
||||||
} else if (this.state.communityName) {
|
} else if (this.state.communityName) {
|
||||||
WebSocketService.Instance.getCommunityByName(this.state.communityName);
|
WebSocketService.Instance.getCommunityByName(this.state.communityName);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.keepFetchingPosts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -90,10 +116,13 @@ export class Community extends Component<any, State> {
|
||||||
|
|
||||||
// Necessary for back button for some reason
|
// Necessary for back button for some reason
|
||||||
componentWillReceiveProps(nextProps: any) {
|
componentWillReceiveProps(nextProps: any) {
|
||||||
if (nextProps.history.action == 'POP') {
|
if (
|
||||||
this.state = this.emptyState;
|
nextProps.history.action == 'POP' ||
|
||||||
|
nextProps.history.action == 'PUSH'
|
||||||
|
) {
|
||||||
this.state.sort = this.getSortTypeFromProps(nextProps);
|
this.state.sort = this.getSortTypeFromProps(nextProps);
|
||||||
this.state.page = this.getPageFromProps(nextProps);
|
this.state.page = this.getPageFromProps(nextProps);
|
||||||
|
this.setState(this.state);
|
||||||
this.fetchPosts();
|
this.fetchPosts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,92 +130,105 @@ export class Community extends Component<any, State> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{this.state.loading ?
|
{this.state.loading ? (
|
||||||
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
<h5>
|
||||||
<div class="row">
|
<svg class="icon icon-spinner spin">
|
||||||
<div class="col-12 col-md-8">
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
<h5>{this.state.community.title}
|
</svg>
|
||||||
{this.state.community.removed &&
|
|
||||||
<small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
|
|
||||||
}
|
|
||||||
{this.state.community.nsfw &&
|
|
||||||
<small className="ml-2 text-muted font-italic"><T i18nKey="nsfw">#</T></small>
|
|
||||||
}
|
|
||||||
</h5>
|
</h5>
|
||||||
{this.selects()}
|
) : (
|
||||||
<PostListings posts={this.state.posts} />
|
<div class="row">
|
||||||
{this.paginator()}
|
<div class="col-12 col-md-8">
|
||||||
|
<h5>
|
||||||
|
{this.state.community.title}
|
||||||
|
{this.state.community.removed && (
|
||||||
|
<small className="ml-2 text-muted font-italic">
|
||||||
|
<T i18nKey="removed">#</T>
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
{this.state.community.nsfw && (
|
||||||
|
<small className="ml-2 text-muted font-italic">
|
||||||
|
<T i18nKey="nsfw">#</T>
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</h5>
|
||||||
|
{this.selects()}
|
||||||
|
<PostListings posts={this.state.posts} />
|
||||||
|
{this.paginator()}
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<Sidebar
|
||||||
|
community={this.state.community}
|
||||||
|
moderators={this.state.moderators}
|
||||||
|
admins={this.state.admins}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4">
|
)}
|
||||||
<Sidebar
|
|
||||||
community={this.state.community}
|
|
||||||
moderators={this.state.moderators}
|
|
||||||
admins={this.state.admins}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
selects() {
|
selects() {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div class="mb-2">
|
||||||
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto">
|
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
|
||||||
<option disabled><T i18nKey="sort_type">#</T></option>
|
|
||||||
<option value={SortType.Hot}><T i18nKey="hot">#</T></option>
|
|
||||||
<option value={SortType.New}><T i18nKey="new">#</T></option>
|
|
||||||
<option disabled>──────</option>
|
|
||||||
<option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
|
|
||||||
<option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
|
|
||||||
<option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
|
|
||||||
<option value={SortType.TopYear}><T i18nKey="year">#</T></option>
|
|
||||||
<option value={SortType.TopAll}><T i18nKey="all">#</T></option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
paginator() {
|
paginator() {
|
||||||
return (
|
return (
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
{this.state.page > 1 &&
|
{this.state.page > 1 && (
|
||||||
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
|
<button
|
||||||
}
|
class="btn btn-sm btn-secondary mr-1"
|
||||||
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
|
>
|
||||||
|
<T i18nKey="prev">#</T>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
|
>
|
||||||
|
<T i18nKey="next">#</T>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPage(i: Community) {
|
nextPage(i: Community) {
|
||||||
i.state.page++;
|
i.state.page++;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchPosts();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPage(i: Community) {
|
prevPage(i: Community) {
|
||||||
i.state.page--;
|
i.state.page--;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchPosts();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortChange(i: Community, event: any) {
|
handleSortChange(val: SortType) {
|
||||||
i.state.sort = Number(event.target.value);
|
this.state.sort = val;
|
||||||
i.state.page = 1;
|
this.state.page = 1;
|
||||||
i.setState(i.state);
|
this.state.loading = true;
|
||||||
i.updateUrl();
|
this.setState(this.state);
|
||||||
i.fetchPosts();
|
this.updateUrl();
|
||||||
window.scrollTo(0,0);
|
this.fetchPosts();
|
||||||
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrl() {
|
updateUrl() {
|
||||||
let sortStr = SortType[this.state.sort].toLowerCase();
|
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||||
this.props.history.push(`/c/${this.state.community.name}/sort/${sortStr}/page/${this.state.page}`);
|
this.props.history.push(
|
||||||
|
`/c/${this.state.community.name}/sort/${sortStr}/page/${this.state.page}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
keepFetchingPosts() {
|
keepFetchingPosts() {
|
||||||
|
@ -201,7 +243,7 @@ export class Community extends Component<any, State> {
|
||||||
sort: SortType[this.state.sort],
|
sort: SortType[this.state.sort],
|
||||||
type_: ListingType[ListingType.Community],
|
type_: ListingType[ListingType.Community],
|
||||||
community_id: this.state.community.id,
|
community_id: this.state.community.id,
|
||||||
}
|
};
|
||||||
WebSocketService.Instance.getPosts(getPostsForm);
|
WebSocketService.Instance.getPosts(getPostsForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,7 +260,7 @@ export class Community extends Component<any, State> {
|
||||||
this.state.admins = res.admins;
|
this.state.admins = res.admins;
|
||||||
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.keepFetchingPosts();
|
||||||
} else if (op == UserOperation.EditCommunity) {
|
} else if (op == UserOperation.EditCommunity) {
|
||||||
let res: CommunityResponse = msg;
|
let res: CommunityResponse = msg;
|
||||||
this.state.community = res.community;
|
this.state.community = res.community;
|
||||||
|
@ -226,7 +268,8 @@ export class Community extends Component<any, State> {
|
||||||
} else if (op == UserOperation.FollowCommunity) {
|
} else if (op == UserOperation.FollowCommunity) {
|
||||||
let res: CommunityResponse = msg;
|
let res: CommunityResponse = msg;
|
||||||
this.state.community.subscribed = res.community.subscribed;
|
this.state.community.subscribed = res.community.subscribed;
|
||||||
this.state.community.number_of_subscribers = res.community.number_of_subscribers;
|
this.state.community.number_of_subscribers =
|
||||||
|
res.community.number_of_subscribers;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.GetPosts) {
|
} else if (op == UserOperation.GetPosts) {
|
||||||
let res: GetPostsResponse = msg;
|
let res: GetPostsResponse = msg;
|
||||||
|
@ -244,4 +287,3 @@ export class Community extends Component<any, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,14 +6,15 @@ import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
export class CreateCommunity extends Component<any, any> {
|
export class CreateCommunity extends Component<any, any> {
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
|
this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.title = `${i18n.t('create_community')} - ${WebSocketService.Instance.site.name}`;
|
document.title = `${i18n.t('create_community')} - ${
|
||||||
|
WebSocketService.Instance.site.name
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -21,17 +22,17 @@ export class CreateCommunity extends Component<any, any> {
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
<h5><T i18nKey="create_community">#</T></h5>
|
<h5>
|
||||||
<CommunityForm onCreate={this.handleCommunityCreate}/>
|
<T i18nKey="create_community">#</T>
|
||||||
|
</h5>
|
||||||
|
<CommunityForm onCreate={this.handleCommunityCreate} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommunityCreate(community: Community) {
|
handleCommunityCreate(community: Community) {
|
||||||
this.props.history.push(`/c/${community.name}`);
|
this.props.history.push(`/c/${community.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,14 +6,15 @@ import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
export class CreatePost extends Component<any, any> {
|
export class CreatePost extends Component<any, any> {
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.handlePostCreate = this.handlePostCreate.bind(this);
|
this.handlePostCreate = this.handlePostCreate.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.title = `${i18n.t('create_post')} - ${WebSocketService.Instance.site.name}`;
|
document.title = `${i18n.t('create_post')} - ${
|
||||||
|
WebSocketService.Instance.site.name
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -21,21 +22,23 @@ export class CreatePost extends Component<any, any> {
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
<h5><T i18nKey="create_post">#</T></h5>
|
<h5>
|
||||||
|
<T i18nKey="create_post">#</T>
|
||||||
|
</h5>
|
||||||
<PostForm onCreate={this.handlePostCreate} params={this.params} />
|
<PostForm onCreate={this.handlePostCreate} params={this.params} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get params(): PostFormParams {
|
get params(): PostFormParams {
|
||||||
let urlParams = new URLSearchParams(this.props.location.search);
|
let urlParams = new URLSearchParams(this.props.location.search);
|
||||||
let params: PostFormParams = {
|
let params: PostFormParams = {
|
||||||
name: urlParams.get("name"),
|
name: urlParams.get('name'),
|
||||||
community: urlParams.get("community") || this.prevCommunityName,
|
community: urlParams.get('community') || this.prevCommunityName,
|
||||||
body: urlParams.get("body"),
|
body: urlParams.get('body'),
|
||||||
url: urlParams.get("url"),
|
url: urlParams.get('url'),
|
||||||
};
|
};
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
|
@ -46,9 +49,9 @@ export class CreatePost extends Component<any, any> {
|
||||||
return this.props.match.params.name;
|
return this.props.match.params.name;
|
||||||
} else if (this.props.location.state) {
|
} else if (this.props.location.state) {
|
||||||
let lastLocation = this.props.location.state.prevPath;
|
let lastLocation = this.props.location.state.prevPath;
|
||||||
if (lastLocation.includes("/c/")) {
|
if (lastLocation.includes('/c/')) {
|
||||||
return lastLocation.split("/c/")[1];
|
return lastLocation.split('/c/')[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -57,5 +60,3 @@ export class CreatePost extends Component<any, any> {
|
||||||
this.props.history.push(`/post/${id}`);
|
this.props.history.push(`/post/${id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,6 @@ import { version } from '../version';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
export class Footer extends Component<any, any> {
|
export class Footer extends Component<any, any> {
|
||||||
|
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
}
|
}
|
||||||
|
@ -20,16 +18,24 @@ export class Footer extends Component<any, any> {
|
||||||
<span class="navbar-text">{version}</span>
|
<span class="navbar-text">{version}</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<Link class="nav-link" to="/modlog"><T i18nKey="modlog">#</T></Link>
|
<Link class="nav-link" to="/modlog">
|
||||||
|
<T i18nKey="modlog">#</T>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href={`${repoUrl}/blob/master/docs/api.md`}><T i18nKey="api">#</T></a>
|
<a class="nav-link" href={`${repoUrl}/blob/master/docs/api.md`}>
|
||||||
|
<T i18nKey="api">#</T>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<Link class="nav-link" to="/sponsors"><T i18nKey="sponsors">#</T></Link>
|
<Link class="nav-link" to="/sponsors">
|
||||||
|
<T i18nKey="sponsors">#</T>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href={repoUrl}><T i18nKey="code">#</T></a>
|
<a class="nav-link" href={repoUrl}>
|
||||||
|
<T i18nKey="code">#</T>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,4 +43,3 @@ export class Footer extends Component<any, any> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,47 +1,76 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { UserOperation, Comment, SortType, GetRepliesForm, GetRepliesResponse, CommentResponse } from '../interfaces';
|
import {
|
||||||
|
UserOperation,
|
||||||
|
Comment,
|
||||||
|
SortType,
|
||||||
|
GetRepliesForm,
|
||||||
|
GetRepliesResponse,
|
||||||
|
GetUserMentionsForm,
|
||||||
|
GetUserMentionsResponse,
|
||||||
|
UserMentionResponse,
|
||||||
|
CommentResponse,
|
||||||
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { msgOp } from '../utils';
|
import { msgOp } from '../utils';
|
||||||
import { CommentNodes } from './comment-nodes';
|
import { CommentNodes } from './comment-nodes';
|
||||||
|
import { SortSelect } from './sort-select';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
|
enum UnreadOrAll {
|
||||||
|
Unread,
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
enum UnreadType {
|
enum UnreadType {
|
||||||
Unread, All
|
Both,
|
||||||
|
Replies,
|
||||||
|
Mentions,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InboxState {
|
interface InboxState {
|
||||||
|
unreadOrAll: UnreadOrAll;
|
||||||
unreadType: UnreadType;
|
unreadType: UnreadType;
|
||||||
replies: Array<Comment>;
|
replies: Array<Comment>;
|
||||||
|
mentions: Array<Comment>;
|
||||||
sort: SortType;
|
sort: SortType;
|
||||||
page: number;
|
page: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Inbox extends Component<any, InboxState> {
|
export class Inbox extends Component<any, InboxState> {
|
||||||
|
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
private emptyState: InboxState = {
|
private emptyState: InboxState = {
|
||||||
unreadType: UnreadType.Unread,
|
unreadOrAll: UnreadOrAll.Unread,
|
||||||
|
unreadType: UnreadType.Both,
|
||||||
replies: [],
|
replies: [],
|
||||||
|
mentions: [],
|
||||||
sort: SortType.New,
|
sort: SortType.New,
|
||||||
page: 1,
|
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.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(
|
||||||
.subscribe(
|
retryWhen(errors =>
|
||||||
(msg) => this.parseMessage(msg),
|
errors.pipe(
|
||||||
(err) => console.error(err),
|
delay(3000),
|
||||||
|
take(10)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
msg => this.parseMessage(msg),
|
||||||
|
err => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
this.refetch();
|
this.refetch();
|
||||||
}
|
}
|
||||||
|
@ -51,7 +80,9 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.title = `/u/${UserService.Instance.user.username} ${i18n.t('inbox')} - ${WebSocketService.Instance.site.name}`;
|
document.title = `/u/${UserService.Instance.user.username} ${i18n.t(
|
||||||
|
'inbox'
|
||||||
|
)} - ${WebSocketService.Instance.site.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -61,52 +92,125 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<span><T i18nKey="inbox_for" interpolation={{user: user.username}}>#<Link to={`/u/${user.username}`}>#</Link></T></span>
|
<span>
|
||||||
|
<T i18nKey="inbox_for" interpolation={{ user: user.username }}>
|
||||||
|
#<Link to={`/u/${user.username}`}>#</Link>
|
||||||
|
</T>
|
||||||
|
</span>
|
||||||
</h5>
|
</h5>
|
||||||
{this.state.replies.length > 0 && this.state.unreadType == UnreadType.Unread &&
|
{this.state.replies.length + this.state.mentions.length > 0 &&
|
||||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
this.state.unreadOrAll == UnreadOrAll.Unread && (
|
||||||
<li className="list-inline-item">
|
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||||
<span class="pointer" onClick={this.markAllAsRead}><T i18nKey="mark_all_as_read">#</T></span>
|
<li className="list-inline-item">
|
||||||
</li>
|
<span class="pointer" onClick={this.markAllAsRead}>
|
||||||
</ul>
|
<T i18nKey="mark_all_as_read">#</T>
|
||||||
}
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
{this.selects()}
|
{this.selects()}
|
||||||
{this.replies()}
|
{this.state.unreadType == UnreadType.Both && this.both()}
|
||||||
|
{this.state.unreadType == UnreadType.Replies && this.replies()}
|
||||||
|
{this.state.unreadType == UnreadType.Mentions && this.mentions()}
|
||||||
{this.paginator()}
|
{this.paginator()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
selects() {
|
selects() {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<select value={this.state.unreadType} onChange={linkEvent(this, this.handleUnreadTypeChange)} class="custom-select custom-select-sm w-auto">
|
<select
|
||||||
<option disabled><T i18nKey="type">#</T></option>
|
value={this.state.unreadOrAll}
|
||||||
<option value={UnreadType.Unread}><T i18nKey="unread">#</T></option>
|
onChange={linkEvent(this, this.handleUnreadOrAllChange)}
|
||||||
<option value={UnreadType.All}><T i18nKey="all">#</T></option>
|
class="custom-select custom-select-sm w-auto mr-2"
|
||||||
|
>
|
||||||
|
<option disabled>
|
||||||
|
<T i18nKey="type">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={UnreadOrAll.Unread}>
|
||||||
|
<T i18nKey="unread">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={UnreadOrAll.All}>
|
||||||
|
<T i18nKey="all">#</T>
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
|
<select
|
||||||
<option disabled><T i18nKey="sort_type">#</T></option>
|
value={this.state.unreadType}
|
||||||
<option value={SortType.New}><T i18nKey="new">#</T></option>
|
onChange={linkEvent(this, this.handleUnreadTypeChange)}
|
||||||
<option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
|
class="custom-select custom-select-sm w-auto mr-2"
|
||||||
<option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
|
>
|
||||||
<option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
|
<option disabled>
|
||||||
<option value={SortType.TopYear}><T i18nKey="year">#</T></option>
|
<T i18nKey="type">#</T>
|
||||||
<option value={SortType.TopAll}><T i18nKey="all">#</T></option>
|
</option>
|
||||||
|
<option value={UnreadType.Both}>
|
||||||
|
<T i18nKey="both">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={UnreadType.Replies}>
|
||||||
|
<T i18nKey="replies">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={UnreadType.Mentions}>
|
||||||
|
<T i18nKey="mentions">#</T>
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
<SortSelect
|
||||||
|
sort={this.state.sort}
|
||||||
|
onChange={this.handleSortChange}
|
||||||
|
hideHot
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
both() {
|
||||||
|
let combined: Array<{
|
||||||
|
type_: string;
|
||||||
|
data: Comment;
|
||||||
|
}> = [];
|
||||||
|
let replies = this.state.replies.map(e => {
|
||||||
|
return { type_: 'replies', data: e };
|
||||||
|
});
|
||||||
|
let mentions = this.state.mentions.map(e => {
|
||||||
|
return { type_: 'mentions', data: e };
|
||||||
|
});
|
||||||
|
|
||||||
|
combined.push(...replies);
|
||||||
|
combined.push(...mentions);
|
||||||
|
|
||||||
|
// Sort it
|
||||||
|
if (this.state.sort == SortType.New) {
|
||||||
|
combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
|
||||||
|
} else {
|
||||||
|
combined.sort((a, b) => b.data.score - a.data.score);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{combined.map(i => (
|
||||||
|
<CommentNodes nodes={[{ comment: i.data }]} noIndent markable />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
replies() {
|
replies() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.replies.map(reply =>
|
{this.state.replies.map(reply => (
|
||||||
<CommentNodes nodes={[{comment: reply}]} noIndent markable />
|
<CommentNodes nodes={[{ comment: reply }]} noIndent markable />
|
||||||
)}
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mentions() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{this.state.mentions.map(mention => (
|
||||||
|
<CommentNodes nodes={[{ comment: mention }]} noIndent markable />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -114,26 +218,43 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
paginator() {
|
paginator() {
|
||||||
return (
|
return (
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{this.state.page > 1 &&
|
{this.state.page > 1 && (
|
||||||
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
|
<button
|
||||||
}
|
class="btn btn-sm btn-secondary mr-1"
|
||||||
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
|
>
|
||||||
|
<T i18nKey="prev">#</T>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
|
>
|
||||||
|
<T i18nKey="next">#</T>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPage(i: Inbox) {
|
nextPage(i: Inbox) {
|
||||||
i.state.page++;
|
i.state.page++;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.refetch();
|
i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPage(i: Inbox) {
|
prevPage(i: Inbox) {
|
||||||
i.state.page--;
|
i.state.page--;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.refetch();
|
i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleUnreadOrAllChange(i: Inbox, event: any) {
|
||||||
|
i.state.unreadOrAll = Number(event.target.value);
|
||||||
|
i.state.page = 1;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
handleUnreadTypeChange(i: Inbox, event: any) {
|
handleUnreadTypeChange(i: Inbox, event: any) {
|
||||||
i.state.unreadType = Number(event.target.value);
|
i.state.unreadType = Number(event.target.value);
|
||||||
i.state.page = 1;
|
i.state.page = 1;
|
||||||
|
@ -142,20 +263,28 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
refetch() {
|
refetch() {
|
||||||
let form: GetRepliesForm = {
|
let repliesForm: GetRepliesForm = {
|
||||||
sort: SortType[this.state.sort],
|
sort: SortType[this.state.sort],
|
||||||
unread_only: (this.state.unreadType == UnreadType.Unread),
|
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
|
||||||
page: this.state.page,
|
page: this.state.page,
|
||||||
limit: 9999,
|
limit: 9999,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.getReplies(form);
|
WebSocketService.Instance.getReplies(repliesForm);
|
||||||
|
|
||||||
|
let userMentionsForm: GetUserMentionsForm = {
|
||||||
|
sort: SortType[this.state.sort],
|
||||||
|
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
|
||||||
|
page: this.state.page,
|
||||||
|
limit: 9999,
|
||||||
|
};
|
||||||
|
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortChange(i: Inbox, event: any) {
|
handleSortChange(val: SortType) {
|
||||||
i.state.sort = Number(event.target.value);
|
this.state.sort = val;
|
||||||
i.state.page = 1;
|
this.state.page = 1;
|
||||||
i.setState(i.state);
|
this.setState(this.state);
|
||||||
i.refetch();
|
this.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
markAllAsRead() {
|
markAllAsRead() {
|
||||||
|
@ -168,11 +297,22 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
alert(i18n.t(msg.error));
|
alert(i18n.t(msg.error));
|
||||||
return;
|
return;
|
||||||
} else if (op == UserOperation.GetReplies || op == UserOperation.MarkAllAsRead) {
|
} else if (op == UserOperation.GetReplies) {
|
||||||
let res: GetRepliesResponse = msg;
|
let res: GetRepliesResponse = msg;
|
||||||
this.state.replies = res.replies;
|
this.state.replies = res.replies;
|
||||||
this.sendRepliesCount();
|
this.sendUnreadCount();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (op == UserOperation.GetUserMentions) {
|
||||||
|
let res: GetUserMentionsResponse = msg;
|
||||||
|
this.state.mentions = res.mentions;
|
||||||
|
this.sendUnreadCount();
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (op == UserOperation.MarkAllAsRead) {
|
||||||
|
this.state.replies = [];
|
||||||
|
this.state.mentions = [];
|
||||||
|
window.scrollTo(0, 0);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.EditComment) {
|
} else if (op == UserOperation.EditComment) {
|
||||||
let res: CommentResponse = msg;
|
let res: CommentResponse = msg;
|
||||||
|
@ -187,14 +327,38 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
found.score = res.comment.score;
|
found.score = res.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.unreadType == UnreadType.Unread && res.comment.read) {
|
if (this.state.unreadOrAll == UnreadOrAll.Unread && res.comment.read) {
|
||||||
this.state.replies = this.state.replies.filter(r => r.id !== res.comment.id);
|
this.state.replies = this.state.replies.filter(
|
||||||
|
r => r.id !== res.comment.id
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
let found = this.state.replies.find(c => c.id == res.comment.id);
|
let found = this.state.replies.find(c => c.id == res.comment.id);
|
||||||
found.read = res.comment.read;
|
found.read = res.comment.read;
|
||||||
}
|
}
|
||||||
this.sendRepliesCount();
|
this.sendUnreadCount();
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (op == UserOperation.EditUserMention) {
|
||||||
|
let res: UserMentionResponse = msg;
|
||||||
|
|
||||||
|
let found = this.state.mentions.find(c => c.id == res.mention.id);
|
||||||
|
found.content = res.mention.content;
|
||||||
|
found.updated = res.mention.updated;
|
||||||
|
found.removed = res.mention.removed;
|
||||||
|
found.deleted = res.mention.deleted;
|
||||||
|
found.upvotes = res.mention.upvotes;
|
||||||
|
found.downvotes = res.mention.downvotes;
|
||||||
|
found.score = res.mention.score;
|
||||||
|
|
||||||
|
// If youre in the unread view, just remove it from the list
|
||||||
|
if (this.state.unreadOrAll == UnreadOrAll.Unread && res.mention.read) {
|
||||||
|
this.state.mentions = this.state.mentions.filter(
|
||||||
|
r => r.id !== res.mention.id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let found = this.state.mentions.find(c => c.id == res.mention.id);
|
||||||
|
found.read = res.mention.read;
|
||||||
|
}
|
||||||
|
this.sendUnreadCount();
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.CreateComment) {
|
} else if (op == UserOperation.CreateComment) {
|
||||||
// let res: CommentResponse = msg;
|
// let res: CommentResponse = msg;
|
||||||
|
@ -208,18 +372,24 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.CreateCommentLike) {
|
} else if (op == UserOperation.CreateCommentLike) {
|
||||||
let res: CommentResponse = msg;
|
let res: CommentResponse = msg;
|
||||||
let found: Comment = this.state.replies.find(c => c.id === res.comment.id);
|
let found: Comment = this.state.replies.find(
|
||||||
|
c => c.id === res.comment.id
|
||||||
|
);
|
||||||
found.score = res.comment.score;
|
found.score = res.comment.score;
|
||||||
found.upvotes = res.comment.upvotes;
|
found.upvotes = res.comment.upvotes;
|
||||||
found.downvotes = res.comment.downvotes;
|
found.downvotes = res.comment.downvotes;
|
||||||
if (res.comment.my_vote !== null)
|
if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
|
||||||
found.my_vote = res.comment.my_vote;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendRepliesCount() {
|
sendUnreadCount() {
|
||||||
UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: this.state.replies.filter(r => !r.read).length});
|
let count =
|
||||||
|
this.state.replies.filter(r => !r.read).length +
|
||||||
|
this.state.mentions.filter(r => !r.read).length;
|
||||||
|
UserService.Instance.sub.next({
|
||||||
|
user: UserService.Instance.user,
|
||||||
|
unreadCount: count,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { ListingType } from '../interfaces';
|
||||||
|
import { UserService } from '../services';
|
||||||
|
|
||||||
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
|
interface ListingTypeSelectProps {
|
||||||
|
type_: ListingType;
|
||||||
|
onChange?(val: ListingType): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListingTypeSelectState {
|
||||||
|
type_: ListingType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ListingTypeSelect extends Component<
|
||||||
|
ListingTypeSelectProps,
|
||||||
|
ListingTypeSelectState
|
||||||
|
> {
|
||||||
|
private emptyState: ListingTypeSelectState = {
|
||||||
|
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={`btn btn-sm btn-secondary
|
||||||
|
${this.state.type_ == ListingType.Subscribed && 'active'}
|
||||||
|
${UserService.Instance.user == undefined ? 'disabled' : 'pointer'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value={ListingType.Subscribed}
|
||||||
|
checked={this.state.type_ == ListingType.Subscribed}
|
||||||
|
onChange={linkEvent(this, this.handleTypeChange)}
|
||||||
|
disabled={UserService.Instance.user == undefined}
|
||||||
|
/>
|
||||||
|
{i18n.t('subscribed')}
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className={`pointer btn btn-sm btn-secondary ${this.state.type_ ==
|
||||||
|
ListingType.All && 'active'}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value={ListingType.All}
|
||||||
|
checked={this.state.type_ == ListingType.All}
|
||||||
|
onChange={linkEvent(this, this.handleTypeChange)}
|
||||||
|
/>
|
||||||
|
{i18n.t('all')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTypeChange(i: ListingTypeSelect, event: any) {
|
||||||
|
i.state.type_ = Number(event.target.value);
|
||||||
|
i.setState(i.state);
|
||||||
|
i.props.onChange(i.state.type_);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,12 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { LoginForm, RegisterForm, LoginResponse, UserOperation } from '../interfaces';
|
import {
|
||||||
|
LoginForm,
|
||||||
|
RegisterForm,
|
||||||
|
LoginResponse,
|
||||||
|
UserOperation,
|
||||||
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { msgOp } from '../utils';
|
import { msgOp } from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
@ -14,14 +19,13 @@ interface State {
|
||||||
registerLoading: boolean;
|
registerLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Login extends Component<any, State> {
|
export class Login extends Component<any, State> {
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
|
|
||||||
emptyState: State = {
|
emptyState: State = {
|
||||||
loginForm: {
|
loginForm: {
|
||||||
username_or_email: undefined,
|
username_or_email: undefined,
|
||||||
password: undefined
|
password: undefined,
|
||||||
},
|
},
|
||||||
registerForm: {
|
registerForm: {
|
||||||
username: undefined,
|
username: undefined,
|
||||||
|
@ -32,7 +36,7 @@ export class Login extends Component<any, State> {
|
||||||
},
|
},
|
||||||
loginLoading: false,
|
loginLoading: false,
|
||||||
registerLoading: false,
|
registerLoading: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -40,12 +44,19 @@ export class Login extends Component<any, State> {
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
|
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(
|
||||||
.subscribe(
|
retryWhen(errors =>
|
||||||
(msg) => this.parseMessage(msg),
|
errors.pipe(
|
||||||
(err) => console.error(err),
|
delay(3000),
|
||||||
() => console.log("complete")
|
take(10)
|
||||||
);
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
msg => this.parseMessage(msg),
|
||||||
|
err => console.error(err),
|
||||||
|
() => console.log('complete')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -53,22 +64,20 @@ export class Login extends Component<any, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.title = `${i18n.t('login')} - ${WebSocketService.Instance.site.name}`;
|
document.title = `${i18n.t('login')} - ${
|
||||||
|
WebSocketService.Instance.site.name
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 mb-4">
|
<div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
|
||||||
{this.loginForm()}
|
<div class="col-12 col-lg-6">{this.registerForm()}</div>
|
||||||
</div>
|
|
||||||
<div class="col-12 col-lg-6">
|
|
||||||
{this.registerForm()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
loginForm() {
|
loginForm() {
|
||||||
|
@ -77,21 +86,45 @@ export class Login extends Component<any, State> {
|
||||||
<form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
|
||||||
<h5>Login</h5>
|
<h5>Login</h5>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="email_or_username">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
|
<T i18nKey="email_or_username">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="text" class="form-control" value={this.state.loginForm.username_or_email} onInput={linkEvent(this, this.handleLoginUsernameChange)} required minLength={3} />
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
value={this.state.loginForm.username_or_email}
|
||||||
|
onInput={linkEvent(this, this.handleLoginUsernameChange)}
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="password">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
|
<T i18nKey="password">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="password" value={this.state.loginForm.password} onInput={linkEvent(this, this.handleLoginPasswordChange)} class="form-control" required />
|
<input
|
||||||
|
type="password"
|
||||||
|
value={this.state.loginForm.password}
|
||||||
|
onInput={linkEvent(this, this.handleLoginPasswordChange)}
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<button type="submit" class="btn btn-secondary">{this.state.loginLoading ?
|
<button type="submit" class="btn btn-secondary">
|
||||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('login')}</button>
|
{this.state.loginLoading ? (
|
||||||
|
<svg class="icon icon-spinner spin">
|
||||||
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
i18n.t('login')
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -101,43 +134,95 @@ export class Login extends Component<any, State> {
|
||||||
registerForm() {
|
registerForm() {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
|
||||||
<h5><T i18nKey="sign_up">#</T></h5>
|
<h5>
|
||||||
|
<T i18nKey="sign_up">#</T>
|
||||||
|
</h5>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="username">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
|
<T i18nKey="username">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="text" class="form-control" value={this.state.registerForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} maxLength={20} pattern="[a-zA-Z0-9_]+" />
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
value={this.state.registerForm.username}
|
||||||
|
onInput={linkEvent(this, this.handleRegisterUsernameChange)}
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
maxLength={20}
|
||||||
|
pattern="[a-zA-Z0-9_]+"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="email">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
|
<T i18nKey="email">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="email" class="form-control" placeholder={i18n.t('optional')} value={this.state.registerForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} />
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
placeholder={i18n.t('optional')}
|
||||||
|
value={this.state.registerForm.email}
|
||||||
|
onInput={linkEvent(this, this.handleRegisterEmailChange)}
|
||||||
|
minLength={3}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="password">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
|
<T i18nKey="password">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="password" value={this.state.registerForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required />
|
<input
|
||||||
|
type="password"
|
||||||
|
value={this.state.registerForm.password}
|
||||||
|
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="verify_password">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
|
<T i18nKey="verify_password">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="password" value={this.state.registerForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required />
|
<input
|
||||||
|
type="password"
|
||||||
|
value={this.state.registerForm.password_verify}
|
||||||
|
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" checked={this.state.registerForm.show_nsfw} onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}/>
|
<input
|
||||||
<label class="form-check-label"><T i18nKey="show_nsfw">#</T></label>
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={this.state.registerForm.show_nsfw}
|
||||||
|
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
|
||||||
|
/>
|
||||||
|
<label class="form-check-label">
|
||||||
|
<T i18nKey="show_nsfw">#</T>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<button type="submit" class="btn btn-secondary">{this.state.registerLoading ?
|
<button type="submit" class="btn btn-secondary">
|
||||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('sign_up')}</button>
|
{this.state.registerLoading ? (
|
||||||
|
<svg class="icon icon-spinner spin">
|
||||||
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
i18n.t('sign_up')
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -217,5 +302,4 @@ export class Login extends Component<any, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,37 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse, ListingType, SiteResponse, GetPostsResponse, CreatePostLikeResponse, Post, GetPostsForm } from '../interfaces';
|
import {
|
||||||
|
UserOperation,
|
||||||
|
CommunityUser,
|
||||||
|
GetFollowedCommunitiesResponse,
|
||||||
|
ListCommunitiesForm,
|
||||||
|
ListCommunitiesResponse,
|
||||||
|
Community,
|
||||||
|
SortType,
|
||||||
|
GetSiteResponse,
|
||||||
|
ListingType,
|
||||||
|
SiteResponse,
|
||||||
|
GetPostsResponse,
|
||||||
|
CreatePostLikeResponse,
|
||||||
|
Post,
|
||||||
|
GetPostsForm,
|
||||||
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { PostListings } from './post-listings';
|
import { PostListings } from './post-listings';
|
||||||
|
import { SortSelect } from './sort-select';
|
||||||
|
import { ListingTypeSelect } from './listing-type-select';
|
||||||
import { SiteForm } from './site-form';
|
import { SiteForm } from './site-form';
|
||||||
import { msgOp, repoUrl, mdToHtml, fetchLimit, routeSortTypeToEnum, routeListingTypeToEnum, postRefetchSeconds } from '../utils';
|
import {
|
||||||
|
msgOp,
|
||||||
|
repoUrl,
|
||||||
|
mdToHtml,
|
||||||
|
fetchLimit,
|
||||||
|
routeSortTypeToEnum,
|
||||||
|
routeListingTypeToEnum,
|
||||||
|
postRefetchSeconds,
|
||||||
|
} from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
|
@ -23,7 +48,6 @@ interface MainState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Main extends Component<any, MainState> {
|
export class Main extends Component<any, MainState> {
|
||||||
|
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
private postFetcher: any;
|
private postFetcher: any;
|
||||||
private emptyState: MainState = {
|
private emptyState: MainState = {
|
||||||
|
@ -52,24 +76,26 @@ export class Main extends Component<any, MainState> {
|
||||||
type_: this.getListingTypeFromProps(this.props),
|
type_: this.getListingTypeFromProps(this.props),
|
||||||
sort: this.getSortTypeFromProps(this.props),
|
sort: this.getSortTypeFromProps(this.props),
|
||||||
page: this.getPageFromProps(this.props),
|
page: this.getPageFromProps(this.props),
|
||||||
}
|
};
|
||||||
|
|
||||||
getListingTypeFromProps(props: any): ListingType {
|
getListingTypeFromProps(props: any): ListingType {
|
||||||
return (props.match.params.type) ?
|
return props.match.params.type
|
||||||
routeListingTypeToEnum(props.match.params.type) :
|
? routeListingTypeToEnum(props.match.params.type)
|
||||||
UserService.Instance.user ?
|
: UserService.Instance.user
|
||||||
ListingType.Subscribed :
|
? UserService.Instance.user.default_listing_type
|
||||||
ListingType.All;
|
: ListingType.All;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortTypeFromProps(props: any): SortType {
|
getSortTypeFromProps(props: any): SortType {
|
||||||
return (props.match.params.sort) ?
|
return props.match.params.sort
|
||||||
routeSortTypeToEnum(props.match.params.sort) :
|
? routeSortTypeToEnum(props.match.params.sort)
|
||||||
SortType.Hot;
|
: UserService.Instance.user
|
||||||
|
? UserService.Instance.user.default_sort_type
|
||||||
|
: SortType.Hot;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPageFromProps(props: any): number {
|
getPageFromProps(props: any): number {
|
||||||
return (props.match.params.page) ? Number(props.match.params.page) : 1;
|
return props.match.params.page ? Number(props.match.params.page) : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -77,14 +103,23 @@ export class Main extends Component<any, MainState> {
|
||||||
|
|
||||||
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.handleTypeChange = this.handleTypeChange.bind(this);
|
||||||
|
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(
|
||||||
.subscribe(
|
retryWhen(errors =>
|
||||||
(msg) => this.parseMessage(msg),
|
errors.pipe(
|
||||||
(err) => console.error(err),
|
delay(3000),
|
||||||
|
take(10)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
msg => this.parseMessage(msg),
|
||||||
|
err => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
WebSocketService.Instance.getSite();
|
WebSocketService.Instance.getSite();
|
||||||
|
|
||||||
|
@ -94,8 +129,8 @@ export class Main extends Component<any, MainState> {
|
||||||
|
|
||||||
let listCommunitiesForm: ListCommunitiesForm = {
|
let listCommunitiesForm: ListCommunitiesForm = {
|
||||||
sort: SortType[SortType.Hot],
|
sort: SortType[SortType.Hot],
|
||||||
limit: 6
|
limit: 6,
|
||||||
}
|
};
|
||||||
|
|
||||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||||
|
|
||||||
|
@ -109,7 +144,10 @@ export class Main extends Component<any, MainState> {
|
||||||
|
|
||||||
// Necessary for back button for some reason
|
// Necessary for back button for some reason
|
||||||
componentWillReceiveProps(nextProps: any) {
|
componentWillReceiveProps(nextProps: any) {
|
||||||
if (nextProps.history.action == 'POP' || nextProps.history.action == 'PUSH') {
|
if (
|
||||||
|
nextProps.history.action == 'POP' ||
|
||||||
|
nextProps.history.action == 'PUSH'
|
||||||
|
) {
|
||||||
this.state.type_ = this.getListingTypeFromProps(nextProps);
|
this.state.type_ = this.getListingTypeFromProps(nextProps);
|
||||||
this.state.sort = this.getSortTypeFromProps(nextProps);
|
this.state.sort = this.getSortTypeFromProps(nextProps);
|
||||||
this.state.page = this.getPageFromProps(nextProps);
|
this.state.page = this.getPageFromProps(nextProps);
|
||||||
|
@ -122,39 +160,47 @@ export class Main extends Component<any, MainState> {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-8">
|
<div class="col-12 col-md-8">{this.posts()}</div>
|
||||||
{this.posts()}
|
<div class="col-12 col-md-4">{this.my_sidebar()}</div>
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-4">
|
|
||||||
{this.my_sidebar()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
my_sidebar() {
|
my_sidebar() {
|
||||||
return(
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!this.state.loading &&
|
{!this.state.loading && (
|
||||||
<div>
|
<div>
|
||||||
<div class="card border-secondary mb-3">
|
<div class="card border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{this.trendingCommunities()}
|
{this.trendingCommunities()}
|
||||||
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
|
{UserService.Instance.user &&
|
||||||
<div>
|
this.state.subscribedCommunities.length > 0 && (
|
||||||
<h5>
|
<div>
|
||||||
<T i18nKey="subscribed_to_communities">#<Link class="text-white" to="/communities">#</Link></T>
|
<h5>
|
||||||
</h5>
|
<T i18nKey="subscribed_to_communities">
|
||||||
<ul class="list-inline">
|
#
|
||||||
{this.state.subscribedCommunities.map(community =>
|
<Link class="text-white" to="/communities">
|
||||||
<li class="list-inline-item"><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
|
#
|
||||||
)}
|
</Link>
|
||||||
</ul>
|
</T>
|
||||||
</div>
|
</h5>
|
||||||
}
|
<ul class="list-inline">
|
||||||
<Link class="btn btn-sm btn-secondary btn-block"
|
{this.state.subscribedCommunities.map(community => (
|
||||||
to="/create_community">
|
<li class="list-inline-item">
|
||||||
|
<Link to={`/c/${community.community_name}`}>
|
||||||
|
{community.community_name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Link
|
||||||
|
class="btn btn-sm btn-secondary btn-block"
|
||||||
|
to="/create_community"
|
||||||
|
>
|
||||||
<T i18nKey="create_a_community">#</T>
|
<T i18nKey="create_a_community">#</T>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -162,44 +208,54 @@ export class Main extends Component<any, MainState> {
|
||||||
{this.sidebar()}
|
{this.sidebar()}
|
||||||
{this.landing()}
|
{this.landing()}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
trendingCommunities() {
|
trendingCommunities() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h5>
|
<h5>
|
||||||
<T i18nKey="trending_communities">#<Link class="text-white" to="/communities">#</Link></T>
|
<T i18nKey="trending_communities">
|
||||||
|
#
|
||||||
|
<Link class="text-white" to="/communities">
|
||||||
|
#
|
||||||
|
</Link>
|
||||||
|
</T>
|
||||||
</h5>
|
</h5>
|
||||||
<ul class="list-inline">
|
<ul class="list-inline">
|
||||||
{this.state.trendingCommunities.map(community =>
|
{this.state.trendingCommunities.map(community => (
|
||||||
<li class="list-inline-item"><Link to={`/c/${community.name}`}>{community.name}</Link></li>
|
<li class="list-inline-item">
|
||||||
)}
|
<Link to={`/c/${community.name}`}>{community.name}</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sidebar() {
|
sidebar() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!this.state.showEditSite ?
|
{!this.state.showEditSite ? (
|
||||||
this.siteInfo() :
|
this.siteInfo()
|
||||||
|
) : (
|
||||||
<SiteForm
|
<SiteForm
|
||||||
site={this.state.site.site}
|
site={this.state.site.site}
|
||||||
onCancel={this.handleEditCancel}
|
onCancel={this.handleEditCancel}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrl() {
|
updateUrl() {
|
||||||
let typeStr = ListingType[this.state.type_].toLowerCase();
|
let typeStr = ListingType[this.state.type_].toLowerCase();
|
||||||
let sortStr = SortType[this.state.sort].toLowerCase();
|
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||||
this.props.history.push(`/home/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`);
|
this.props.history.push(
|
||||||
|
`/home/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
siteInfo() {
|
siteInfo() {
|
||||||
|
@ -208,30 +264,66 @@ export class Main extends Component<any, MainState> {
|
||||||
<div class="card border-secondary mb-3">
|
<div class="card border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="mb-0">{`${this.state.site.site.name}`}</h5>
|
<h5 class="mb-0">{`${this.state.site.site.name}`}</h5>
|
||||||
{this.canAdmin &&
|
{this.canAdmin && (
|
||||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleEditClick)}
|
||||||
|
>
|
||||||
<T i18nKey="edit">#</T>
|
<T i18nKey="edit">#</T>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
}
|
)}
|
||||||
<ul class="my-2 list-inline">
|
<ul class="my-2 list-inline">
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-secondary">
|
||||||
<T i18nKey="number_online" interpolation={{count: this.state.site.online}}>#</T>
|
<T
|
||||||
|
i18nKey="number_online"
|
||||||
|
interpolation={{ count: this.state.site.online }}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-secondary">
|
||||||
<T i18nKey="number_of_users" interpolation={{count: this.state.site.site.number_of_users}}>#</T>
|
<T
|
||||||
|
i18nKey="number_of_users"
|
||||||
|
interpolation={{
|
||||||
|
count: this.state.site.site.number_of_users,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-secondary">
|
||||||
<T i18nKey="number_of_communities" interpolation={{count: this.state.site.site.number_of_communities}}>#</T>
|
<T
|
||||||
|
i18nKey="number_of_communities"
|
||||||
|
interpolation={{
|
||||||
|
count: this.state.site.site.number_of_communities,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-secondary">
|
||||||
<T i18nKey="number_of_posts" interpolation={{count: this.state.site.site.number_of_posts}}>#</T>
|
<T
|
||||||
|
i18nKey="number_of_posts"
|
||||||
|
interpolation={{
|
||||||
|
count: this.state.site.site.number_of_posts,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item badge badge-secondary">
|
<li className="list-inline-item badge badge-secondary">
|
||||||
<T i18nKey="number_of_comments" interpolation={{count: this.state.site.site.number_of_comments}}>#</T>
|
<T
|
||||||
|
i18nKey="number_of_comments"
|
||||||
|
interpolation={{
|
||||||
|
count: this.state.site.site.number_of_comments,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<Link className="badge badge-secondary" to="/modlog">
|
<Link className="badge badge-secondary" to="/modlog">
|
||||||
|
@ -239,25 +331,37 @@ export class Main extends Component<any, MainState> {
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="mt-1 list-inline small mb-0">
|
<ul class="mt-1 list-inline small mb-0">
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
<T i18nKey="admins" class="d-inline">#</T>:
|
<T i18nKey="admins" class="d-inline">
|
||||||
|
#
|
||||||
|
</T>
|
||||||
|
:
|
||||||
|
</li>
|
||||||
|
{this.state.site.admins.map(admin => (
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<Link class="text-info" to={`/u/${admin.name}`}>
|
||||||
|
{admin.name}
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{this.state.site.admins.map(admin =>
|
))}
|
||||||
<li class="list-inline-item"><Link class="text-info" to={`/u/${admin.name}`}>{admin.name}</Link></li>
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{this.state.site.site.description && (
|
||||||
|
<div class="card border-secondary mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<div
|
||||||
|
className="md-div"
|
||||||
|
dangerouslySetInnerHTML={mdToHtml(
|
||||||
|
this.state.site.site.description
|
||||||
)}
|
)}
|
||||||
</ul>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.state.site.site.description &&
|
)}
|
||||||
<div class="card border-secondary mb-3">
|
</div>
|
||||||
<div class="card-body">
|
);
|
||||||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.site.site.description)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
landing() {
|
landing() {
|
||||||
|
@ -265,87 +369,103 @@ export class Main extends Component<any, MainState> {
|
||||||
<div class="card border-secondary">
|
<div class="card border-secondary">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5>
|
<h5>
|
||||||
<T i18nKey="powered_by" class="d-inline">#</T>
|
<T i18nKey="powered_by" class="d-inline">
|
||||||
<svg class="icon mx-2"><use xlinkHref="#icon-mouse">#</use></svg>
|
#
|
||||||
<a href={repoUrl}>Lemmy<sup>beta</sup></a>
|
</T>
|
||||||
|
<svg class="icon mx-2">
|
||||||
|
<use xlinkHref="#icon-mouse">#</use>
|
||||||
|
</svg>
|
||||||
|
<a href={repoUrl}>
|
||||||
|
Lemmy<sup>beta</sup>
|
||||||
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
<T i18nKey="landing_0">#<a href="https://en.wikipedia.org/wiki/Social_network_aggregation">#</a><a href="https://en.wikipedia.org/wiki/Fediverse">#</a><br></br><code>#</code><br></br><b>#</b><br></br><a href={repoUrl}>#</a><br></br><a href="https://www.rust-lang.org">#</a><a href="https://actix.rs/">#</a><a href="https://infernojs.org">#</a><a href="https://www.typescriptlang.org/">#</a>
|
<T i18nKey="landing_0">
|
||||||
</T>
|
#
|
||||||
</p>
|
<a href="https://en.wikipedia.org/wiki/Social_network_aggregation">
|
||||||
|
#
|
||||||
|
</a>
|
||||||
|
<a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
|
||||||
|
<br></br>
|
||||||
|
<code>#</code>
|
||||||
|
<br></br>
|
||||||
|
<b>#</b>
|
||||||
|
<br></br>
|
||||||
|
<a href={repoUrl}>#</a>
|
||||||
|
<br></br>
|
||||||
|
<a href="https://www.rust-lang.org">#</a>
|
||||||
|
<a href="https://actix.rs/">#</a>
|
||||||
|
<a href="https://infernojs.org">#</a>
|
||||||
|
<a href="https://www.typescriptlang.org/">#</a>
|
||||||
|
</T>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
posts() {
|
posts() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.loading ?
|
{this.state.loading ? (
|
||||||
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
<h5>
|
||||||
<div>
|
<svg class="icon icon-spinner spin">
|
||||||
{this.selects()}
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
<PostListings posts={this.state.posts} showCommunity />
|
</svg>
|
||||||
{this.paginator()}
|
</h5>
|
||||||
</div>
|
) : (
|
||||||
}
|
<div>
|
||||||
|
{this.selects()}
|
||||||
|
<PostListings posts={this.state.posts} showCommunity />
|
||||||
|
{this.paginator()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
selects() {
|
selects() {
|
||||||
return (
|
return (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div class="btn-group btn-group-toggle">
|
<ListingTypeSelect
|
||||||
<label className={`btn btn-sm btn-secondary
|
type_={this.state.type_}
|
||||||
${this.state.type_ == ListingType.Subscribed && 'active'}
|
onChange={this.handleTypeChange}
|
||||||
${UserService.Instance.user == undefined ? 'disabled' : 'pointer'}
|
/>
|
||||||
`}>
|
<span class="ml-2">
|
||||||
<input type="radio"
|
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
|
||||||
value={ListingType.Subscribed}
|
</span>
|
||||||
checked={this.state.type_ == ListingType.Subscribed}
|
|
||||||
onChange={linkEvent(this, this.handleTypeChange)}
|
|
||||||
disabled={UserService.Instance.user == undefined}
|
|
||||||
/>
|
|
||||||
{i18n.t('subscribed')}
|
|
||||||
</label>
|
|
||||||
<label className={`pointer btn btn-sm btn-secondary ${this.state.type_ == ListingType.All && 'active'}`}>
|
|
||||||
<input type="radio"
|
|
||||||
value={ListingType.All}
|
|
||||||
checked={this.state.type_ == ListingType.All}
|
|
||||||
onChange={linkEvent(this, this.handleTypeChange)}
|
|
||||||
/>
|
|
||||||
{i18n.t('all')}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="ml-2 custom-select custom-select-sm w-auto">
|
|
||||||
<option disabled><T i18nKey="sort_type">#</T></option>
|
|
||||||
<option value={SortType.Hot}><T i18nKey="hot">#</T></option>
|
|
||||||
<option value={SortType.New}><T i18nKey="new">#</T></option>
|
|
||||||
<option disabled>─────</option>
|
|
||||||
<option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
|
|
||||||
<option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
|
|
||||||
<option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
|
|
||||||
<option value={SortType.TopYear}><T i18nKey="year">#</T></option>
|
|
||||||
<option value={SortType.TopAll}><T i18nKey="all">#</T></option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
paginator() {
|
paginator() {
|
||||||
return (
|
return (
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
{this.state.page > 1 &&
|
{this.state.page > 1 && (
|
||||||
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
|
<button
|
||||||
}
|
class="btn btn-sm btn-secondary mr-1"
|
||||||
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
|
>
|
||||||
|
<T i18nKey="prev">#</T>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
|
>
|
||||||
|
<T i18nKey="next">#</T>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get canAdmin(): boolean {
|
get canAdmin(): boolean {
|
||||||
return UserService.Instance.user && this.state.site.admins.map(a => a.id).includes(UserService.Instance.user.id);
|
return (
|
||||||
|
UserService.Instance.user &&
|
||||||
|
this.state.site.admins
|
||||||
|
.map(a => a.id)
|
||||||
|
.includes(UserService.Instance.user.id)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditClick(i: Main) {
|
handleEditClick(i: Main) {
|
||||||
|
@ -358,42 +478,42 @@ export class Main extends Component<any, MainState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPage(i: Main) {
|
nextPage(i: Main) {
|
||||||
i.state.page++;
|
i.state.page++;
|
||||||
i.state.loading = true;
|
i.state.loading = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchPosts();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPage(i: Main) {
|
prevPage(i: Main) {
|
||||||
i.state.page--;
|
i.state.page--;
|
||||||
i.state.loading = true;
|
i.state.loading = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchPosts();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortChange(i: Main, event: any) {
|
handleSortChange(val: SortType) {
|
||||||
i.state.sort = Number(event.target.value);
|
this.state.sort = val;
|
||||||
i.state.page = 1;
|
this.state.page = 1;
|
||||||
i.state.loading = true;
|
this.state.loading = true;
|
||||||
i.setState(i.state);
|
this.setState(this.state);
|
||||||
i.updateUrl();
|
this.updateUrl();
|
||||||
i.fetchPosts();
|
this.fetchPosts();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTypeChange(i: Main, event: any) {
|
handleTypeChange(val: ListingType) {
|
||||||
i.state.type_ = Number(event.target.value);
|
this.state.type_ = val;
|
||||||
i.state.page = 1;
|
this.state.page = 1;
|
||||||
i.state.loading = true;
|
this.state.loading = true;
|
||||||
i.setState(i.state);
|
this.setState(this.state);
|
||||||
i.updateUrl();
|
this.updateUrl();
|
||||||
i.fetchPosts();
|
this.fetchPosts();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
keepFetchingPosts() {
|
keepFetchingPosts() {
|
||||||
|
@ -406,8 +526,8 @@ export class Main extends Component<any, MainState> {
|
||||||
page: this.state.page,
|
page: this.state.page,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
sort: SortType[this.state.sort],
|
sort: SortType[this.state.sort],
|
||||||
type_: ListingType[this.state.type_]
|
type_: ListingType[this.state.type_],
|
||||||
}
|
};
|
||||||
WebSocketService.Instance.getPosts(getPostsForm);
|
WebSocketService.Instance.getPosts(getPostsForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -430,7 +550,7 @@ export class Main extends Component<any, MainState> {
|
||||||
|
|
||||||
// This means it hasn't been set up yet
|
// This means it hasn't been set up yet
|
||||||
if (!res.site) {
|
if (!res.site) {
|
||||||
this.context.router.history.push("/setup");
|
this.context.router.history.push('/setup');
|
||||||
}
|
}
|
||||||
this.state.site.admins = res.admins;
|
this.state.site.admins = res.admins;
|
||||||
this.state.site.site = res.site;
|
this.state.site.site = res.site;
|
||||||
|
@ -438,7 +558,6 @@ export class Main extends Component<any, MainState> {
|
||||||
this.state.site.online = res.online;
|
this.state.site.online = res.online;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `${WebSocketService.Instance.site.name}`;
|
document.title = `${WebSocketService.Instance.site.name}`;
|
||||||
|
|
||||||
} else if (op == UserOperation.EditSite) {
|
} else if (op == UserOperation.EditSite) {
|
||||||
let res: SiteResponse = msg;
|
let res: SiteResponse = msg;
|
||||||
this.state.site.site = res.site;
|
this.state.site.site = res.site;
|
||||||
|
@ -460,4 +579,3 @@ export class Main extends Component<any, MainState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,21 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { UserOperation, GetModlogForm, GetModlogResponse, ModRemovePost, ModLockPost, ModStickyPost, ModRemoveComment, ModRemoveCommunity, ModBanFromCommunity, ModBan, ModAddCommunity, ModAdd } from '../interfaces';
|
import {
|
||||||
|
UserOperation,
|
||||||
|
GetModlogForm,
|
||||||
|
GetModlogResponse,
|
||||||
|
ModRemovePost,
|
||||||
|
ModLockPost,
|
||||||
|
ModStickyPost,
|
||||||
|
ModRemoveComment,
|
||||||
|
ModRemoveCommunity,
|
||||||
|
ModBanFromCommunity,
|
||||||
|
ModBan,
|
||||||
|
ModAddCommunity,
|
||||||
|
ModAdd,
|
||||||
|
} from '../interfaces';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { msgOp, addTypeInfo, fetchLimit } from '../utils';
|
import { msgOp, addTypeInfo, fetchLimit } from '../utils';
|
||||||
import { MomentTime } from './moment-time';
|
import { MomentTime } from './moment-time';
|
||||||
|
@ -10,9 +23,18 @@ import * as moment from 'moment';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
interface ModlogState {
|
interface ModlogState {
|
||||||
combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModStickyPost | ModRemoveCommunity | ModAdd | ModBan}>,
|
combined: Array<{
|
||||||
communityId?: number,
|
type_: string;
|
||||||
communityName?: string,
|
data:
|
||||||
|
| ModRemovePost
|
||||||
|
| ModLockPost
|
||||||
|
| ModStickyPost
|
||||||
|
| ModRemoveCommunity
|
||||||
|
| ModAdd
|
||||||
|
| ModBan;
|
||||||
|
}>;
|
||||||
|
communityId?: number;
|
||||||
|
communityName?: string;
|
||||||
page: number;
|
page: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
@ -23,20 +45,29 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
combined: [],
|
combined: [],
|
||||||
page: 1,
|
page: 1,
|
||||||
loading: true,
|
loading: true,
|
||||||
}
|
};
|
||||||
|
|
||||||
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.state.communityId = this.props.match.params.community_id ? Number(this.props.match.params.community_id) : undefined;
|
this.state.communityId = this.props.match.params.community_id
|
||||||
|
? Number(this.props.match.params.community_id)
|
||||||
|
: undefined;
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(
|
||||||
.subscribe(
|
retryWhen(errors =>
|
||||||
(msg) => this.parseMessage(msg),
|
errors.pipe(
|
||||||
(err) => console.error(err),
|
delay(3000),
|
||||||
|
take(10)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
msg => this.parseMessage(msg),
|
||||||
|
err => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
this.refetch();
|
this.refetch();
|
||||||
}
|
}
|
||||||
|
@ -50,15 +81,27 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
setCombined(res: GetModlogResponse) {
|
setCombined(res: GetModlogResponse) {
|
||||||
let removed_posts = addTypeInfo(res.removed_posts, "removed_posts");
|
let removed_posts = addTypeInfo(res.removed_posts, 'removed_posts');
|
||||||
let locked_posts = addTypeInfo(res.locked_posts, "locked_posts");
|
let locked_posts = addTypeInfo(res.locked_posts, 'locked_posts');
|
||||||
let stickied_posts = addTypeInfo(res.stickied_posts, "stickied_posts");
|
let stickied_posts = addTypeInfo(res.stickied_posts, 'stickied_posts');
|
||||||
let removed_comments = addTypeInfo(res.removed_comments, "removed_comments");
|
let removed_comments = addTypeInfo(
|
||||||
let removed_communities = addTypeInfo(res.removed_communities, "removed_communities");
|
res.removed_comments,
|
||||||
let banned_from_community = addTypeInfo(res.banned_from_community, "banned_from_community");
|
'removed_comments'
|
||||||
let added_to_community = addTypeInfo(res.added_to_community, "added_to_community");
|
);
|
||||||
let added = addTypeInfo(res.added, "added");
|
let removed_communities = addTypeInfo(
|
||||||
let banned = addTypeInfo(res.banned, "banned");
|
res.removed_communities,
|
||||||
|
'removed_communities'
|
||||||
|
);
|
||||||
|
let banned_from_community = addTypeInfo(
|
||||||
|
res.banned_from_community,
|
||||||
|
'banned_from_community'
|
||||||
|
);
|
||||||
|
let added_to_community = addTypeInfo(
|
||||||
|
res.added_to_community,
|
||||||
|
'added_to_community'
|
||||||
|
);
|
||||||
|
let added = addTypeInfo(res.added, 'added');
|
||||||
|
let banned = addTypeInfo(res.banned, 'banned');
|
||||||
this.state.combined = [];
|
this.state.combined = [];
|
||||||
|
|
||||||
this.state.combined.push(...removed_posts);
|
this.state.combined.push(...removed_posts);
|
||||||
|
@ -72,11 +115,14 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
this.state.combined.push(...banned);
|
this.state.combined.push(...banned);
|
||||||
|
|
||||||
if (this.state.communityId && this.state.combined.length > 0) {
|
if (this.state.communityId && this.state.combined.length > 0) {
|
||||||
this.state.communityName = (this.state.combined[0].data as ModRemovePost).community_name;
|
this.state.communityName = (this.state.combined[0]
|
||||||
|
.data as ModRemovePost).community_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort them by time
|
// Sort them by time
|
||||||
this.state.combined.sort((a, b) => b.data.when_.localeCompare(a.data.when_));
|
this.state.combined.sort((a, b) =>
|
||||||
|
b.data.when_.localeCompare(a.data.when_)
|
||||||
|
);
|
||||||
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
|
@ -84,114 +130,259 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
combined() {
|
combined() {
|
||||||
return (
|
return (
|
||||||
<tbody>
|
<tbody>
|
||||||
{this.state.combined.map(i =>
|
{this.state.combined.map(i => (
|
||||||
<tr>
|
<tr>
|
||||||
<td><MomentTime data={i.data} /></td>
|
|
||||||
<td><Link to={`/u/${i.data.mod_user_name}`}>{i.data.mod_user_name}</Link></td>
|
|
||||||
<td>
|
<td>
|
||||||
{i.type_ == 'removed_posts' &&
|
<MomentTime data={i.data} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Link to={`/u/${i.data.mod_user_name}`}>
|
||||||
|
{i.data.mod_user_name}
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{i.type_ == 'removed_posts' && (
|
||||||
<>
|
<>
|
||||||
{(i.data as ModRemovePost).removed? 'Removed' : 'Restored'}
|
{(i.data as ModRemovePost).removed ? 'Removed' : 'Restored'}
|
||||||
<span> Post <Link to={`/post/${(i.data as ModRemovePost).post_id}`}>{(i.data as ModRemovePost).post_name}</Link></span>
|
<span>
|
||||||
<div>{(i.data as ModRemovePost).reason && ` reason: ${(i.data as ModRemovePost).reason}`}</div>
|
{' '}
|
||||||
|
Post{' '}
|
||||||
|
<Link to={`/post/${(i.data as ModRemovePost).post_id}`}>
|
||||||
|
{(i.data as ModRemovePost).post_name}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
{(i.data as ModRemovePost).reason &&
|
||||||
|
` reason: ${(i.data as ModRemovePost).reason}`}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{i.type_ == 'locked_posts' &&
|
{i.type_ == 'locked_posts' && (
|
||||||
<>
|
<>
|
||||||
{(i.data as ModLockPost).locked? 'Locked' : 'Unlocked'}
|
{(i.data as ModLockPost).locked ? 'Locked' : 'Unlocked'}
|
||||||
<span> Post <Link to={`/post/${(i.data as ModLockPost).post_id}`}>{(i.data as ModLockPost).post_name}</Link></span>
|
<span>
|
||||||
|
{' '}
|
||||||
|
Post{' '}
|
||||||
|
<Link to={`/post/${(i.data as ModLockPost).post_id}`}>
|
||||||
|
{(i.data as ModLockPost).post_name}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{i.type_ == 'stickied_posts' &&
|
{i.type_ == 'stickied_posts' && (
|
||||||
<>
|
<>
|
||||||
{(i.data as ModStickyPost).stickied? 'Stickied' : 'Unstickied'}
|
{(i.data as ModStickyPost).stickied
|
||||||
<span> Post <Link to={`/post/${(i.data as ModStickyPost).post_id}`}>{(i.data as ModStickyPost).post_name}</Link></span>
|
? 'Stickied'
|
||||||
|
: 'Unstickied'}
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
Post{' '}
|
||||||
|
<Link to={`/post/${(i.data as ModStickyPost).post_id}`}>
|
||||||
|
{(i.data as ModStickyPost).post_name}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{i.type_ == 'removed_comments' &&
|
{i.type_ == 'removed_comments' && (
|
||||||
<>
|
<>
|
||||||
{(i.data as ModRemoveComment).removed? 'Removed' : 'Restored'}
|
{(i.data as ModRemoveComment).removed
|
||||||
<span> Comment <Link to={`/post/${(i.data as ModRemoveComment).post_id}/comment/${(i.data as ModRemoveComment).comment_id}`}>{(i.data as ModRemoveComment).comment_content}</Link></span>
|
? 'Removed'
|
||||||
<span> by <Link to={`/u/${(i.data as ModRemoveComment).comment_user_name}`}>{(i.data as ModRemoveComment).comment_user_name}</Link></span>
|
: 'Restored'}
|
||||||
<div>{(i.data as ModRemoveComment).reason && ` reason: ${(i.data as ModRemoveComment).reason}`}</div>
|
<span>
|
||||||
|
{' '}
|
||||||
|
Comment{' '}
|
||||||
|
<Link
|
||||||
|
to={`/post/${
|
||||||
|
(i.data as ModRemoveComment).post_id
|
||||||
|
}/comment/${(i.data as ModRemoveComment).comment_id}`}
|
||||||
|
>
|
||||||
|
{(i.data as ModRemoveComment).comment_content}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
by{' '}
|
||||||
|
<Link
|
||||||
|
to={`/u/${
|
||||||
|
(i.data as ModRemoveComment).comment_user_name
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{(i.data as ModRemoveComment).comment_user_name}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
{(i.data as ModRemoveComment).reason &&
|
||||||
|
` reason: ${(i.data as ModRemoveComment).reason}`}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{i.type_ == 'removed_communities' &&
|
{i.type_ == 'removed_communities' && (
|
||||||
<>
|
<>
|
||||||
{(i.data as ModRemoveCommunity).removed ? 'Removed' : 'Restored'}
|
{(i.data as ModRemoveCommunity).removed
|
||||||
<span> Community <Link to={`/c/${(i.data as ModRemoveCommunity).community_name}`}>{(i.data as ModRemoveCommunity).community_name}</Link></span>
|
? 'Removed'
|
||||||
<div>{(i.data as ModRemoveCommunity).reason && ` reason: ${(i.data as ModRemoveCommunity).reason}`}</div>
|
: 'Restored'}
|
||||||
<div>{(i.data as ModRemoveCommunity).expires && ` expires: ${moment.utc((i.data as ModRemoveCommunity).expires).fromNow()}`}</div>
|
<span>
|
||||||
|
{' '}
|
||||||
|
Community{' '}
|
||||||
|
<Link
|
||||||
|
to={`/c/${(i.data as ModRemoveCommunity).community_name}`}
|
||||||
|
>
|
||||||
|
{(i.data as ModRemoveCommunity).community_name}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
{(i.data as ModRemoveCommunity).reason &&
|
||||||
|
` reason: ${(i.data as ModRemoveCommunity).reason}`}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{(i.data as ModRemoveCommunity).expires &&
|
||||||
|
` expires: ${moment
|
||||||
|
.utc((i.data as ModRemoveCommunity).expires)
|
||||||
|
.fromNow()}`}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{i.type_ == 'banned_from_community' &&
|
{i.type_ == 'banned_from_community' && (
|
||||||
<>
|
<>
|
||||||
<span>{(i.data as ModBanFromCommunity).banned ? 'Banned ' : 'Unbanned '} </span>
|
<span>
|
||||||
<span><Link to={`/u/${(i.data as ModBanFromCommunity).other_user_name}`}>{(i.data as ModBanFromCommunity).other_user_name}</Link></span>
|
{(i.data as ModBanFromCommunity).banned
|
||||||
|
? 'Banned '
|
||||||
|
: 'Unbanned '}{' '}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<Link
|
||||||
|
to={`/u/${
|
||||||
|
(i.data as ModBanFromCommunity).other_user_name
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{(i.data as ModBanFromCommunity).other_user_name}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
<span> from the community </span>
|
<span> from the community </span>
|
||||||
<span><Link to={`/c/${(i.data as ModBanFromCommunity).community_name}`}>{(i.data as ModBanFromCommunity).community_name}</Link></span>
|
<span>
|
||||||
<div>{(i.data as ModBanFromCommunity).reason && ` reason: ${(i.data as ModBanFromCommunity).reason}`}</div>
|
<Link
|
||||||
<div>{(i.data as ModBanFromCommunity).expires && ` expires: ${moment.utc((i.data as ModBanFromCommunity).expires).fromNow()}`}</div>
|
to={`/c/${
|
||||||
|
(i.data as ModBanFromCommunity).community_name
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{(i.data as ModBanFromCommunity).community_name}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
{(i.data as ModBanFromCommunity).reason &&
|
||||||
|
` reason: ${(i.data as ModBanFromCommunity).reason}`}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{(i.data as ModBanFromCommunity).expires &&
|
||||||
|
` expires: ${moment
|
||||||
|
.utc((i.data as ModBanFromCommunity).expires)
|
||||||
|
.fromNow()}`}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{i.type_ == 'added_to_community' &&
|
{i.type_ == 'added_to_community' && (
|
||||||
<>
|
<>
|
||||||
<span>{(i.data as ModAddCommunity).removed ? 'Removed ' : 'Appointed '} </span>
|
<span>
|
||||||
<span><Link to={`/u/${(i.data as ModAddCommunity).other_user_name}`}>{(i.data as ModAddCommunity).other_user_name}</Link></span>
|
{(i.data as ModAddCommunity).removed
|
||||||
|
? 'Removed '
|
||||||
|
: 'Appointed '}{' '}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<Link
|
||||||
|
to={`/u/${(i.data as ModAddCommunity).other_user_name}`}
|
||||||
|
>
|
||||||
|
{(i.data as ModAddCommunity).other_user_name}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
<span> as a mod to the community </span>
|
<span> as a mod to the community </span>
|
||||||
<span><Link to={`/c/${(i.data as ModAddCommunity).community_name}`}>{(i.data as ModAddCommunity).community_name}</Link></span>
|
<span>
|
||||||
|
<Link
|
||||||
|
to={`/c/${(i.data as ModAddCommunity).community_name}`}
|
||||||
|
>
|
||||||
|
{(i.data as ModAddCommunity).community_name}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{i.type_ == 'banned' &&
|
{i.type_ == 'banned' && (
|
||||||
<>
|
<>
|
||||||
<span>{(i.data as ModBan).banned ? 'Banned ' : 'Unbanned '} </span>
|
<span>
|
||||||
<span><Link to={`/u/${(i.data as ModBan).other_user_name}`}>{(i.data as ModBan).other_user_name}</Link></span>
|
{(i.data as ModBan).banned ? 'Banned ' : 'Unbanned '}{' '}
|
||||||
<div>{(i.data as ModBan).reason && ` reason: ${(i.data as ModBan).reason}`}</div>
|
</span>
|
||||||
<div>{(i.data as ModBan).expires && ` expires: ${moment.utc((i.data as ModBan).expires).fromNow()}`}</div>
|
<span>
|
||||||
|
<Link to={`/u/${(i.data as ModBan).other_user_name}`}>
|
||||||
|
{(i.data as ModBan).other_user_name}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
{(i.data as ModBan).reason &&
|
||||||
|
` reason: ${(i.data as ModBan).reason}`}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{(i.data as ModBan).expires &&
|
||||||
|
` expires: ${moment
|
||||||
|
.utc((i.data as ModBan).expires)
|
||||||
|
.fromNow()}`}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{i.type_ == 'added' &&
|
{i.type_ == 'added' && (
|
||||||
<>
|
<>
|
||||||
<span>{(i.data as ModAdd).removed ? 'Removed ' : 'Appointed '} </span>
|
<span>
|
||||||
<span><Link to={`/u/${(i.data as ModAdd).other_user_name}`}>{(i.data as ModAdd).other_user_name}</Link></span>
|
{(i.data as ModAdd).removed ? 'Removed ' : 'Appointed '}{' '}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<Link to={`/u/${(i.data as ModAdd).other_user_name}`}>
|
||||||
|
{(i.data as ModAdd).other_user_name}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
<span> as an admin </span>
|
<span> as an admin </span>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
))}
|
||||||
}
|
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{this.state.loading ?
|
{this.state.loading ? (
|
||||||
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
<h5 class="">
|
||||||
<div>
|
<svg class="icon icon-spinner spin">
|
||||||
<h5>
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
{this.state.communityName && <Link className="text-white" to={`/c/${this.state.communityName}`}>/c/{this.state.communityName} </Link>}
|
</svg>
|
||||||
<span>Modlog</span>
|
|
||||||
</h5>
|
</h5>
|
||||||
<div class="table-responsive">
|
) : (
|
||||||
<table id="modlog_table" class="table table-sm table-hover">
|
<div>
|
||||||
<thead class="pointer">
|
<h5>
|
||||||
<tr>
|
{this.state.communityName && (
|
||||||
<th>Time</th>
|
<Link
|
||||||
<th>Mod</th>
|
className="text-white"
|
||||||
<th>Action</th>
|
to={`/c/${this.state.communityName}`}
|
||||||
</tr>
|
>
|
||||||
</thead>
|
/c/{this.state.communityName}{' '}
|
||||||
{this.combined()}
|
</Link>
|
||||||
</table>
|
)}
|
||||||
{this.paginator()}
|
<span>Modlog</span>
|
||||||
|
</h5>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="modlog_table" class="table table-sm table-hover">
|
||||||
|
<thead class="pointer">
|
||||||
|
<tr>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Mod</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{this.combined()}
|
||||||
|
</table>
|
||||||
|
{this.paginator()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -199,27 +390,37 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
paginator() {
|
paginator() {
|
||||||
return (
|
return (
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{this.state.page > 1 &&
|
{this.state.page > 1 && (
|
||||||
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
|
<button
|
||||||
}
|
class="btn btn-sm btn-secondary mr-1"
|
||||||
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
|
>
|
||||||
|
Prev
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPage(i: Modlog) {
|
nextPage(i: Modlog) {
|
||||||
i.state.page++;
|
i.state.page++;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.refetch();
|
i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPage(i: Modlog) {
|
prevPage(i: Modlog) {
|
||||||
i.state.page--;
|
i.state.page--;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.refetch();
|
i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
refetch(){
|
refetch() {
|
||||||
let modlogForm: GetModlogForm = {
|
let modlogForm: GetModlogForm = {
|
||||||
community_id: this.state.communityId,
|
community_id: this.state.communityId,
|
||||||
page: this.state.page,
|
page: this.state.page,
|
||||||
|
@ -237,8 +438,8 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
} else if (op == UserOperation.GetModlog) {
|
} else if (op == UserOperation.GetModlog) {
|
||||||
let res: GetModlogResponse = msg;
|
let res: GetModlogResponse = msg;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
this.setCombined(res);
|
this.setCombined(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,10 @@ interface MomentTimeProps {
|
||||||
published?: string;
|
published?: string;
|
||||||
when_?: string;
|
when_?: string;
|
||||||
updated?: string;
|
updated?: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MomentTime extends Component<MomentTimeProps, any> {
|
export class MomentTime extends Component<MomentTimeProps, any> {
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
@ -24,13 +23,13 @@ export class MomentTime extends Component<MomentTimeProps, any> {
|
||||||
render() {
|
render() {
|
||||||
if (this.props.data.updated) {
|
if (this.props.data.updated) {
|
||||||
return (
|
return (
|
||||||
<span title={this.props.data.updated} className="font-italics">{i18n.t('modified')} {moment.utc(this.props.data.updated).fromNow()}</span>
|
<span title={this.props.data.updated} className="font-italics">
|
||||||
)
|
{i18n.t('modified')} {moment.utc(this.props.data.updated).fromNow()}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
let str = this.props.data.published || this.props.data.when_;
|
let str = this.props.data.published || this.props.data.when_;
|
||||||
return (
|
return <span title={str}>{moment.utc(str).fromNow()}</span>;
|
||||||
<span title={str}>{moment.utc(str).fromNow()}</span>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { UserOperation, GetRepliesForm, GetRepliesResponse, SortType, GetSiteResponse, Comment} from '../interfaces';
|
import {
|
||||||
|
UserOperation,
|
||||||
|
GetRepliesForm,
|
||||||
|
GetRepliesResponse,
|
||||||
|
GetUserMentionsForm,
|
||||||
|
GetUserMentionsResponse,
|
||||||
|
SortType,
|
||||||
|
GetSiteResponse,
|
||||||
|
Comment,
|
||||||
|
} from '../interfaces';
|
||||||
import { msgOp } from '../utils';
|
import { msgOp } from '../utils';
|
||||||
import { version } from '../version';
|
import { version } from '../version';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
@ -13,8 +22,9 @@ interface NavbarState {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
expandUserDropdown: boolean;
|
expandUserDropdown: boolean;
|
||||||
replies: Array<Comment>,
|
replies: Array<Comment>;
|
||||||
fetchCount: number,
|
mentions: Array<Comment>;
|
||||||
|
fetchCount: number;
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
siteName: string;
|
siteName: string;
|
||||||
}
|
}
|
||||||
|
@ -23,21 +33,22 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
private wsSub: Subscription;
|
private wsSub: Subscription;
|
||||||
private userSub: Subscription;
|
private userSub: Subscription;
|
||||||
emptyState: NavbarState = {
|
emptyState: NavbarState = {
|
||||||
isLoggedIn: (UserService.Instance.user !== undefined),
|
isLoggedIn: UserService.Instance.user !== undefined,
|
||||||
unreadCount: 0,
|
unreadCount: 0,
|
||||||
fetchCount: 0,
|
fetchCount: 0,
|
||||||
replies: [],
|
replies: [],
|
||||||
|
mentions: [],
|
||||||
expanded: false,
|
expanded: false,
|
||||||
expandUserDropdown: false,
|
expandUserDropdown: false,
|
||||||
siteName: undefined
|
siteName: undefined,
|
||||||
}
|
};
|
||||||
|
|
||||||
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.handleOverviewClick = this.handleOverviewClick.bind(this);
|
this.handleOverviewClick = this.handleOverviewClick.bind(this);
|
||||||
|
|
||||||
this.keepFetchingReplies();
|
this.keepFetchingUnreads();
|
||||||
|
|
||||||
// Subscribe to user changes
|
// Subscribe to user changes
|
||||||
this.userSub = UserService.Instance.sub.subscribe(user => {
|
this.userSub = UserService.Instance.sub.subscribe(user => {
|
||||||
|
@ -48,12 +59,19 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.wsSub = WebSocketService.Instance.subject
|
this.wsSub = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(
|
||||||
.subscribe(
|
retryWhen(errors =>
|
||||||
(msg) => this.parseMessage(msg),
|
errors.pipe(
|
||||||
(err) => console.error(err),
|
delay(3000),
|
||||||
|
take(10)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
msg => this.parseMessage(msg),
|
||||||
|
err => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.state.isLoggedIn) {
|
if (this.state.isLoggedIn) {
|
||||||
this.requestNotificationPermission();
|
this.requestNotificationPermission();
|
||||||
|
@ -63,9 +81,7 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return <div>{this.navbar()}</div>;
|
||||||
<div>{this.navbar()}</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -80,48 +96,98 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
<Link title={version} class="navbar-brand" to="/">
|
<Link title={version} class="navbar-brand" to="/">
|
||||||
{this.state.siteName}
|
{this.state.siteName}
|
||||||
</Link>
|
</Link>
|
||||||
<button class="navbar-toggler" type="button" onClick={linkEvent(this, this.expandNavbar)}>
|
<button
|
||||||
|
class="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
onClick={linkEvent(this, this.expandNavbar)}
|
||||||
|
>
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}>
|
<div
|
||||||
|
className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
|
||||||
|
>
|
||||||
<ul class="navbar-nav mr-auto">
|
<ul class="navbar-nav mr-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<Link class="nav-link" to="/communities"><T i18nKey="communities">#</T></Link>
|
<Link class="nav-link" to="/communities">
|
||||||
|
<T i18nKey="communities">#</T>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<Link class="nav-link" to="/search"><T i18nKey="search">#</T></Link>
|
<Link class="nav-link" to="/search">
|
||||||
|
<T i18nKey="search">#</T>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<Link class="nav-link" to={{pathname: '/create_post', state: { prevPath: this.currentLocation }}}><T i18nKey="create_post">#</T></Link>
|
<Link
|
||||||
|
class="nav-link"
|
||||||
|
to={{
|
||||||
|
pathname: '/create_post',
|
||||||
|
state: { prevPath: this.currentLocation },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<T i18nKey="create_post">#</T>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<Link class="nav-link" to="/create_community"><T i18nKey="create_community">#</T></Link>
|
<Link class="nav-link" to="/create_community">
|
||||||
|
<T i18nKey="create_community">#</T>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav ml-auto mr-2">
|
<ul class="navbar-nav ml-auto mr-2">
|
||||||
{this.state.isLoggedIn ?
|
{this.state.isLoggedIn ? (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<Link class="nav-link" to="/inbox">
|
<Link class="nav-link" to="/inbox">
|
||||||
<svg class="icon"><use xlinkHref="#icon-mail"></use></svg>
|
<svg class="icon">
|
||||||
{this.state.unreadCount> 0 && <span class="ml-1 badge badge-light">{this.state.unreadCount}</span>}
|
<use xlinkHref="#icon-mail"></use>
|
||||||
</Link>
|
</svg>
|
||||||
|
{this.state.unreadCount > 0 && (
|
||||||
|
<span class="ml-1 badge badge-light">
|
||||||
|
{this.state.unreadCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
<li
|
||||||
|
className={`nav-item dropdown ${this.state
|
||||||
|
.expandUserDropdown && 'show'}`}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="pointer nav-link dropdown-toggle"
|
||||||
|
onClick={linkEvent(this, this.expandUserDropdown)}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
{UserService.Instance.user.username}
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
className={`dropdown-menu dropdown-menu-right ${this.state
|
||||||
|
.expandUserDropdown && 'show'}`}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
class="dropdown-item pointer"
|
||||||
|
onClick={linkEvent(this, this.handleOverviewClick)}
|
||||||
|
>
|
||||||
|
<T i18nKey="overview">#</T>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
class="dropdown-item pointer"
|
||||||
|
onClick={linkEvent(this, this.handleLogoutClick)}
|
||||||
|
>
|
||||||
|
<T i18nKey="logout">#</T>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
}
|
</>
|
||||||
<li className={`nav-item dropdown ${this.state.expandUserDropdown && 'show'}`}>
|
) : (
|
||||||
<a class="pointer nav-link dropdown-toggle" onClick={linkEvent(this, this.expandUserDropdown)} role="button">
|
<Link class="nav-link" to="/login">
|
||||||
{UserService.Instance.user.username}
|
<T i18nKey="login_sign_up">#</T>
|
||||||
</a>
|
</Link>
|
||||||
<div className={`dropdown-menu dropdown-menu-right ${this.state.expandUserDropdown && 'show'}`}>
|
)}
|
||||||
<a role="button" class="dropdown-item pointer" onClick={linkEvent(this, this.handleOverviewClick)}><T i18nKey="overview">#</T></a>
|
|
||||||
<a role="button" class="dropdown-item pointer" onClick={ linkEvent(this, this.handleLogoutClick) }><T i18nKey="logout">#</T></a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
:
|
|
||||||
<Link class="nav-link" to="/login"><T i18nKey="login_sign_up">#</T></Link>
|
|
||||||
}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -154,7 +220,7 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
let op: UserOperation = msgOp(msg);
|
let op: UserOperation = msgOp(msg);
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
if (msg.error == "not_logged_in") {
|
if (msg.error == 'not_logged_in') {
|
||||||
UserService.Instance.logout();
|
UserService.Instance.logout();
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
@ -162,13 +228,31 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
} else if (op == UserOperation.GetReplies) {
|
} else if (op == UserOperation.GetReplies) {
|
||||||
let res: GetRepliesResponse = msg;
|
let res: GetRepliesResponse = msg;
|
||||||
let unreadReplies = res.replies.filter(r => !r.read);
|
let unreadReplies = res.replies.filter(r => !r.read);
|
||||||
if (unreadReplies.length > 0 && this.state.fetchCount > 1 &&
|
if (
|
||||||
(JSON.stringify(this.state.replies) !== JSON.stringify(unreadReplies))) {
|
unreadReplies.length > 0 &&
|
||||||
|
this.state.fetchCount > 1 &&
|
||||||
|
JSON.stringify(this.state.replies) !== JSON.stringify(unreadReplies)
|
||||||
|
) {
|
||||||
this.notify(unreadReplies);
|
this.notify(unreadReplies);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.replies = unreadReplies;
|
this.state.replies = unreadReplies;
|
||||||
this.sendRepliesCount(res);
|
this.setState(this.state);
|
||||||
|
this.sendUnreadCount();
|
||||||
|
} else if (op == UserOperation.GetUserMentions) {
|
||||||
|
let res: GetUserMentionsResponse = msg;
|
||||||
|
let unreadMentions = res.mentions.filter(r => !r.read);
|
||||||
|
if (
|
||||||
|
unreadMentions.length > 0 &&
|
||||||
|
this.state.fetchCount > 1 &&
|
||||||
|
JSON.stringify(this.state.mentions) !== JSON.stringify(unreadMentions)
|
||||||
|
) {
|
||||||
|
this.notify(unreadMentions);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.mentions = unreadMentions;
|
||||||
|
this.setState(this.state);
|
||||||
|
this.sendUnreadCount();
|
||||||
} else if (op == UserOperation.GetSite) {
|
} else if (op == UserOperation.GetSite) {
|
||||||
let res: GetSiteResponse = msg;
|
let res: GetSiteResponse = msg;
|
||||||
|
|
||||||
|
@ -177,15 +261,15 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
WebSocketService.Instance.site = res.site;
|
WebSocketService.Instance.site = res.site;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keepFetchingReplies() {
|
keepFetchingUnreads() {
|
||||||
this.fetchReplies();
|
this.fetchUnreads();
|
||||||
setInterval(() => this.fetchReplies(), 15000);
|
setInterval(() => this.fetchUnreads(), 15000);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchReplies() {
|
fetchUnreads() {
|
||||||
if (this.state.isLoggedIn) {
|
if (this.state.isLoggedIn) {
|
||||||
let repliesForm: GetRepliesForm = {
|
let repliesForm: GetRepliesForm = {
|
||||||
sort: SortType[SortType.New],
|
sort: SortType[SortType.New],
|
||||||
|
@ -193,8 +277,16 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 9999,
|
limit: 9999,
|
||||||
};
|
};
|
||||||
if (this.currentLocation !=='/inbox') {
|
|
||||||
|
let userMentionsForm: GetUserMentionsForm = {
|
||||||
|
sort: SortType[SortType.New],
|
||||||
|
unread_only: true,
|
||||||
|
page: 1,
|
||||||
|
limit: 9999,
|
||||||
|
};
|
||||||
|
if (this.currentLocation !== '/inbox') {
|
||||||
WebSocketService.Instance.getReplies(repliesForm);
|
WebSocketService.Instance.getReplies(repliesForm);
|
||||||
|
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
||||||
this.state.fetchCount++;
|
this.state.fetchCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -204,38 +296,51 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
return this.context.router.history.location.pathname;
|
return this.context.router.history.location.pathname;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendRepliesCount(res: GetRepliesResponse) {
|
sendUnreadCount() {
|
||||||
UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: res.replies.filter(r => !r.read).length});
|
UserService.Instance.sub.next({
|
||||||
|
user: UserService.Instance.user,
|
||||||
|
unreadCount: this.unreadCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get unreadCount() {
|
||||||
|
return (
|
||||||
|
this.state.replies.filter(r => !r.read).length +
|
||||||
|
this.state.mentions.filter(r => !r.read).length
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
requestNotificationPermission() {
|
requestNotificationPermission() {
|
||||||
if (UserService.Instance.user) {
|
if (UserService.Instance.user) {
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (!Notification) {
|
if (!Notification) {
|
||||||
alert(i18n.t('notifications_error'));
|
alert(i18n.t('notifications_error'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Notification.permission !== 'granted')
|
if (Notification.permission !== 'granted')
|
||||||
Notification.requestPermission();
|
Notification.requestPermission();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(replies: Array<Comment>) {
|
notify(replies: Array<Comment>) {
|
||||||
let recentReply = replies[0];
|
let recentReply = replies[0];
|
||||||
if (Notification.permission !== 'granted')
|
if (Notification.permission !== 'granted') Notification.requestPermission();
|
||||||
Notification.requestPermission();
|
|
||||||
else {
|
else {
|
||||||
var notification = new Notification(`${replies.length} ${i18n.t('unread_messages')}`, {
|
var notification = new Notification(
|
||||||
icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
|
`${replies.length} ${i18n.t('unread_messages')}`,
|
||||||
body: `${recentReply.creator_name}: ${recentReply.content}`
|
{
|
||||||
});
|
icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
|
||||||
|
body: `${recentReply.creator_name}: ${recentReply.content}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
notification.onclick = () => {
|
notification.onclick = () => {
|
||||||
this.context.router.history.push(`/post/${recentReply.post_id}/comment/${recentReply.id}`);
|
this.context.router.history.push(
|
||||||
|
`/post/${recentReply.post_id}/comment/${recentReply.id}`
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,31 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { PostListings } from './post-listings';
|
import { PostListings } from './post-listings';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { PostForm as PostFormI, PostFormParams, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType, SearchForm, SearchType, SearchResponse } from '../interfaces';
|
import {
|
||||||
|
PostForm as PostFormI,
|
||||||
|
PostFormParams,
|
||||||
|
Post,
|
||||||
|
PostResponse,
|
||||||
|
UserOperation,
|
||||||
|
Community,
|
||||||
|
ListCommunitiesResponse,
|
||||||
|
ListCommunitiesForm,
|
||||||
|
SortType,
|
||||||
|
SearchForm,
|
||||||
|
SearchType,
|
||||||
|
SearchResponse,
|
||||||
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { msgOp, getPageTitle, debounce, validURL, capitalizeFirstLetter, markdownHelpUrl, mdToHtml } from '../utils';
|
import {
|
||||||
|
msgOp,
|
||||||
|
getPageTitle,
|
||||||
|
debounce,
|
||||||
|
validURL,
|
||||||
|
capitalizeFirstLetter,
|
||||||
|
markdownHelpUrl,
|
||||||
|
mdToHtml,
|
||||||
|
} from '../utils';
|
||||||
import * as autosize from 'autosize';
|
import * as autosize from 'autosize';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
@ -29,7 +50,6 @@ interface PostFormState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PostForm extends Component<PostFormProps, PostFormState> {
|
export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
|
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
private emptyState: PostFormState = {
|
private emptyState: PostFormState = {
|
||||||
postForm: {
|
postForm: {
|
||||||
|
@ -37,7 +57,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
auth: null,
|
auth: null,
|
||||||
community_id: null,
|
community_id: null,
|
||||||
creator_id: (UserService.Instance.user) ? UserService.Instance.user.id : null,
|
creator_id: UserService.Instance.user
|
||||||
|
? UserService.Instance.user.id
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
communities: [],
|
communities: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -46,7 +68,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
suggestedTitle: undefined,
|
suggestedTitle: undefined,
|
||||||
suggestedPosts: [],
|
suggestedPosts: [],
|
||||||
crossPosts: [],
|
crossPosts: [],
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -62,8 +84,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
creator_id: this.props.post.creator_id,
|
creator_id: this.props.post.creator_id,
|
||||||
url: this.props.post.url,
|
url: this.props.post.url,
|
||||||
nsfw: this.props.post.nsfw,
|
nsfw: this.props.post.nsfw,
|
||||||
auth: null
|
auth: null,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.params) {
|
if (this.props.params) {
|
||||||
|
@ -77,17 +99,24 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(
|
||||||
.subscribe(
|
retryWhen(errors =>
|
||||||
(msg) => this.parseMessage(msg),
|
errors.pipe(
|
||||||
(err) => console.error(err),
|
delay(3000),
|
||||||
|
take(10)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
msg => this.parseMessage(msg),
|
||||||
|
err => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
let listCommunitiesForm: ListCommunitiesForm = {
|
let listCommunitiesForm: ListCommunitiesForm = {
|
||||||
sort: SortType[SortType.TopAll],
|
sort: SortType[SortType.TopAll],
|
||||||
limit: 9999,
|
limit: 9999,
|
||||||
}
|
};
|
||||||
|
|
||||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||||
}
|
}
|
||||||
|
@ -105,79 +134,177 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={linkEvent(this, this.handlePostSubmit)}>
|
<form onSubmit={linkEvent(this, this.handlePostSubmit)}>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="url">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
|
<T i18nKey="url">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="url" class="form-control" value={this.state.postForm.url} onInput={linkEvent(this, this.handlePostUrlChange)} />
|
<input
|
||||||
{this.state.suggestedTitle &&
|
type="url"
|
||||||
<div class="mt-1 text-muted small font-weight-bold pointer" onClick={linkEvent(this, this.copySuggestedTitle)}><T i18nKey="copy_suggested_title" interpolation={{title: this.state.suggestedTitle}}>#</T></div>
|
class="form-control"
|
||||||
}
|
value={this.state.postForm.url}
|
||||||
|
onInput={linkEvent(this, this.handlePostUrlChange)}
|
||||||
|
/>
|
||||||
|
{this.state.suggestedTitle && (
|
||||||
|
<div
|
||||||
|
class="mt-1 text-muted small font-weight-bold pointer"
|
||||||
|
onClick={linkEvent(this, this.copySuggestedTitle)}
|
||||||
|
>
|
||||||
|
<T
|
||||||
|
i18nKey="copy_suggested_title"
|
||||||
|
interpolation={{ title: this.state.suggestedTitle }}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<form>
|
<form>
|
||||||
<label htmlFor="file-upload" className={`${UserService.Instance.user && 'pointer'} d-inline-block mr-2 float-right text-muted small font-weight-bold`}><T i18nKey="upload_image">#</T></label>
|
<label
|
||||||
<input id="file-upload" type="file" accept="image/*,video/*" name="file" class="d-none" disabled={!UserService.Instance.user} onChange={linkEvent(this, this.handleImageUpload)} />
|
htmlFor="file-upload"
|
||||||
|
className={`${UserService.Instance.user &&
|
||||||
|
'pointer'} d-inline-block mr-2 float-right text-muted small font-weight-bold`}
|
||||||
|
>
|
||||||
|
<T i18nKey="upload_image">#</T>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="file-upload"
|
||||||
|
type="file"
|
||||||
|
accept="image/*,video/*"
|
||||||
|
name="file"
|
||||||
|
class="d-none"
|
||||||
|
disabled={!UserService.Instance.user}
|
||||||
|
onChange={linkEvent(this, this.handleImageUpload)}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
{this.state.imageLoading &&
|
{this.state.imageLoading && (
|
||||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg>
|
<svg class="icon icon-spinner spin">
|
||||||
}
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
{this.state.crossPosts.length > 0 &&
|
</svg>
|
||||||
|
)}
|
||||||
|
{this.state.crossPosts.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div class="my-1 text-muted small font-weight-bold"><T i18nKey="cross_posts">#</T></div>
|
<div class="my-1 text-muted small font-weight-bold">
|
||||||
|
<T i18nKey="cross_posts">#</T>
|
||||||
|
</div>
|
||||||
<PostListings showCommunity posts={this.state.crossPosts} />
|
<PostListings showCommunity posts={this.state.crossPosts} />
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="title">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
|
<T i18nKey="title">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<textarea value={this.state.postForm.name} onInput={linkEvent(this, this.handlePostNameChange)} class="form-control" required rows={2} minLength={3} maxLength={100} />
|
<textarea
|
||||||
{this.state.suggestedPosts.length > 0 &&
|
value={this.state.postForm.name}
|
||||||
|
onInput={linkEvent(this, this.handlePostNameChange)}
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
rows={2}
|
||||||
|
minLength={3}
|
||||||
|
maxLength={100}
|
||||||
|
/>
|
||||||
|
{this.state.suggestedPosts.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div class="my-1 text-muted small font-weight-bold"><T i18nKey="related_posts">#</T></div>
|
<div class="my-1 text-muted small font-weight-bold">
|
||||||
|
<T i18nKey="related_posts">#</T>
|
||||||
|
</div>
|
||||||
<PostListings posts={this.state.suggestedPosts} />
|
<PostListings posts={this.state.suggestedPosts} />
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="body">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
|
<T i18nKey="body">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} className={`form-control ${this.state.previewMode && 'd-none'}`} rows={4} maxLength={10000} />
|
<textarea
|
||||||
{this.state.previewMode &&
|
value={this.state.postForm.body}
|
||||||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.postForm.body)} />
|
onInput={linkEvent(this, this.handlePostBodyChange)}
|
||||||
}
|
className={`form-control ${this.state.previewMode && 'd-none'}`}
|
||||||
{this.state.postForm.body &&
|
rows={4}
|
||||||
<button className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state.previewMode && 'active'}`} onClick={linkEvent(this, this.handlePreviewToggle)}><T i18nKey="preview">#</T></button>
|
maxLength={10000}
|
||||||
}
|
/>
|
||||||
<a href={markdownHelpUrl} target="_blank" class="d-inline-block float-right text-muted small font-weight-bold"><T i18nKey="formatting_help">#</T></a>
|
{this.state.previewMode && (
|
||||||
|
<div
|
||||||
|
className="md-div"
|
||||||
|
dangerouslySetInnerHTML={mdToHtml(this.state.postForm.body)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{this.state.postForm.body && (
|
||||||
|
<button
|
||||||
|
className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
|
||||||
|
.previewMode && 'active'}`}
|
||||||
|
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||||
|
>
|
||||||
|
<T i18nKey="preview">#</T>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href={markdownHelpUrl}
|
||||||
|
target="_blank"
|
||||||
|
class="d-inline-block float-right text-muted small font-weight-bold"
|
||||||
|
>
|
||||||
|
<T i18nKey="formatting_help">#</T>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!this.props.post &&
|
{!this.props.post && (
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="community">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
<div class="col-sm-10">
|
<T i18nKey="community">#</T>
|
||||||
<select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}>
|
</label>
|
||||||
{this.state.communities.map(community =>
|
<div class="col-sm-10">
|
||||||
<option value={community.id}>{community.name}</option>
|
<select
|
||||||
)}
|
class="form-control"
|
||||||
</select>
|
value={this.state.postForm.community_id}
|
||||||
|
onInput={linkEvent(this, this.handlePostCommunityChange)}
|
||||||
|
>
|
||||||
|
{this.state.communities.map(community => (
|
||||||
|
<option value={community.id}>{community.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
}
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" checked={this.state.postForm.nsfw} onChange={linkEvent(this, this.handlePostNsfwChange)}/>
|
<input
|
||||||
<label class="form-check-label"><T i18nKey="nsfw">#</T></label>
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={this.state.postForm.nsfw}
|
||||||
|
onChange={linkEvent(this, this.handlePostNsfwChange)}
|
||||||
|
/>
|
||||||
|
<label class="form-check-label">
|
||||||
|
<T i18nKey="nsfw">#</T>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<button type="submit" class="btn btn-secondary mr-2">
|
<button type="submit" class="btn btn-secondary mr-2">
|
||||||
{this.state.loading ?
|
{this.state.loading ? (
|
||||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
|
<svg class="icon icon-spinner spin">
|
||||||
this.props.post ? capitalizeFirstLetter(i18n.t('save')) : capitalizeFirstLetter(i18n.t('create'))}</button>
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
{this.props.post && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}><T i18nKey="cancel">#</T></button>}
|
</svg>
|
||||||
|
) : this.props.post ? (
|
||||||
|
capitalizeFirstLetter(i18n.t('save'))
|
||||||
|
) : (
|
||||||
|
capitalizeFirstLetter(i18n.t('create'))
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{this.props.post && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.handleCancel)}
|
||||||
|
>
|
||||||
|
<T i18nKey="cancel">#</T>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -205,7 +332,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
handlePostUrlChange(i: PostForm, event: any) {
|
handlePostUrlChange(i: PostForm, event: any) {
|
||||||
i.state.postForm.url = event.target.value;
|
i.state.postForm.url = event.target.value;
|
||||||
if (validURL(i.state.postForm.url)) {
|
if (validURL(i.state.postForm.url)) {
|
||||||
|
|
||||||
let form: SearchForm = {
|
let form: SearchForm = {
|
||||||
q: i.state.postForm.url,
|
q: i.state.postForm.url,
|
||||||
type_: SearchType[SearchType.Url],
|
type_: SearchType[SearchType.Url],
|
||||||
|
@ -288,21 +414,21 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(res => {
|
.then(res => {
|
||||||
let url = `${window.location.origin}/pictshare/${res.url}`;
|
let url = `${window.location.origin}/pictshare/${res.url}`;
|
||||||
if (res.filetype == 'mp4') {
|
if (res.filetype == 'mp4') {
|
||||||
url += '/raw';
|
url += '/raw';
|
||||||
}
|
}
|
||||||
i.state.postForm.url = url;
|
i.state.postForm.url = url;
|
||||||
i.state.imageLoading = false;
|
i.state.imageLoading = false;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(error => {
|
||||||
i.state.imageLoading = false;
|
i.state.imageLoading = false;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
alert(error);
|
alert(error);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
|
@ -318,7 +444,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
if (this.props.post) {
|
if (this.props.post) {
|
||||||
this.state.postForm.community_id = this.props.post.community_id;
|
this.state.postForm.community_id = this.props.post.community_id;
|
||||||
} else if (this.props.params && this.props.params.community) {
|
} else if (this.props.params && this.props.params.community) {
|
||||||
let foundCommunityId = res.communities.find(r => r.name == this.props.params.community).id;
|
let foundCommunityId = res.communities.find(
|
||||||
|
r => r.name == this.props.params.community
|
||||||
|
).id;
|
||||||
this.state.postForm.community_id = foundCommunityId;
|
this.state.postForm.community_id = foundCommunityId;
|
||||||
} else {
|
} else {
|
||||||
this.state.postForm.community_id = res.communities[0].id;
|
this.state.postForm.community_id = res.communities[0].id;
|
||||||
|
@ -334,7 +462,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
this.props.onEdit(res.post);
|
this.props.onEdit(res.post);
|
||||||
} else if (op == UserOperation.Search) {
|
} else if (op == UserOperation.Search) {
|
||||||
let res: SearchResponse = msg;
|
let res: SearchResponse = msg;
|
||||||
|
|
||||||
if (res.type_ == SearchType[SearchType.Posts]) {
|
if (res.type_ == SearchType[SearchType.Posts]) {
|
||||||
this.state.suggestedPosts = res.posts;
|
this.state.suggestedPosts = res.posts;
|
||||||
} else if (res.type_ == SearchType[SearchType.Url]) {
|
} else if (res.type_ == SearchType[SearchType.Url]) {
|
||||||
|
@ -343,7 +471,4 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,31 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { Post, CreatePostLikeForm, PostForm as PostFormI, SavePostForm, CommunityUser, UserView, BanType, BanFromCommunityForm, BanUserForm, AddModToCommunityForm, AddAdminForm, TransferSiteForm, TransferCommunityForm } from '../interfaces';
|
import {
|
||||||
|
Post,
|
||||||
|
CreatePostLikeForm,
|
||||||
|
PostForm as PostFormI,
|
||||||
|
SavePostForm,
|
||||||
|
CommunityUser,
|
||||||
|
UserView,
|
||||||
|
BanType,
|
||||||
|
BanFromCommunityForm,
|
||||||
|
BanUserForm,
|
||||||
|
AddModToCommunityForm,
|
||||||
|
AddAdminForm,
|
||||||
|
TransferSiteForm,
|
||||||
|
TransferCommunityForm,
|
||||||
|
} from '../interfaces';
|
||||||
import { MomentTime } from './moment-time';
|
import { MomentTime } from './moment-time';
|
||||||
import { PostForm } from './post-form';
|
import { PostForm } from './post-form';
|
||||||
import { mdToHtml, canMod, isMod, isImage, isVideo, getUnixTime } from '../utils';
|
import {
|
||||||
|
mdToHtml,
|
||||||
|
canMod,
|
||||||
|
isMod,
|
||||||
|
isImage,
|
||||||
|
isVideo,
|
||||||
|
getUnixTime,
|
||||||
|
} from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
|
@ -32,7 +53,6 @@ interface PostListingProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PostListing extends Component<PostListingProps, PostListingState> {
|
export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
private emptyState: PostListingState = {
|
private emptyState: PostListingState = {
|
||||||
showEdit: false,
|
showEdit: false,
|
||||||
showRemoveDialog: false,
|
showRemoveDialog: false,
|
||||||
|
@ -45,7 +65,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
showConfirmTransferCommunity: false,
|
showConfirmTransferCommunity: false,
|
||||||
imageExpanded: false,
|
imageExpanded: false,
|
||||||
viewSource: false,
|
viewSource: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -60,322 +80,625 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{!this.state.showEdit
|
{!this.state.showEdit ? (
|
||||||
? this.listing()
|
this.listing()
|
||||||
:
|
) : (
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<PostForm post={this.props.post} onEdit={this.handleEditPost} onCancel={this.handleEditCancel}/>
|
<PostForm
|
||||||
|
post={this.props.post}
|
||||||
|
onEdit={this.handleEditPost}
|
||||||
|
onCancel={this.handleEditCancel}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ${this.props.viewOnly && 'no-click'}`}>
|
<div
|
||||||
<button className={`btn p-0 ${post.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostLike)}>
|
className={`vote-bar mr-2 float-left small text-center ${this.props
|
||||||
<svg class="icon upvote"><use xlinkHref="#icon-arrow-up"></use></svg>
|
.viewOnly && 'no-click'}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={`btn p-0 ${
|
||||||
|
post.my_vote == 1 ? 'text-info' : 'text-muted'
|
||||||
|
}`}
|
||||||
|
onClick={linkEvent(this, this.handlePostLike)}
|
||||||
|
>
|
||||||
|
<svg class="icon upvote">
|
||||||
|
<use xlinkHref="#icon-arrow-up"></use>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class={`font-weight-bold text-muted`}>{post.score}</div>
|
<div class={`font-weight-bold text-muted`}>{post.score}</div>
|
||||||
<button className={`btn p-0 ${post.my_vote == -1 ? 'text-danger' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostDisLike)}>
|
<button
|
||||||
<svg class="icon downvote"><use xlinkHref="#icon-arrow-down"></use></svg>
|
className={`btn p-0 ${
|
||||||
|
post.my_vote == -1 ? 'text-danger' : 'text-muted'
|
||||||
|
}`}
|
||||||
|
onClick={linkEvent(this, this.handlePostDisLike)}
|
||||||
|
>
|
||||||
|
<svg class="icon downvote">
|
||||||
|
<use xlinkHref="#icon-arrow-down"></use>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{post.url && isImage(post.url) &&
|
{post.url && isImage(post.url) && (
|
||||||
<span title={i18n.t('expand_here')} class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)}><img class="mx-2 mt-1 float-left img-fluid thumbnail rounded" src={post.url} /></span>
|
<span
|
||||||
}
|
title={i18n.t('expand_here')}
|
||||||
{post.url && isVideo(post.url) &&
|
class="pointer"
|
||||||
<video playsinline muted loop controls class="mx-2 mt-1 float-left" height="100" width="150">
|
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="mx-2 mt-1 float-left img-fluid thumbnail rounded"
|
||||||
|
src={post.url}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{post.url && isVideo(post.url) && (
|
||||||
|
<video
|
||||||
|
playsinline
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
controls
|
||||||
|
class="mx-2 mt-1 float-left"
|
||||||
|
height="100"
|
||||||
|
width="150"
|
||||||
|
>
|
||||||
<source src={post.url} type="video/mp4" />
|
<source src={post.url} type="video/mp4" />
|
||||||
</video>
|
</video>
|
||||||
}
|
)}
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<div className="post-title">
|
<div className="post-title">
|
||||||
<h5 className="mb-0 d-inline">
|
<h5 className="mb-0 d-inline">
|
||||||
{post.url ?
|
{post.url ? (
|
||||||
<a className="text-body" href={post.url} target="_blank" title={post.url}>{post.name}</a> :
|
<a
|
||||||
<Link className="text-body" to={`/post/${post.id}`} title={i18n.t('comments')}>{post.name}</Link>
|
className="text-body"
|
||||||
}
|
href={post.url}
|
||||||
|
target="_blank"
|
||||||
|
title={post.url}
|
||||||
|
>
|
||||||
|
{post.name}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
className="text-body"
|
||||||
|
to={`/post/${post.id}`}
|
||||||
|
title={i18n.t('comments')}
|
||||||
|
>
|
||||||
|
{post.name}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</h5>
|
</h5>
|
||||||
{post.url &&
|
{post.url && (
|
||||||
<small>
|
<small>
|
||||||
<a className="ml-2 text-muted font-italic" href={post.url} target="_blank" title={post.url}>{(new URL(post.url)).hostname}</a>
|
<a
|
||||||
|
className="ml-2 text-muted font-italic"
|
||||||
|
href={post.url}
|
||||||
|
target="_blank"
|
||||||
|
title={post.url}
|
||||||
|
>
|
||||||
|
{new URL(post.url).hostname}
|
||||||
|
</a>
|
||||||
</small>
|
</small>
|
||||||
}
|
)}
|
||||||
{ post.url && isImage(post.url) &&
|
{post.url && isImage(post.url) && (
|
||||||
<>
|
<>
|
||||||
{ !this.state.imageExpanded
|
{!this.state.imageExpanded ? (
|
||||||
? <span class="text-monospace pointer ml-2 text-muted small" title={i18n.t('expand_here')} onClick={linkEvent(this, this.handleImageExpandClick)}>[+]</span>
|
<span
|
||||||
:
|
class="text-monospace pointer ml-2 text-muted small"
|
||||||
|
title={i18n.t('expand_here')}
|
||||||
|
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||||
|
>
|
||||||
|
[+]
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
<span>
|
<span>
|
||||||
<span class="text-monospace pointer ml-2 text-muted small" onClick={linkEvent(this, this.handleImageExpandClick)}>[-]</span>
|
<span
|
||||||
|
class="text-monospace pointer ml-2 text-muted small"
|
||||||
|
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||||
|
>
|
||||||
|
[-]
|
||||||
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)}><img class="img-fluid" src={post.url} /></span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||||
|
>
|
||||||
|
<img class="img-fluid" src={post.url} />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{post.removed &&
|
{post.removed && (
|
||||||
<small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
|
<small className="ml-2 text-muted font-italic">
|
||||||
}
|
<T i18nKey="removed">#</T>
|
||||||
{post.deleted &&
|
</small>
|
||||||
<small className="ml-2 text-muted font-italic"><T i18nKey="deleted">#</T></small>
|
)}
|
||||||
}
|
{post.deleted && (
|
||||||
{post.locked &&
|
<small className="ml-2 text-muted font-italic">
|
||||||
<small className="ml-2 text-muted font-italic"><T i18nKey="locked">#</T></small>
|
<T i18nKey="deleted">#</T>
|
||||||
}
|
</small>
|
||||||
{post.stickied &&
|
)}
|
||||||
<small className="ml-2 text-muted font-italic"><T i18nKey="stickied">#</T></small>
|
{post.locked && (
|
||||||
}
|
<small className="ml-2 text-muted font-italic">
|
||||||
{post.nsfw &&
|
<T i18nKey="locked">#</T>
|
||||||
<small className="ml-2 text-muted font-italic"><T i18nKey="nsfw">#</T></small>
|
</small>
|
||||||
}
|
)}
|
||||||
|
{post.stickied && (
|
||||||
|
<small className="ml-2 text-muted font-italic">
|
||||||
|
<T i18nKey="stickied">#</T>
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
{post.nsfw && (
|
||||||
|
<small className="ml-2 text-muted font-italic">
|
||||||
|
<T i18nKey="nsfw">#</T>
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="details ml-4">
|
<div className="details ml-4">
|
||||||
<ul class="list-inline mb-0 text-muted small">
|
<ul class="list-inline mb-0 text-muted small">
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span>{i18n.t('by')} </span>
|
<span>{i18n.t('by')} </span>
|
||||||
<Link className="text-info" to={`/u/${post.creator_name}`}>{post.creator_name}</Link>
|
<Link className="text-info" to={`/u/${post.creator_name}`}>
|
||||||
{this.isMod &&
|
{post.creator_name}
|
||||||
<span className="mx-1 badge badge-light"><T i18nKey="mod">#</T></span>
|
</Link>
|
||||||
}
|
{this.isMod && (
|
||||||
{this.isAdmin &&
|
<span className="mx-1 badge badge-light">
|
||||||
<span className="mx-1 badge badge-light"><T i18nKey="admin">#</T></span>
|
<T i18nKey="mod">#</T>
|
||||||
}
|
</span>
|
||||||
{(post.banned_from_community || post.banned) &&
|
)}
|
||||||
<span className="mx-1 badge badge-danger"><T i18nKey="banned">#</T></span>
|
{this.isAdmin && (
|
||||||
}
|
<span className="mx-1 badge badge-light">
|
||||||
{this.props.showCommunity &&
|
<T i18nKey="admin">#</T>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{(post.banned_from_community || post.banned) && (
|
||||||
|
<span className="mx-1 badge badge-danger">
|
||||||
|
<T i18nKey="banned">#</T>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{this.props.showCommunity && (
|
||||||
<span>
|
<span>
|
||||||
<span> {i18n.t('to')} </span>
|
<span> {i18n.t('to')} </span>
|
||||||
<Link to={`/c/${post.community_name}`}>{post.community_name}</Link>
|
<Link to={`/c/${post.community_name}`}>
|
||||||
|
{post.community_name}
|
||||||
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
}
|
)}
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span><MomentTime data={post} /></span>
|
<span>
|
||||||
|
<MomentTime data={post} />
|
||||||
|
</span>
|
||||||
</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">+{post.upvotes}</span>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<span className="text-danger">-{post.downvotes}</span>
|
<span className="text-danger">-{post.downvotes}</span>
|
||||||
<span>) </span>
|
<span>) </span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<Link className="text-muted" to={`/post/${post.id}`}><T i18nKey="number_of_comments" interpolation={{count: post.number_of_comments}}>#</T></Link>
|
<Link className="text-muted" to={`/post/${post.id}`}>
|
||||||
|
<T
|
||||||
|
i18nKey="number_of_comments"
|
||||||
|
interpolation={{ count: post.number_of_comments }}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||||
{UserService.Instance.user &&
|
{UserService.Instance.user && (
|
||||||
<>
|
<>
|
||||||
{this.props.showBody &&
|
{this.props.showBody && (
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item mr-2">
|
<li className="list-inline-item mr-2">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleSavePostClick)}>{post.saved ? i18n.t('unsave') : i18n.t('save')}</span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleSavePostClick)}
|
||||||
|
>
|
||||||
|
{post.saved ? i18n.t('unsave') : i18n.t('save')}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item mr-2">
|
<li className="list-inline-item mr-2">
|
||||||
<Link className="text-muted" to={`/create_post${this.crossPostParams}`}><T i18nKey="cross_post">#</T></Link>
|
<Link
|
||||||
|
className="text-muted"
|
||||||
|
to={`/create_post${this.crossPostParams}`}
|
||||||
|
>
|
||||||
|
<T i18nKey="cross_post">#</T>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{this.myPost &&
|
{this.myPost && (
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleEditClick)}
|
||||||
|
>
|
||||||
|
<T i18nKey="edit">#</T>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item mr-2">
|
<li className="list-inline-item mr-2">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||||
|
>
|
||||||
{!post.deleted ? i18n.t('delete') : i18n.t('restore')}
|
{!post.deleted ? i18n.t('delete') : i18n.t('restore')}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{this.canModOnSelf &&
|
{this.canModOnSelf && (
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModLock)}>{post.locked ? i18n.t('unlock') : i18n.t('lock')}</span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleModLock)}
|
||||||
|
>
|
||||||
|
{post.locked ? i18n.t('unlock') : i18n.t('lock')}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModSticky)}>{post.stickied ? i18n.t('unsticky') : i18n.t('sticky')}</span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleModSticky)}
|
||||||
|
>
|
||||||
|
{post.stickied ? i18n.t('unsticky') : i18n.t('sticky')}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{/* Mods can ban from community, and appoint as mods to community */}
|
{/* Mods can ban from community, and appoint as mods to community */}
|
||||||
{(this.canMod || this.canAdmin) &&
|
{(this.canMod || this.canAdmin) && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!post.removed ?
|
{!post.removed ? (
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
|
<span
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
|
class="pointer"
|
||||||
}
|
onClick={linkEvent(this, this.handleModRemoveShow)}
|
||||||
|
>
|
||||||
|
<T i18nKey="remove">#</T>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleModRemoveSubmit)}
|
||||||
|
>
|
||||||
|
<T i18nKey="restore">#</T>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
{this.canMod &&
|
{this.canMod && (
|
||||||
<>
|
<>
|
||||||
{!this.isMod &&
|
{!this.isMod && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!post.banned_from_community ?
|
{!post.banned_from_community ? (
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}><T i18nKey="ban">#</T></span> :
|
<span
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}><T i18nKey="unban">#</T></span>
|
class="pointer"
|
||||||
}
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleModBanFromCommunityShow
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="ban">#</T>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleModBanFromCommunitySubmit
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="unban">#</T>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
{!post.banned_from_community &&
|
{!post.banned_from_community && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{this.isMod ? i18n.t('remove_as_mod') : i18n.t('appoint_as_mod')}</span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleAddModToCommunity
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{this.isMod
|
||||||
|
? i18n.t('remove_as_mod')
|
||||||
|
: i18n.t('appoint_as_mod')}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{/* Community creators and admins can transfer community to another mod */}
|
{/* Community creators and admins can transfer community to another mod */}
|
||||||
{(this.amCommunityCreator || this.canAdmin) && this.isMod &&
|
{(this.amCommunityCreator || this.canAdmin) && this.isMod && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!this.state.showConfirmTransferCommunity ?
|
{!this.state.showConfirmTransferCommunity ? (
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferCommunity)}><T i18nKey="transfer_community">#</T>
|
<span
|
||||||
</span> : <>
|
class="pointer"
|
||||||
<span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
|
onClick={linkEvent(
|
||||||
<span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferCommunity)}><T i18nKey="yes">#</T></span>
|
this,
|
||||||
<span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferCommunity)}><T i18nKey="no">#</T></span>
|
this.handleShowConfirmTransferCommunity
|
||||||
</>
|
)}
|
||||||
}
|
>
|
||||||
|
<T i18nKey="transfer_community">#</T>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span class="d-inline-block mr-1">
|
||||||
|
<T i18nKey="are_you_sure">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block mr-1"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleTransferCommunity
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="yes">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleCancelShowConfirmTransferCommunity
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="no">#</T>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
{/* Admins can ban from all, and appoint other admins */}
|
{/* Admins can ban from all, and appoint other admins */}
|
||||||
{this.canAdmin &&
|
{this.canAdmin && (
|
||||||
<>
|
<>
|
||||||
{!this.isAdmin &&
|
{!this.isAdmin && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!post.banned ?
|
{!post.banned ? (
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}><T i18nKey="ban_from_site">#</T></span> :
|
<span
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}><T i18nKey="unban_from_site">#</T></span>
|
class="pointer"
|
||||||
}
|
onClick={linkEvent(this, this.handleModBanShow)}
|
||||||
|
>
|
||||||
|
<T i18nKey="ban_from_site">#</T>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleModBanSubmit)}
|
||||||
|
>
|
||||||
|
<T i18nKey="unban_from_site">#</T>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
{!post.banned &&
|
{!post.banned && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleAddAdmin)}>{this.isAdmin ? i18n.t('remove_as_admin') : i18n.t('appoint_as_admin')}</span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleAddAdmin)}
|
||||||
|
>
|
||||||
|
{this.isAdmin
|
||||||
|
? i18n.t('remove_as_admin')
|
||||||
|
: i18n.t('appoint_as_admin')}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{/* Site Creator can transfer to another admin */}
|
{/* Site Creator can transfer to another admin */}
|
||||||
{this.amSiteCreator && this.isAdmin &&
|
{this.amSiteCreator && this.isAdmin && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!this.state.showConfirmTransferSite ?
|
{!this.state.showConfirmTransferSite ? (
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferSite)}><T i18nKey="transfer_site">#</T>
|
<span
|
||||||
</span> : <>
|
class="pointer"
|
||||||
<span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
|
onClick={linkEvent(
|
||||||
<span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferSite)}><T i18nKey="yes">#</T></span>
|
this,
|
||||||
<span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferSite)}><T i18nKey="no">#</T></span>
|
this.handleShowConfirmTransferSite
|
||||||
</>
|
)}
|
||||||
}
|
>
|
||||||
|
<T i18nKey="transfer_site">#</T>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span class="d-inline-block mr-1">
|
||||||
|
<T i18nKey="are_you_sure">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block mr-1"
|
||||||
|
onClick={linkEvent(this, this.handleTransferSite)}
|
||||||
|
>
|
||||||
|
<T i18nKey="yes">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleCancelShowConfirmTransferSite
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="no">#</T>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{this.props.showBody && post.body &&
|
{this.props.showBody && post.body && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span className="pointer" onClick={linkEvent(this, this.handleViewSource)}><T i18nKey="view_source">#</T></span>
|
<span
|
||||||
|
className="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleViewSource)}
|
||||||
|
>
|
||||||
|
<T i18nKey="view_source">#</T>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
{this.state.showRemoveDialog &&
|
{this.state.showRemoveDialog && (
|
||||||
<form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
|
<form
|
||||||
<input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
|
class="form-inline"
|
||||||
<button type="submit" class="btn btn-secondary"><T i18nKey="remove_post">#</T></button>
|
onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control mr-2"
|
||||||
|
placeholder={i18n.t('reason')}
|
||||||
|
value={this.state.removeReason}
|
||||||
|
onInput={linkEvent(this, this.handleModRemoveReasonChange)}
|
||||||
|
/>
|
||||||
|
<button type="submit" class="btn btn-secondary">
|
||||||
|
<T i18nKey="remove_post">#</T>
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
}
|
)}
|
||||||
{this.state.showBanDialog &&
|
{this.state.showBanDialog && (
|
||||||
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-form-label"><T i18nKey="reason">#</T></label>
|
<label class="col-form-label">
|
||||||
<input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
|
<T i18nKey="reason">#</T>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control mr-2"
|
||||||
|
placeholder={i18n.t('reason')}
|
||||||
|
value={this.state.banReason}
|
||||||
|
onInput={linkEvent(this, this.handleModBanReasonChange)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* TODO hold off on expires until later */}
|
{/* TODO hold off on expires until later */}
|
||||||
{/* <div class="form-group row"> */}
|
{/* <div class="form-group row"> */}
|
||||||
{/* <label class="col-form-label">Expires</label> */}
|
{/* <label class="col-form-label">Expires</label> */}
|
||||||
{/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
|
{/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<button type="submit" class="btn btn-secondary">{i18n.t('ban')} {post.creator_name}</button>
|
<button type="submit" class="btn btn-secondary">
|
||||||
</div>
|
{i18n.t('ban')} {post.creator_name}
|
||||||
</form>
|
</button>
|
||||||
}
|
</div>
|
||||||
{this.props.showBody && post.body &&
|
</form>
|
||||||
<>
|
)}
|
||||||
{this.state.viewSource ? <pre>{post.body}</pre> :
|
{this.props.showBody && post.body && (
|
||||||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(post.body)} />
|
<>
|
||||||
}
|
{this.state.viewSource ? (
|
||||||
</>
|
<pre>{post.body}</pre>
|
||||||
}
|
) : (
|
||||||
|
<div
|
||||||
|
className="md-div"
|
||||||
|
dangerouslySetInnerHTML={mdToHtml(post.body)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get myPost(): boolean {
|
private get myPost(): boolean {
|
||||||
return UserService.Instance.user && this.props.post.creator_id == UserService.Instance.user.id;
|
return (
|
||||||
|
UserService.Instance.user &&
|
||||||
|
this.props.post.creator_id == UserService.Instance.user.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isMod(): boolean {
|
get isMod(): boolean {
|
||||||
return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.post.creator_id);
|
return (
|
||||||
|
this.props.moderators &&
|
||||||
|
isMod(
|
||||||
|
this.props.moderators.map(m => m.user_id),
|
||||||
|
this.props.post.creator_id
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isAdmin(): boolean {
|
get isAdmin(): boolean {
|
||||||
return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.post.creator_id);
|
return (
|
||||||
|
this.props.admins &&
|
||||||
|
isMod(this.props.admins.map(a => a.id), this.props.post.creator_id)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get canMod(): boolean {
|
get canMod(): boolean {
|
||||||
if (this.props.admins && this.props.moderators) {
|
if (this.props.admins && this.props.moderators) {
|
||||||
let adminsThenMods = this.props.admins.map(a => a.id)
|
let adminsThenMods = this.props.admins
|
||||||
.concat(this.props.moderators.map(m => m.user_id));
|
.map(a => a.id)
|
||||||
|
.concat(this.props.moderators.map(m => m.user_id));
|
||||||
|
|
||||||
return canMod(UserService.Instance.user, adminsThenMods, this.props.post.creator_id);
|
return canMod(
|
||||||
} else {
|
UserService.Instance.user,
|
||||||
|
adminsThenMods,
|
||||||
|
this.props.post.creator_id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get canModOnSelf(): boolean {
|
get canModOnSelf(): boolean {
|
||||||
if (this.props.admins && this.props.moderators) {
|
if (this.props.admins && this.props.moderators) {
|
||||||
let adminsThenMods = this.props.admins.map(a => a.id)
|
let adminsThenMods = this.props.admins
|
||||||
.concat(this.props.moderators.map(m => m.user_id));
|
.map(a => a.id)
|
||||||
|
.concat(this.props.moderators.map(m => m.user_id));
|
||||||
|
|
||||||
return canMod(UserService.Instance.user, adminsThenMods, this.props.post.creator_id, true);
|
return canMod(
|
||||||
} else {
|
UserService.Instance.user,
|
||||||
|
adminsThenMods,
|
||||||
|
this.props.post.creator_id,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get canAdmin(): boolean {
|
get canAdmin(): boolean {
|
||||||
return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.post.creator_id);
|
return (
|
||||||
|
this.props.admins &&
|
||||||
|
canMod(
|
||||||
|
UserService.Instance.user,
|
||||||
|
this.props.admins.map(a => a.id),
|
||||||
|
this.props.post.creator_id
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get amCommunityCreator(): boolean {
|
get amCommunityCreator(): boolean {
|
||||||
return this.props.moderators &&
|
return (
|
||||||
UserService.Instance.user &&
|
this.props.moderators &&
|
||||||
(this.props.post.creator_id != UserService.Instance.user.id) &&
|
UserService.Instance.user &&
|
||||||
(UserService.Instance.user.id == this.props.moderators[0].user_id);
|
this.props.post.creator_id != UserService.Instance.user.id &&
|
||||||
|
UserService.Instance.user.id == this.props.moderators[0].user_id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get amSiteCreator(): boolean {
|
get amSiteCreator(): boolean {
|
||||||
return this.props.admins &&
|
return (
|
||||||
UserService.Instance.user &&
|
this.props.admins &&
|
||||||
(this.props.post.creator_id != UserService.Instance.user.id) &&
|
UserService.Instance.user &&
|
||||||
(UserService.Instance.user.id == this.props.admins[0].id);
|
this.props.post.creator_id != UserService.Instance.user.id &&
|
||||||
|
UserService.Instance.user.id == this.props.admins[0].id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePostLike(i: PostListing) {
|
handlePostLike(i: PostListing) {
|
||||||
|
|
||||||
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.props.post.my_vote == 1 ? 0 : 1,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.likePost(form);
|
WebSocketService.Instance.likePost(form);
|
||||||
}
|
}
|
||||||
|
@ -383,7 +706,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
handlePostDisLike(i: PostListing) {
|
handlePostDisLike(i: PostListing) {
|
||||||
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.props.post.my_vote == -1 ? 0 : -1,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.likePost(form);
|
WebSocketService.Instance.likePost(form);
|
||||||
}
|
}
|
||||||
|
@ -414,16 +737,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
creator_id: i.props.post.creator_id,
|
creator_id: i.props.post.creator_id,
|
||||||
deleted: !i.props.post.deleted,
|
deleted: !i.props.post.deleted,
|
||||||
nsfw: i.props.post.nsfw,
|
nsfw: i.props.post.nsfw,
|
||||||
auth: null
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editPost(deleteForm);
|
WebSocketService.Instance.editPost(deleteForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSavePostClick(i: PostListing) {
|
handleSavePostClick(i: PostListing) {
|
||||||
let saved = (i.props.post.saved == undefined) ? true : !i.props.post.saved;
|
let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
|
||||||
let form: SavePostForm = {
|
let form: SavePostForm = {
|
||||||
post_id: i.props.post.id,
|
post_id: i.props.post.id,
|
||||||
save: saved
|
save: saved,
|
||||||
};
|
};
|
||||||
|
|
||||||
WebSocketService.Instance.savePost(form);
|
WebSocketService.Instance.savePost(form);
|
||||||
|
@ -573,12 +896,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowConfirmTransferCommunity(i: PostListing) {
|
handleShowConfirmTransferCommunity(i: PostListing) {
|
||||||
i.state.showConfirmTransferCommunity = true;
|
i.state.showConfirmTransferCommunity = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCancelShowConfirmTransferCommunity(i: PostListing) {
|
handleCancelShowConfirmTransferCommunity(i: PostListing) {
|
||||||
i.state.showConfirmTransferCommunity = false;
|
i.state.showConfirmTransferCommunity = false;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
@ -593,12 +916,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowConfirmTransferSite(i: PostListing) {
|
handleShowConfirmTransferSite(i: PostListing) {
|
||||||
i.state.showConfirmTransferSite = true;
|
i.state.showConfirmTransferSite = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCancelShowConfirmTransferSite(i: PostListing) {
|
handleCancelShowConfirmTransferSite(i: PostListing) {
|
||||||
i.state.showConfirmTransferSite = false;
|
i.state.showConfirmTransferSite = false;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
@ -622,4 +945,3 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ interface PostListingsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PostListings extends Component<PostListingsProps, any> {
|
export class PostListings extends Component<PostListingsProps, any> {
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
}
|
}
|
||||||
|
@ -18,19 +17,32 @@ export class PostListings extends Component<PostListingsProps, any> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.props.posts.length > 0 ? this.props.posts.map(post =>
|
{this.props.posts.length > 0 ? (
|
||||||
|
this.props.posts.map(post => (
|
||||||
|
<>
|
||||||
|
<PostListing
|
||||||
|
post={post}
|
||||||
|
showCommunity={this.props.showCommunity}
|
||||||
|
/>
|
||||||
|
<hr class="d-md-none my-2" />
|
||||||
|
<div class="d-none d-md-block my-2"></div>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<PostListing post={post} showCommunity={this.props.showCommunity} />
|
<div>
|
||||||
<hr class="d-md-none my-2" />
|
<T i18nKey="no_posts">#</T>
|
||||||
<div class="d-none d-md-block my-2"></div>
|
</div>
|
||||||
|
{this.props.showCommunity !== undefined && (
|
||||||
|
<div>
|
||||||
|
<T i18nKey="subscribe_to_communities">
|
||||||
|
#<Link to="/communities">#</Link>
|
||||||
|
</T>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) :
|
)}
|
||||||
<>
|
|
||||||
<div><T i18nKey="no_posts">#</T></div>
|
|
||||||
{this.props.showCommunity !== undefined && <div><T i18nKey="subscribe_to_communities">#<Link to="/communities">#</Link></T></div>}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,32 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, BanUserResponse, AddModToCommunityResponse, AddAdminResponse, UserView, SearchType, SortType, SearchForm, SearchResponse, GetSiteResponse, GetCommunityResponse } from '../interfaces';
|
import {
|
||||||
|
UserOperation,
|
||||||
|
Community,
|
||||||
|
Post as PostI,
|
||||||
|
GetPostResponse,
|
||||||
|
PostResponse,
|
||||||
|
Comment,
|
||||||
|
CommentForm as CommentFormI,
|
||||||
|
CommentResponse,
|
||||||
|
CommentSortType,
|
||||||
|
CreatePostLikeResponse,
|
||||||
|
CommunityUser,
|
||||||
|
CommunityResponse,
|
||||||
|
CommentNode as CommentNodeI,
|
||||||
|
BanFromCommunityResponse,
|
||||||
|
BanUserResponse,
|
||||||
|
AddModToCommunityResponse,
|
||||||
|
AddAdminResponse,
|
||||||
|
UserView,
|
||||||
|
SearchType,
|
||||||
|
SortType,
|
||||||
|
SearchForm,
|
||||||
|
SearchResponse,
|
||||||
|
GetSiteResponse,
|
||||||
|
GetCommunityResponse,
|
||||||
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { msgOp, hotRank } from '../utils';
|
import { msgOp, hotRank } from '../utils';
|
||||||
import { PostListing } from './post-listing';
|
import { PostListing } from './post-listing';
|
||||||
|
@ -27,7 +52,6 @@ interface PostState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Post extends Component<any, PostState> {
|
export class Post extends Component<any, PostState> {
|
||||||
|
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
private emptyState: PostState = {
|
private emptyState: PostState = {
|
||||||
post: null,
|
post: null,
|
||||||
|
@ -36,10 +60,10 @@ export class Post extends Component<any, PostState> {
|
||||||
community: null,
|
community: null,
|
||||||
moderators: [],
|
moderators: [],
|
||||||
admins: [],
|
admins: [],
|
||||||
scrolled: false,
|
scrolled: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
crossPosts: [],
|
crossPosts: [],
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -52,10 +76,17 @@ export class Post extends Component<any, PostState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
(msg) => this.parseMessage(msg),
|
msg => this.parseMessage(msg),
|
||||||
(err) => console.error(err),
|
err => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -71,10 +102,16 @@ export class Post extends Component<any, PostState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
|
componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
|
||||||
if (this.state.scrolled_comment_id && !this.state.scrolled && lastState.comments.length > 0) {
|
if (
|
||||||
var elmnt = document.getElementById(`comment-${this.state.scrolled_comment_id}`);
|
this.state.scrolled_comment_id &&
|
||||||
elmnt.scrollIntoView();
|
!this.state.scrolled &&
|
||||||
elmnt.classList.add("mark");
|
lastState.comments.length > 0
|
||||||
|
) {
|
||||||
|
var elmnt = document.getElementById(
|
||||||
|
`comment-${this.state.scrolled_comment_id}`
|
||||||
|
);
|
||||||
|
elmnt.scrollIntoView();
|
||||||
|
elmnt.classList.add('mark');
|
||||||
this.state.scrolled = true;
|
this.state.scrolled = true;
|
||||||
this.markScrolledAsRead(this.state.scrolled_comment_id);
|
this.markScrolledAsRead(this.state.scrolled_comment_id);
|
||||||
}
|
}
|
||||||
|
@ -89,17 +126,20 @@ export class Post extends Component<any, PostState> {
|
||||||
// this.context.router.history.push('/sponsors');
|
// this.context.router.history.push('/sponsors');
|
||||||
// this.context.refresh();
|
// this.context.refresh();
|
||||||
// this.context.router.history.push(_lastProps.location.pathname);
|
// this.context.router.history.push(_lastProps.location.pathname);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
markScrolledAsRead(commentId: number) {
|
markScrolledAsRead(commentId: number) {
|
||||||
let found = this.state.comments.find(c => c.id == commentId);
|
let found = this.state.comments.find(c => c.id == commentId);
|
||||||
let parent = this.state.comments.find(c => found.parent_id == c.id);
|
let parent = this.state.comments.find(c => found.parent_id == c.id);
|
||||||
let parent_user_id = parent ? parent.creator_id : this.state.post.creator_id;
|
let parent_user_id = parent
|
||||||
|
? parent.creator_id
|
||||||
if (UserService.Instance.user && UserService.Instance.user.id == parent_user_id) {
|
: this.state.post.creator_id;
|
||||||
|
|
||||||
|
if (
|
||||||
|
UserService.Instance.user &&
|
||||||
|
UserService.Instance.user.id == parent_user_id
|
||||||
|
) {
|
||||||
let form: CommentFormI = {
|
let form: CommentFormI = {
|
||||||
content: found.content,
|
content: found.content,
|
||||||
edit_id: found.id,
|
edit_id: found.id,
|
||||||
|
@ -107,7 +147,7 @@ export class Post extends Component<any, PostState> {
|
||||||
post_id: found.post_id,
|
post_id: found.post_id,
|
||||||
parent_id: found.parent_id,
|
parent_id: found.parent_id,
|
||||||
read: true,
|
read: true,
|
||||||
auth: null
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.editComment(form);
|
WebSocketService.Instance.editComment(form);
|
||||||
}
|
}
|
||||||
|
@ -116,26 +156,36 @@ export class Post extends Component<any, PostState> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{this.state.loading ?
|
{this.state.loading ? (
|
||||||
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
<h5>
|
||||||
<div class="row">
|
<svg class="icon icon-spinner spin">
|
||||||
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
|
</svg>
|
||||||
|
</h5>
|
||||||
|
) : (
|
||||||
|
<div class="row">
|
||||||
<div class="col-12 col-md-8 mb-3">
|
<div class="col-12 col-md-8 mb-3">
|
||||||
<PostListing
|
<PostListing
|
||||||
post={this.state.post}
|
post={this.state.post}
|
||||||
showBody
|
showBody
|
||||||
showCommunity
|
showCommunity
|
||||||
editable
|
editable
|
||||||
moderators={this.state.moderators}
|
moderators={this.state.moderators}
|
||||||
admins={this.state.admins}
|
admins={this.state.admins}
|
||||||
/>
|
/>
|
||||||
{this.state.crossPosts.length > 0 &&
|
{this.state.crossPosts.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div class="my-1 text-muted small font-weight-bold"><T i18nKey="cross_posts">#</T></div>
|
<div class="my-1 text-muted small font-weight-bold">
|
||||||
|
<T i18nKey="cross_posts">#</T>
|
||||||
|
</div>
|
||||||
<PostListings showCommunity posts={this.state.crossPosts} />
|
<PostListings showCommunity posts={this.state.crossPosts} />
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
<div className="mb-2" />
|
<div className="mb-2" />
|
||||||
<CommentForm postId={this.state.post.id} disabled={this.state.post.locked} />
|
<CommentForm
|
||||||
|
postId={this.state.post.id}
|
||||||
|
disabled={this.state.post.locked}
|
||||||
|
/>
|
||||||
{this.sortRadios()}
|
{this.sortRadios()}
|
||||||
{this.commentsTree()}
|
{this.commentsTree()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -144,65 +194,88 @@ export class Post extends Component<any, PostState> {
|
||||||
{this.sidebar()}
|
{this.sidebar()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sortRadios() {
|
sortRadios() {
|
||||||
return (
|
return (
|
||||||
<div class="btn-group btn-group-toggle mb-3">
|
<div class="btn-group btn-group-toggle mb-3">
|
||||||
<label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Hot && 'active'}`}>{i18n.t('hot')}
|
<label
|
||||||
<input type="radio" value={CommentSortType.Hot}
|
className={`btn btn-sm btn-secondary pointer ${this.state
|
||||||
checked={this.state.commentSort === CommentSortType.Hot}
|
.commentSort === CommentSortType.Hot && 'active'}`}
|
||||||
onChange={linkEvent(this, this.handleCommentSortChange)} />
|
>
|
||||||
|
{i18n.t('hot')}
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value={CommentSortType.Hot}
|
||||||
|
checked={this.state.commentSort === CommentSortType.Hot}
|
||||||
|
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Top && 'active'}`}>{i18n.t('top')}
|
<label
|
||||||
<input type="radio" value={CommentSortType.Top}
|
className={`btn btn-sm btn-secondary pointer ${this.state
|
||||||
checked={this.state.commentSort === CommentSortType.Top}
|
.commentSort === CommentSortType.Top && 'active'}`}
|
||||||
onChange={linkEvent(this, this.handleCommentSortChange)} />
|
>
|
||||||
|
{i18n.t('top')}
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value={CommentSortType.Top}
|
||||||
|
checked={this.state.commentSort === CommentSortType.Top}
|
||||||
|
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.New && 'active'}`}>{i18n.t('new')}
|
<label
|
||||||
<input type="radio" value={CommentSortType.New}
|
className={`btn btn-sm btn-secondary pointer ${this.state
|
||||||
checked={this.state.commentSort === CommentSortType.New}
|
.commentSort === CommentSortType.New && 'active'}`}
|
||||||
onChange={linkEvent(this, this.handleCommentSortChange)} />
|
>
|
||||||
|
{i18n.t('new')}
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value={CommentSortType.New}
|
||||||
|
checked={this.state.commentSort === CommentSortType.New}
|
||||||
|
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
newComments() {
|
newComments() {
|
||||||
return (
|
return (
|
||||||
<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><T i18nKey="recent_comments">#</T></h6>
|
<h6>
|
||||||
{this.state.comments.map(comment =>
|
<T i18nKey="recent_comments">#</T>
|
||||||
<CommentNodes
|
</h6>
|
||||||
nodes={[{comment: comment}]}
|
{this.state.comments.map(comment => (
|
||||||
noIndent
|
<CommentNodes
|
||||||
locked={this.state.post.locked}
|
nodes={[{ comment: comment }]}
|
||||||
moderators={this.state.moderators}
|
noIndent
|
||||||
|
locked={this.state.post.locked}
|
||||||
|
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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sidebar() {
|
sidebar() {
|
||||||
return (
|
return (
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
community={this.state.community}
|
community={this.state.community}
|
||||||
moderators={this.state.moderators}
|
moderators={this.state.moderators}
|
||||||
admins={this.state.admins}
|
admins={this.state.admins}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommentSortChange(i: Post, event: any) {
|
handleCommentSortChange(i: Post, event: any) {
|
||||||
i.state.commentSort = Number(event.target.value);
|
i.state.commentSort = Number(event.target.value);
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
|
@ -213,16 +286,15 @@ export class Post extends Component<any, PostState> {
|
||||||
for (let comment of this.state.comments) {
|
for (let comment of this.state.comments) {
|
||||||
let node: CommentNodeI = {
|
let node: CommentNodeI = {
|
||||||
comment: comment,
|
comment: comment,
|
||||||
children: []
|
children: [],
|
||||||
};
|
};
|
||||||
map.set(comment.id, { ...node });
|
map.set(comment.id, { ...node });
|
||||||
}
|
}
|
||||||
let tree: Array<CommentNodeI> = [];
|
let tree: Array<CommentNodeI> = [];
|
||||||
for (let comment of this.state.comments) {
|
for (let comment of this.state.comments) {
|
||||||
if( comment.parent_id ) {
|
if (comment.parent_id) {
|
||||||
map.get(comment.parent_id).children.push(map.get(comment.id));
|
map.get(comment.parent_id).children.push(map.get(comment.id));
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
tree.push(map.get(comment.id));
|
tree.push(map.get(comment.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,36 +305,43 @@ export class Post extends Component<any, PostState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
sortTree(tree: Array<CommentNodeI>) {
|
sortTree(tree: Array<CommentNodeI>) {
|
||||||
|
|
||||||
// First, put removed and deleted comments at the bottom, then do your other sorts
|
// First, put removed and deleted comments at the bottom, then do your other sorts
|
||||||
if (this.state.commentSort == CommentSortType.Top) {
|
if (this.state.commentSort == CommentSortType.Top) {
|
||||||
tree.sort((a, b) => (+a.comment.removed - +b.comment.removed) ||
|
tree.sort(
|
||||||
(+a.comment.deleted - +b.comment.deleted ) ||
|
(a, b) =>
|
||||||
(b.comment.score - a.comment.score));
|
+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) {
|
} else if (this.state.commentSort == CommentSortType.New) {
|
||||||
tree.sort((a, b) => (+a.comment.removed - +b.comment.removed) ||
|
tree.sort(
|
||||||
(+a.comment.deleted - +b.comment.deleted ) ||
|
(a, b) =>
|
||||||
(b.comment.published.localeCompare(a.comment.published)));
|
+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.Hot) {
|
} else if (this.state.commentSort == CommentSortType.Hot) {
|
||||||
tree.sort((a, b) => (+a.comment.removed - +b.comment.removed) ||
|
tree.sort(
|
||||||
(+a.comment.deleted - +b.comment.deleted ) ||
|
(a, b) =>
|
||||||
(hotRank(b.comment) - hotRank(a.comment)));
|
+a.comment.removed - +b.comment.removed ||
|
||||||
|
+a.comment.deleted - +b.comment.deleted ||
|
||||||
|
hotRank(b.comment) - hotRank(a.comment)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let node of tree) {
|
for (let node of tree) {
|
||||||
this.sortTree(node.children);
|
this.sortTree(node.children);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commentsTree() {
|
commentsTree() {
|
||||||
let nodes = this.buildCommentsTree();
|
let nodes = this.buildCommentsTree();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CommentNodes
|
<CommentNodes
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
|
@ -286,7 +365,7 @@ export class Post extends Component<any, PostState> {
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
|
document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
|
||||||
|
|
||||||
// Get cross-posts
|
// Get cross-posts
|
||||||
if (this.state.post.url) {
|
if (this.state.post.url) {
|
||||||
let form: SearchForm = {
|
let form: SearchForm = {
|
||||||
q: this.state.post.url,
|
q: this.state.post.url,
|
||||||
|
@ -297,7 +376,7 @@ export class Post extends Component<any, PostState> {
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.search(form);
|
WebSocketService.Instance.search(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.CreateComment) {
|
} else if (op == UserOperation.CreateComment) {
|
||||||
let res: CommentResponse = msg;
|
let res: CommentResponse = msg;
|
||||||
|
@ -323,12 +402,13 @@ export class Post extends Component<any, PostState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.CreateCommentLike) {
|
} else if (op == UserOperation.CreateCommentLike) {
|
||||||
let res: CommentResponse = msg;
|
let res: CommentResponse = msg;
|
||||||
let found: Comment = this.state.comments.find(c => c.id === res.comment.id);
|
let found: Comment = this.state.comments.find(
|
||||||
|
c => c.id === res.comment.id
|
||||||
|
);
|
||||||
found.score = res.comment.score;
|
found.score = res.comment.score;
|
||||||
found.upvotes = res.comment.upvotes;
|
found.upvotes = res.comment.upvotes;
|
||||||
found.downvotes = res.comment.downvotes;
|
found.downvotes = res.comment.downvotes;
|
||||||
if (res.comment.my_vote !== null)
|
if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
|
||||||
found.my_vote = res.comment.my_vote;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.CreatePostLike) {
|
} else if (op == UserOperation.CreatePostLike) {
|
||||||
let res: CreatePostLikeResponse = msg;
|
let res: CreatePostLikeResponse = msg;
|
||||||
|
@ -354,13 +434,15 @@ export class Post extends Component<any, PostState> {
|
||||||
} else if (op == UserOperation.FollowCommunity) {
|
} else if (op == UserOperation.FollowCommunity) {
|
||||||
let res: CommunityResponse = msg;
|
let res: CommunityResponse = msg;
|
||||||
this.state.community.subscribed = res.community.subscribed;
|
this.state.community.subscribed = res.community.subscribed;
|
||||||
this.state.community.number_of_subscribers = res.community.number_of_subscribers;
|
this.state.community.number_of_subscribers =
|
||||||
|
res.community.number_of_subscribers;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.BanFromCommunity) {
|
} else if (op == UserOperation.BanFromCommunity) {
|
||||||
let res: BanFromCommunityResponse = msg;
|
let res: BanFromCommunityResponse = msg;
|
||||||
this.state.comments.filter(c => c.creator_id == res.user.id)
|
this.state.comments
|
||||||
.forEach(c => c.banned_from_community = res.banned);
|
.filter(c => c.creator_id == res.user.id)
|
||||||
if (this.state.post.creator_id == res.user.id) {
|
.forEach(c => (c.banned_from_community = res.banned));
|
||||||
|
if (this.state.post.creator_id == res.user.id) {
|
||||||
this.state.post.banned_from_community = res.banned;
|
this.state.post.banned_from_community = res.banned;
|
||||||
}
|
}
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
@ -370,9 +452,10 @@ export class Post extends Component<any, PostState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.BanUser) {
|
} else if (op == UserOperation.BanUser) {
|
||||||
let res: BanUserResponse = msg;
|
let res: BanUserResponse = msg;
|
||||||
this.state.comments.filter(c => c.creator_id == res.user.id)
|
this.state.comments
|
||||||
.forEach(c => c.banned = res.banned);
|
.filter(c => c.creator_id == res.user.id)
|
||||||
if (this.state.post.creator_id == res.user.id) {
|
.forEach(c => (c.banned = res.banned));
|
||||||
|
if (this.state.post.creator_id == res.user.id) {
|
||||||
this.state.post.banned = res.banned;
|
this.state.post.banned = res.banned;
|
||||||
}
|
}
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
@ -384,21 +467,17 @@ export class Post extends Component<any, PostState> {
|
||||||
let res: SearchResponse = msg;
|
let res: SearchResponse = msg;
|
||||||
this.state.crossPosts = res.posts.filter(p => p.id != this.state.post.id);
|
this.state.crossPosts = res.posts.filter(p => p.id != this.state.post.id);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.TransferSite) {
|
} else if (op == UserOperation.TransferSite) {
|
||||||
let res: GetSiteResponse = msg;
|
let res: GetSiteResponse = msg;
|
||||||
|
|
||||||
this.state.admins = res.admins;
|
this.state.admins = res.admins;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.TransferCommunity) {
|
} else if (op == UserOperation.TransferCommunity) {
|
||||||
let res: GetCommunityResponse = msg;
|
let res: GetCommunityResponse = msg;
|
||||||
this.state.community = res.community;
|
this.state.community = res.community;
|
||||||
this.state.moderators = res.moderators;
|
this.state.moderators = res.moderators;
|
||||||
this.state.admins = res.admins;
|
this.state.admins = res.admins;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,41 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { UserOperation, Post, Comment, Community, UserView, SortType, SearchForm, SearchResponse, SearchType } from '../interfaces';
|
import {
|
||||||
|
UserOperation,
|
||||||
|
Post,
|
||||||
|
Comment,
|
||||||
|
Community,
|
||||||
|
UserView,
|
||||||
|
SortType,
|
||||||
|
SearchForm,
|
||||||
|
SearchResponse,
|
||||||
|
SearchType,
|
||||||
|
} from '../interfaces';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { msgOp, fetchLimit, routeSearchTypeToEnum, routeSortTypeToEnum } from '../utils';
|
import {
|
||||||
|
msgOp,
|
||||||
|
fetchLimit,
|
||||||
|
routeSearchTypeToEnum,
|
||||||
|
routeSortTypeToEnum,
|
||||||
|
} from '../utils';
|
||||||
import { PostListing } from './post-listing';
|
import { PostListing } from './post-listing';
|
||||||
|
import { SortSelect } from './sort-select';
|
||||||
import { CommentNodes } from './comment-nodes';
|
import { CommentNodes } from './comment-nodes';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
interface SearchState {
|
interface SearchState {
|
||||||
q: string,
|
q: string;
|
||||||
type_: SearchType,
|
type_: SearchType;
|
||||||
sort: SortType,
|
sort: SortType;
|
||||||
page: number,
|
page: number;
|
||||||
searchResponse: SearchResponse;
|
searchResponse: SearchResponse;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Search extends Component<any, SearchState> {
|
export class Search extends Component<any, SearchState> {
|
||||||
|
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
private emptyState: SearchState = {
|
private emptyState: SearchState = {
|
||||||
q: this.getSearchQueryFromProps(this.props),
|
q: this.getSearchQueryFromProps(this.props),
|
||||||
|
@ -36,45 +51,52 @@ export class Search extends Component<any, SearchState> {
|
||||||
users: [],
|
users: [],
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
getSearchQueryFromProps(props: any): string {
|
getSearchQueryFromProps(props: any): string {
|
||||||
return (props.match.params.q) ? props.match.params.q : '';
|
return props.match.params.q ? props.match.params.q : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
getSearchTypeFromProps(props: any): SearchType {
|
getSearchTypeFromProps(props: any): SearchType {
|
||||||
return (props.match.params.type) ?
|
return props.match.params.type
|
||||||
routeSearchTypeToEnum(props.match.params.type) :
|
? routeSearchTypeToEnum(props.match.params.type)
|
||||||
SearchType.All;
|
: SearchType.All;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortTypeFromProps(props: any): SortType {
|
getSortTypeFromProps(props: any): SortType {
|
||||||
return (props.match.params.sort) ?
|
return props.match.params.sort
|
||||||
routeSortTypeToEnum(props.match.params.sort) :
|
? routeSortTypeToEnum(props.match.params.sort)
|
||||||
SortType.TopAll;
|
: SortType.TopAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPageFromProps(props: any): number {
|
getPageFromProps(props: any): number {
|
||||||
return (props.match.params.page) ? Number(props.match.params.page) : 1;
|
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.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(
|
||||||
.subscribe(
|
retryWhen(errors =>
|
||||||
(msg) => this.parseMessage(msg),
|
errors.pipe(
|
||||||
(err) => console.error(err),
|
delay(3000),
|
||||||
|
take(10)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
msg => this.parseMessage(msg),
|
||||||
|
err => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.state.q) {
|
if (this.state.q) {
|
||||||
this.search();
|
this.search();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -83,7 +105,10 @@ export class Search extends Component<any, SearchState> {
|
||||||
|
|
||||||
// Necessary for back button for some reason
|
// Necessary for back button for some reason
|
||||||
componentWillReceiveProps(nextProps: any) {
|
componentWillReceiveProps(nextProps: any) {
|
||||||
if (nextProps.history.action == 'POP' || nextProps.history.action == 'PUSH') {
|
if (
|
||||||
|
nextProps.history.action == 'POP' ||
|
||||||
|
nextProps.history.action == 'PUSH'
|
||||||
|
) {
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
this.state.q = this.getSearchQueryFromProps(nextProps);
|
this.state.q = this.getSearchQueryFromProps(nextProps);
|
||||||
this.state.type_ = this.getSearchTypeFromProps(nextProps);
|
this.state.type_ = this.getSearchTypeFromProps(nextProps);
|
||||||
|
@ -95,7 +120,9 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.title = `${i18n.t('search')} - ${WebSocketService.Instance.site.name}`;
|
document.title = `${i18n.t('search')} - ${
|
||||||
|
WebSocketService.Instance.site.name
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -103,77 +130,109 @@ export class Search extends Component<any, SearchState> {
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h5><T i18nKey="search">#</T></h5>
|
<h5>
|
||||||
|
<T i18nKey="search">#</T>
|
||||||
|
</h5>
|
||||||
{this.selects()}
|
{this.selects()}
|
||||||
{this.searchForm()}
|
{this.searchForm()}
|
||||||
{this.state.type_ == SearchType.All &&
|
{this.state.type_ == SearchType.All && this.all()}
|
||||||
this.all()
|
{this.state.type_ == SearchType.Comments && this.comments()}
|
||||||
}
|
{this.state.type_ == SearchType.Posts && this.posts()}
|
||||||
{this.state.type_ == SearchType.Comments &&
|
{this.state.type_ == SearchType.Communities && this.communities()}
|
||||||
this.comments()
|
{this.state.type_ == SearchType.Users && this.users()}
|
||||||
}
|
|
||||||
{this.state.type_ == SearchType.Posts &&
|
|
||||||
this.posts()
|
|
||||||
}
|
|
||||||
{this.state.type_ == SearchType.Communities &&
|
|
||||||
this.communities()
|
|
||||||
}
|
|
||||||
{this.state.type_ == SearchType.Users &&
|
|
||||||
this.users()
|
|
||||||
}
|
|
||||||
{this.noResults()}
|
{this.noResults()}
|
||||||
{this.paginator()}
|
{this.paginator()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
searchForm() {
|
searchForm() {
|
||||||
return (
|
return (
|
||||||
<form class="form-inline" onSubmit={linkEvent(this, this.handleSearchSubmit)}>
|
<form
|
||||||
<input type="text" class="form-control mr-2" value={this.state.q} placeholder={`${i18n.t('search')}...`} onInput={linkEvent(this, this.handleQChange)} required minLength={3} />
|
class="form-inline"
|
||||||
|
onSubmit={linkEvent(this, this.handleSearchSubmit)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control mr-2"
|
||||||
|
value={this.state.q}
|
||||||
|
placeholder={`${i18n.t('search')}...`}
|
||||||
|
onInput={linkEvent(this, this.handleQChange)}
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
/>
|
||||||
<button type="submit" class="btn btn-secondary mr-2">
|
<button type="submit" class="btn btn-secondary mr-2">
|
||||||
{this.state.loading ?
|
{this.state.loading ? (
|
||||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
|
<svg class="icon icon-spinner spin">
|
||||||
<span><T i18nKey="search">#</T></span>
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
}
|
</svg>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
<T i18nKey="search">#</T>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
selects() {
|
selects() {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="custom-select custom-select-sm w-auto">
|
<select
|
||||||
<option disabled><T i18nKey="type">#</T></option>
|
value={this.state.type_}
|
||||||
<option value={SearchType.All}><T i18nKey="all">#</T></option>
|
onChange={linkEvent(this, this.handleTypeChange)}
|
||||||
<option value={SearchType.Comments}><T i18nKey="comments">#</T></option>
|
class="custom-select custom-select-sm w-auto"
|
||||||
<option value={SearchType.Posts}><T i18nKey="posts">#</T></option>
|
>
|
||||||
<option value={SearchType.Communities}><T i18nKey="communities">#</T></option>
|
<option disabled>
|
||||||
<option value={SearchType.Users}><T i18nKey="users">#</T></option>
|
<T i18nKey="type">#</T>
|
||||||
</select>
|
</option>
|
||||||
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
|
<option value={SearchType.All}>
|
||||||
<option disabled><T i18nKey="sort_type">#</T></option>
|
<T i18nKey="all">#</T>
|
||||||
<option value={SortType.New}><T i18nKey="new">#</T></option>
|
</option>
|
||||||
<option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
|
<option value={SearchType.Comments}>
|
||||||
<option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
|
<T i18nKey="comments">#</T>
|
||||||
<option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
|
</option>
|
||||||
<option value={SortType.TopYear}><T i18nKey="year">#</T></option>
|
<option value={SearchType.Posts}>
|
||||||
<option value={SortType.TopAll}><T i18nKey="all">#</T></option>
|
<T i18nKey="posts">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={SearchType.Communities}>
|
||||||
|
<T i18nKey="communities">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={SearchType.Users}>
|
||||||
|
<T i18nKey="users">#</T>
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
<span class="ml-2">
|
||||||
|
<SortSelect
|
||||||
|
sort={this.state.sort}
|
||||||
|
onChange={this.handleSortChange}
|
||||||
|
hideHot
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
all() {
|
all() {
|
||||||
let combined: Array<{type_: string, data: Comment | Post | Community | UserView}> = [];
|
let combined: Array<{
|
||||||
let comments = this.state.searchResponse.comments.map(e => {return {type_: "comments", data: e}});
|
type_: string;
|
||||||
let posts = this.state.searchResponse.posts.map(e => {return {type_: "posts", data: e}});
|
data: Comment | Post | Community | UserView;
|
||||||
let communities = this.state.searchResponse.communities.map(e => {return {type_: "communities", data: e}});
|
}> = [];
|
||||||
let users = this.state.searchResponse.users.map(e => {return {type_: "users", data: e}});
|
let comments = this.state.searchResponse.comments.map(e => {
|
||||||
|
return { type_: 'comments', data: e };
|
||||||
|
});
|
||||||
|
let posts = this.state.searchResponse.posts.map(e => {
|
||||||
|
return { type_: 'posts', data: e };
|
||||||
|
});
|
||||||
|
let communities = this.state.searchResponse.communities.map(e => {
|
||||||
|
return { type_: 'communities', data: e };
|
||||||
|
});
|
||||||
|
let users = this.state.searchResponse.users.map(e => {
|
||||||
|
return { type_: 'users', data: e };
|
||||||
|
});
|
||||||
|
|
||||||
combined.push(...comments);
|
combined.push(...comments);
|
||||||
combined.push(...posts);
|
combined.push(...posts);
|
||||||
|
@ -184,49 +243,68 @@ export class Search extends Component<any, SearchState> {
|
||||||
if (this.state.sort == SortType.New) {
|
if (this.state.sort == SortType.New) {
|
||||||
combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
|
combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
|
||||||
} else {
|
} else {
|
||||||
combined.sort((a, b) => ((b.data as Comment | Post).score
|
combined.sort(
|
||||||
| (b.data as Community).number_of_subscribers
|
(a, b) =>
|
||||||
| (b.data as UserView).comment_score)
|
((b.data as Comment | Post).score |
|
||||||
- ((a.data as Comment | Post).score
|
(b.data as Community).number_of_subscribers |
|
||||||
| (a.data as Community).number_of_subscribers
|
(b.data as UserView).comment_score) -
|
||||||
| (a.data as UserView).comment_score));
|
((a.data as Comment | Post).score |
|
||||||
|
(a.data as Community).number_of_subscribers |
|
||||||
|
(a.data as UserView).comment_score)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{combined.map(i =>
|
{combined.map(i => (
|
||||||
<div>
|
<div>
|
||||||
{i.type_ == "posts" &&
|
{i.type_ == 'posts' && (
|
||||||
<PostListing post={i.data as Post} showCommunity viewOnly />
|
<PostListing post={i.data as Post} showCommunity viewOnly />
|
||||||
}
|
)}
|
||||||
{i.type_ == "comments" &&
|
{i.type_ == 'comments' && (
|
||||||
<CommentNodes nodes={[{comment: i.data as Comment}]} viewOnly noIndent />
|
<CommentNodes
|
||||||
}
|
nodes={[{ comment: i.data as Comment }]}
|
||||||
{i.type_ == "communities" &&
|
viewOnly
|
||||||
|
noIndent
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{i.type_ == 'communities' && (
|
||||||
<div>
|
<div>
|
||||||
<span><Link to={`/c/${(i.data as Community).name}`}>{`/c/${(i.data as Community).name}`}</Link></span>
|
<span>
|
||||||
<span>{` - ${(i.data as Community).title} - ${(i.data as Community).number_of_subscribers} subscribers`}</span>
|
<Link to={`/c/${(i.data as Community).name}`}>{`/c/${
|
||||||
|
(i.data as Community).name
|
||||||
|
}`}</Link>
|
||||||
|
</span>
|
||||||
|
<span>{` - ${(i.data as Community).title} - ${
|
||||||
|
(i.data as Community).number_of_subscribers
|
||||||
|
} subscribers`}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{i.type_ == "users" &&
|
{i.type_ == 'users' && (
|
||||||
<div>
|
<div>
|
||||||
<span><Link className="text-info" to={`/u/${(i.data as UserView).name}`}>{`/u/${(i.data as UserView).name}`}</Link></span>
|
<span>
|
||||||
<span>{` - ${(i.data as UserView).comment_score} comment karma`}</span>
|
<Link
|
||||||
|
className="text-info"
|
||||||
|
to={`/u/${(i.data as UserView).name}`}
|
||||||
|
>{`/u/${(i.data as UserView).name}`}</Link>
|
||||||
|
</span>
|
||||||
|
<span>{` - ${
|
||||||
|
(i.data as UserView).comment_score
|
||||||
|
} comment karma`}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
comments() {
|
comments() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.searchResponse.comments.map(comment =>
|
{this.state.searchResponse.comments.map(comment => (
|
||||||
<CommentNodes nodes={[{comment: comment}]} noIndent viewOnly />
|
<CommentNodes nodes={[{ comment: comment }]} noIndent viewOnly />
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -234,9 +312,9 @@ export class Search extends Component<any, SearchState> {
|
||||||
posts() {
|
posts() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.searchResponse.posts.map(post =>
|
{this.state.searchResponse.posts.map(post => (
|
||||||
<PostListing post={post} showCommunity viewOnly />
|
<PostListing post={post} showCommunity viewOnly />
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -245,12 +323,14 @@ export class Search extends Component<any, SearchState> {
|
||||||
communities() {
|
communities() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.searchResponse.communities.map(community =>
|
{this.state.searchResponse.communities.map(community => (
|
||||||
<div>
|
<div>
|
||||||
<span><Link to={`/c/${community.name}`}>{`/c/${community.name}`}</Link></span>
|
<span>
|
||||||
|
<Link to={`/c/${community.name}`}>{`/c/${community.name}`}</Link>
|
||||||
|
</span>
|
||||||
<span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span>
|
<span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -258,12 +338,17 @@ export class Search extends Component<any, SearchState> {
|
||||||
users() {
|
users() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.searchResponse.users.map(user =>
|
{this.state.searchResponse.users.map(user => (
|
||||||
<div>
|
<div>
|
||||||
<span><Link className="text-info" to={`/u/${user.name}`}>{`/u/${user.name}`}</Link></span>
|
<span>
|
||||||
|
<Link
|
||||||
|
className="text-info"
|
||||||
|
to={`/u/${user.name}`}
|
||||||
|
>{`/u/${user.name}`}</Link>
|
||||||
|
</span>
|
||||||
<span>{` - ${user.comment_score} comment karma`}</span>
|
<span>{` - ${user.comment_score} comment karma`}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -271,10 +356,20 @@ export class Search extends Component<any, SearchState> {
|
||||||
paginator() {
|
paginator() {
|
||||||
return (
|
return (
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{this.state.page > 1 &&
|
{this.state.page > 1 && (
|
||||||
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
|
<button
|
||||||
}
|
class="btn btn-sm btn-secondary mr-1"
|
||||||
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
|
>
|
||||||
|
<T i18nKey="prev">#</T>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
|
>
|
||||||
|
<T i18nKey="next">#</T>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -283,21 +378,28 @@ export class Search extends Component<any, SearchState> {
|
||||||
let res = this.state.searchResponse;
|
let res = this.state.searchResponse;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{res && res.op && res.posts.length == 0 && res.comments.length == 0 &&
|
{res &&
|
||||||
<span><T i18nKey="no_results">#</T></span>
|
res.op &&
|
||||||
}
|
res.posts.length == 0 &&
|
||||||
|
res.comments.length == 0 &&
|
||||||
|
res.communities.length == 0 &&
|
||||||
|
res.users.length == 0 && (
|
||||||
|
<span>
|
||||||
|
<T i18nKey="no_results">#</T>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPage(i: Search) {
|
nextPage(i: Search) {
|
||||||
i.state.page++;
|
i.state.page++;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.search();
|
i.search();
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPage(i: Search) {
|
prevPage(i: Search) {
|
||||||
i.state.page--;
|
i.state.page--;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
|
@ -305,7 +407,6 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
search() {
|
search() {
|
||||||
// TODO community
|
|
||||||
let form: SearchForm = {
|
let form: SearchForm = {
|
||||||
q: this.state.q,
|
q: this.state.q,
|
||||||
type_: SearchType[this.state.type_],
|
type_: SearchType[this.state.type_],
|
||||||
|
@ -319,11 +420,11 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortChange(i: Search, event: any) {
|
handleSortChange(val: SortType) {
|
||||||
i.state.sort = Number(event.target.value);
|
this.state.sort = val;
|
||||||
i.state.page = 1;
|
this.state.page = 1;
|
||||||
i.setState(i.state);
|
this.setState(this.state);
|
||||||
i.updateUrl();
|
this.updateUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTypeChange(i: Search, event: any) {
|
handleTypeChange(i: Search, event: any) {
|
||||||
|
@ -349,7 +450,9 @@ export class Search extends Component<any, SearchState> {
|
||||||
updateUrl() {
|
updateUrl() {
|
||||||
let typeStr = SearchType[this.state.type_].toLowerCase();
|
let typeStr = SearchType[this.state.type_].toLowerCase();
|
||||||
let sortStr = SortType[this.state.sort].toLowerCase();
|
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||||
this.props.history.push(`/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`);
|
this.props.history.push(
|
||||||
|
`/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseMessage(msg: any) {
|
parseMessage(msg: any) {
|
||||||
|
@ -362,10 +465,11 @@ export class Search extends Component<any, SearchState> {
|
||||||
let res: SearchResponse = msg;
|
let res: SearchResponse = msg;
|
||||||
this.state.searchResponse = res;
|
this.state.searchResponse = res;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
document.title = `${i18n.t('search')} - ${this.state.q} - ${WebSocketService.Instance.site.name}`;
|
document.title = `${i18n.t('search')} - ${this.state.q} - ${
|
||||||
window.scrollTo(0,0);
|
WebSocketService.Instance.site.name
|
||||||
|
}`;
|
||||||
|
window.scrollTo(0, 0);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { RegisterForm, LoginResponse, UserOperation } from '../interfaces';
|
import { RegisterForm, LoginResponse, UserOperation } from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
|
@ -27,8 +27,7 @@ export class Setup extends Component<any, State> {
|
||||||
},
|
},
|
||||||
doneRegisteringUser: false,
|
doneRegisteringUser: false,
|
||||||
userLoading: false,
|
userLoading: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -36,12 +35,19 @@ export class Setup extends Component<any, State> {
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
|
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(
|
||||||
.subscribe(
|
retryWhen(errors =>
|
||||||
(msg) => this.parseMessage(msg),
|
errors.pipe(
|
||||||
(err) => console.error(err),
|
delay(3000),
|
||||||
() => console.log("complete")
|
take(10)
|
||||||
);
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
msg => this.parseMessage(msg),
|
||||||
|
err => console.error(err),
|
||||||
|
() => console.log('complete')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -57,54 +63,103 @@ export class Setup extends Component<any, State> {
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 offset-lg-3 col-lg-6">
|
<div class="col-12 offset-lg-3 col-lg-6">
|
||||||
<h3><T i18nKey="lemmy_instance_setup">#</T></h3>
|
<h3>
|
||||||
{!this.state.doneRegisteringUser ? this.registerUser() : <SiteForm />}
|
<T i18nKey="lemmy_instance_setup">#</T>
|
||||||
|
</h3>
|
||||||
|
{!this.state.doneRegisteringUser ? (
|
||||||
|
this.registerUser()
|
||||||
|
) : (
|
||||||
|
<SiteForm />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerUser() {
|
registerUser() {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
|
||||||
<h5><T i18nKey="setup_admin">#</T></h5>
|
<h5>
|
||||||
|
<T i18nKey="setup_admin">#</T>
|
||||||
|
</h5>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="username">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
|
<T i18nKey="username">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="text" class="form-control" value={this.state.userForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} maxLength={20} pattern="[a-zA-Z0-9_]+" />
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
value={this.state.userForm.username}
|
||||||
|
onInput={linkEvent(this, this.handleRegisterUsernameChange)}
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
maxLength={20}
|
||||||
|
pattern="[a-zA-Z0-9_]+"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="email">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
|
<T i18nKey="email">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="email" class="form-control" placeholder={i18n.t('optional')} value={this.state.userForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} />
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
placeholder={i18n.t('optional')}
|
||||||
|
value={this.state.userForm.email}
|
||||||
|
onInput={linkEvent(this, this.handleRegisterEmailChange)}
|
||||||
|
minLength={3}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="password">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
|
<T i18nKey="password">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="password" value={this.state.userForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required />
|
<input
|
||||||
|
type="password"
|
||||||
|
value={this.state.userForm.password}
|
||||||
|
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label"><T i18nKey="verify_password">#</T></label>
|
<label class="col-sm-2 col-form-label">
|
||||||
|
<T i18nKey="verify_password">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="password" value={this.state.userForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required />
|
<input
|
||||||
|
type="password"
|
||||||
|
value={this.state.userForm.password_verify}
|
||||||
|
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<button type="submit" class="btn btn-secondary">{this.state.userLoading ?
|
<button type="submit" class="btn btn-secondary">
|
||||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('sign_up')}</button>
|
{this.state.userLoading ? (
|
||||||
|
<svg class="icon icon-spinner spin">
|
||||||
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
i18n.t('sign_up')
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleRegisterSubmit(i: Setup, event: any) {
|
handleRegisterSubmit(i: Setup, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.state.userLoading = true;
|
i.state.userLoading = true;
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Community, CommunityUser, FollowCommunityForm, CommunityForm as CommunityFormI, UserView } from '../interfaces';
|
import {
|
||||||
|
Community,
|
||||||
|
CommunityUser,
|
||||||
|
FollowCommunityForm,
|
||||||
|
CommunityForm as CommunityFormI,
|
||||||
|
UserView,
|
||||||
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { mdToHtml, getUnixTime } from '../utils';
|
import { mdToHtml, getUnixTime } from '../utils';
|
||||||
import { CommunityForm } from './community-form';
|
import { CommunityForm } from './community-form';
|
||||||
|
@ -21,13 +27,12 @@ interface SidebarState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Sidebar extends Component<SidebarProps, SidebarState> {
|
export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
|
|
||||||
private emptyState: SidebarState = {
|
private emptyState: SidebarState = {
|
||||||
showEdit: false,
|
showEdit: false,
|
||||||
showRemoveDialog: false,
|
showRemoveDialog: false,
|
||||||
removeReason: null,
|
removeReason: null,
|
||||||
removeExpires: null
|
removeExpires: null,
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -39,15 +44,17 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!this.state.showEdit
|
{!this.state.showEdit ? (
|
||||||
? this.sidebar()
|
this.sidebar()
|
||||||
: <CommunityForm
|
) : (
|
||||||
community={this.props.community}
|
<CommunityForm
|
||||||
onEdit={this.handleEditCommunity}
|
community={this.props.community}
|
||||||
onCancel={this.handleEditCancel} />
|
onEdit={this.handleEditCommunity}
|
||||||
}
|
onCancel={this.handleEditCancel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sidebar() {
|
sidebar() {
|
||||||
|
@ -58,86 +65,178 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 className="mb-0">
|
<h5 className="mb-0">
|
||||||
<span>{community.title}</span>
|
<span>{community.title}</span>
|
||||||
{community.removed &&
|
{community.removed && (
|
||||||
<small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
|
<small className="ml-2 text-muted font-italic">
|
||||||
}
|
<T i18nKey="removed">#</T>
|
||||||
{community.deleted &&
|
</small>
|
||||||
<small className="ml-2 text-muted font-italic"><T i18nKey="deleted">#</T></small>
|
)}
|
||||||
}
|
{community.deleted && (
|
||||||
|
<small className="ml-2 text-muted font-italic">
|
||||||
|
<T i18nKey="deleted">#</T>
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
</h5>
|
</h5>
|
||||||
<Link className="text-muted" to={`/c/${community.name}`}>/c/{community.name}</Link>
|
<Link className="text-muted" to={`/c/${community.name}`}>
|
||||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
/c/{community.name}
|
||||||
{this.canMod &&
|
</Link>
|
||||||
|
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||||
|
{this.canMod && (
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleEditClick)}
|
||||||
|
>
|
||||||
|
<T i18nKey="edit">#</T>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
{this.amCreator &&
|
{this.amCreator && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
|
<span
|
||||||
{!community.deleted ? i18n.t('delete') : i18n.t('restore')}
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||||
|
>
|
||||||
|
{!community.deleted
|
||||||
|
? i18n.t('delete')
|
||||||
|
: i18n.t('restore')}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{this.canAdmin &&
|
{this.canAdmin && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!this.props.community.removed ?
|
{!this.props.community.removed ? (
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
|
<span
|
||||||
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
|
class="pointer"
|
||||||
}
|
onClick={linkEvent(this, this.handleModRemoveShow)}
|
||||||
|
>
|
||||||
|
<T i18nKey="remove">#</T>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleModRemoveSubmit)}
|
||||||
|
>
|
||||||
|
<T i18nKey="restore">#</T>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
)}
|
||||||
}
|
|
||||||
</ul>
|
</ul>
|
||||||
{this.state.showRemoveDialog &&
|
{this.state.showRemoveDialog && (
|
||||||
<form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-form-label"><T i18nKey="reason">#</T></label>
|
<label class="col-form-label">
|
||||||
<input type="text" class="form-control mr-2" placeholder={i18n.t('optional')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
|
<T i18nKey="reason">#</T>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control mr-2"
|
||||||
|
placeholder={i18n.t('optional')}
|
||||||
|
value={this.state.removeReason}
|
||||||
|
onInput={linkEvent(this, this.handleModRemoveReasonChange)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* TODO hold off on expires for now */}
|
{/* TODO hold off on expires for now */}
|
||||||
{/* <div class="form-group row"> */}
|
{/* <div class="form-group row"> */}
|
||||||
{/* <label class="col-form-label">Expires</label> */}
|
{/* <label class="col-form-label">Expires</label> */}
|
||||||
{/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
|
{/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<button type="submit" class="btn btn-secondary"><T i18nKey="remove_community">#</T></button>
|
<button type="submit" class="btn btn-secondary">
|
||||||
</div>
|
<T i18nKey="remove_community">#</T>
|
||||||
</form>
|
</button>
|
||||||
}
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
<ul class="my-1 list-inline">
|
<ul class="my-1 list-inline">
|
||||||
<li className="list-inline-item"><Link className="badge badge-secondary" to="/communities">{community.category_name}</Link></li>
|
<li className="list-inline-item">
|
||||||
<li className="list-inline-item badge badge-secondary"><T i18nKey="number_of_subscribers" interpolation={{count: community.number_of_subscribers}}>#</T></li>
|
<Link className="badge badge-secondary" to="/communities">
|
||||||
<li className="list-inline-item badge badge-secondary"><T i18nKey="number_of_posts" interpolation={{count: community.number_of_posts}}>#</T></li>
|
{community.category_name}
|
||||||
<li className="list-inline-item badge badge-secondary"><T i18nKey="number_of_comments" interpolation={{count: community.number_of_comments}}>#</T></li>
|
</Link>
|
||||||
<li className="list-inline-item"><Link className="badge badge-secondary" to={`/modlog/community/${this.props.community.id}`}><T i18nKey="modlog">#</T></Link></li>
|
</li>
|
||||||
|
<li className="list-inline-item badge badge-secondary">
|
||||||
|
<T
|
||||||
|
i18nKey="number_of_subscribers"
|
||||||
|
interpolation={{ count: community.number_of_subscribers }}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
|
</li>
|
||||||
|
<li className="list-inline-item badge badge-secondary">
|
||||||
|
<T
|
||||||
|
i18nKey="number_of_posts"
|
||||||
|
interpolation={{ count: community.number_of_posts }}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
|
</li>
|
||||||
|
<li className="list-inline-item badge badge-secondary">
|
||||||
|
<T
|
||||||
|
i18nKey="number_of_comments"
|
||||||
|
interpolation={{ count: community.number_of_comments }}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
|
</li>
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<Link
|
||||||
|
className="badge badge-secondary"
|
||||||
|
to={`/modlog/community/${this.props.community.id}`}
|
||||||
|
>
|
||||||
|
<T i18nKey="modlog">#</T>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="list-inline small">
|
<ul class="list-inline small">
|
||||||
<li class="list-inline-item">{i18n.t('mods')}: </li>
|
<li class="list-inline-item">{i18n.t('mods')}: </li>
|
||||||
{this.props.moderators.map(mod =>
|
{this.props.moderators.map(mod => (
|
||||||
<li class="list-inline-item"><Link class="text-info" to={`/u/${mod.user_name}`}>{mod.user_name}</Link></li>
|
<li class="list-inline-item">
|
||||||
)}
|
<Link class="text-info" to={`/u/${mod.user_name}`}>
|
||||||
|
{mod.user_name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<Link class={`btn btn-sm btn-secondary btn-block mb-3 ${(community.deleted || community.removed) && 'no-click'}`}
|
<Link
|
||||||
to={`/create_post?community=${community.name}`}><T i18nKey="create_a_post">#</T></Link>
|
class={`btn btn-sm btn-secondary btn-block mb-3 ${(community.deleted ||
|
||||||
<div>
|
community.removed) &&
|
||||||
{community.subscribed
|
'no-click'}`}
|
||||||
? <button class="btn btn-sm btn-secondary btn-block" onClick={linkEvent(community.id, this.handleUnsubscribe)}><T i18nKey="unsubscribe">#</T></button>
|
to={`/create_post?community=${community.name}`}
|
||||||
: <button class="btn btn-sm btn-secondary btn-block" onClick={linkEvent(community.id, this.handleSubscribe)}><T i18nKey="subscribe">#</T></button>
|
>
|
||||||
}
|
<T i18nKey="create_a_post">#</T>
|
||||||
</div>
|
</Link>
|
||||||
</div>
|
<div>
|
||||||
</div>
|
{community.subscribed ? (
|
||||||
{community.description &&
|
<button
|
||||||
<div class="card border-secondary">
|
class="btn btn-sm btn-secondary btn-block"
|
||||||
<div class="card-body">
|
onClick={linkEvent(community.id, this.handleUnsubscribe)}
|
||||||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(community.description)} />
|
>
|
||||||
|
<T i18nKey="unsubscribe">#</T>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-secondary btn-block"
|
||||||
|
onClick={linkEvent(community.id, this.handleSubscribe)}
|
||||||
|
>
|
||||||
|
<T i18nKey="subscribe">#</T>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
{community.description && (
|
||||||
|
<div class="card border-secondary">
|
||||||
|
<div class="card-body">
|
||||||
|
<div
|
||||||
|
className="md-div"
|
||||||
|
dangerouslySetInnerHTML={mdToHtml(community.description)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,7 +272,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
handleUnsubscribe(communityId: number) {
|
handleUnsubscribe(communityId: number) {
|
||||||
let form: FollowCommunityForm = {
|
let form: FollowCommunityForm = {
|
||||||
community_id: communityId,
|
community_id: communityId,
|
||||||
follow: false
|
follow: false,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.followCommunity(form);
|
WebSocketService.Instance.followCommunity(form);
|
||||||
}
|
}
|
||||||
|
@ -181,7 +280,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
handleSubscribe(communityId: number) {
|
handleSubscribe(communityId: number) {
|
||||||
let form: FollowCommunityForm = {
|
let form: FollowCommunityForm = {
|
||||||
community_id: communityId,
|
community_id: communityId,
|
||||||
follow: true
|
follow: true,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.followCommunity(form);
|
WebSocketService.Instance.followCommunity(form);
|
||||||
}
|
}
|
||||||
|
@ -191,11 +290,19 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get canMod(): boolean {
|
get canMod(): boolean {
|
||||||
return UserService.Instance.user && this.props.moderators.map(m => m.user_id).includes(UserService.Instance.user.id);
|
return (
|
||||||
|
UserService.Instance.user &&
|
||||||
|
this.props.moderators
|
||||||
|
.map(m => m.user_id)
|
||||||
|
.includes(UserService.Instance.user.id)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get canAdmin(): boolean {
|
get canAdmin(): boolean {
|
||||||
return UserService.Instance.user && this.props.admins.map(a => a.id).includes(UserService.Instance.user.id);
|
return (
|
||||||
|
UserService.Instance.user &&
|
||||||
|
this.props.admins.map(a => a.id).includes(UserService.Instance.user.id)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModRemoveShow(i: Sidebar) {
|
handleModRemoveShow(i: Sidebar) {
|
||||||
|
|
|
@ -17,12 +17,12 @@ interface SiteFormState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
private emptyState: SiteFormState ={
|
private emptyState: SiteFormState = {
|
||||||
siteForm: {
|
siteForm: {
|
||||||
name: null
|
name: null,
|
||||||
},
|
},
|
||||||
loading: false
|
loading: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -31,7 +31,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
this.state.siteForm = {
|
this.state.siteForm = {
|
||||||
name: this.props.site.name,
|
name: this.props.site.name,
|
||||||
description: this.props.site.description,
|
description: this.props.site.description,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,26 +42,63 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
|
||||||
<h5>{`${this.props.site ? capitalizeFirstLetter(i18n.t('edit')) : capitalizeFirstLetter(i18n.t('name'))} ${i18n.t('your_site')}`}</h5>
|
<h5>{`${
|
||||||
|
this.props.site
|
||||||
|
? capitalizeFirstLetter(i18n.t('edit'))
|
||||||
|
: capitalizeFirstLetter(i18n.t('name'))
|
||||||
|
} ${i18n.t('your_site')}`}</h5>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-12 col-form-label"><T i18nKey="name">#</T></label>
|
<label class="col-12 col-form-label">
|
||||||
|
<T i18nKey="name">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<input type="text" class="form-control" value={this.state.siteForm.name} onInput={linkEvent(this, this.handleSiteNameChange)} required minLength={3} maxLength={20} />
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
value={this.state.siteForm.name}
|
||||||
|
onInput={linkEvent(this, this.handleSiteNameChange)}
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
maxLength={20}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-12 col-form-label"><T i18nKey="sidebar">#</T></label>
|
<label class="col-12 col-form-label">
|
||||||
|
<T i18nKey="sidebar">#</T>
|
||||||
|
</label>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<textarea value={this.state.siteForm.description} onInput={linkEvent(this, this.handleSiteDescriptionChange)} class="form-control" rows={3} maxLength={10000} />
|
<textarea
|
||||||
|
value={this.state.siteForm.description}
|
||||||
|
onInput={linkEvent(this, this.handleSiteDescriptionChange)}
|
||||||
|
class="form-control"
|
||||||
|
rows={3}
|
||||||
|
maxLength={10000}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<button type="submit" class="btn btn-secondary mr-2">
|
<button type="submit" class="btn btn-secondary mr-2">
|
||||||
{this.state.loading ?
|
{this.state.loading ? (
|
||||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
|
<svg class="icon icon-spinner spin">
|
||||||
this.props.site ? capitalizeFirstLetter(i18n.t('save')) : capitalizeFirstLetter(i18n.t('create'))}</button>
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
{this.props.site && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}><T i18nKey="cancel">#</T></button>}
|
</svg>
|
||||||
|
) : this.props.site ? (
|
||||||
|
capitalizeFirstLetter(i18n.t('save'))
|
||||||
|
) : (
|
||||||
|
capitalizeFirstLetter(i18n.t('create'))
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{this.props.site && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.handleCancel)}
|
||||||
|
>
|
||||||
|
<T i18nKey="cancel">#</T>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { SortType } from '../interfaces';
|
||||||
|
|
||||||
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
|
interface SortSelectProps {
|
||||||
|
sort: SortType;
|
||||||
|
onChange?(val: SortType): any;
|
||||||
|
hideHot?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SortSelectState {
|
||||||
|
sort: SortType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SortSelect extends Component<SortSelectProps, SortSelectState> {
|
||||||
|
private emptyState: SortSelectState = {
|
||||||
|
sort: this.props.sort,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: any, context: any) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = this.emptyState;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
value={this.state.sort}
|
||||||
|
onChange={linkEvent(this, this.handleSortChange)}
|
||||||
|
class="custom-select custom-select-sm w-auto"
|
||||||
|
>
|
||||||
|
<option disabled>
|
||||||
|
<T i18nKey="sort_type">#</T>
|
||||||
|
</option>
|
||||||
|
{!this.props.hideHot && (
|
||||||
|
<option value={SortType.Hot}>
|
||||||
|
<T i18nKey="hot">#</T>
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
<option value={SortType.New}>
|
||||||
|
<T i18nKey="new">#</T>
|
||||||
|
</option>
|
||||||
|
<option disabled>─────</option>
|
||||||
|
<option value={SortType.TopDay}>
|
||||||
|
<T i18nKey="top_day">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={SortType.TopWeek}>
|
||||||
|
<T i18nKey="week">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={SortType.TopMonth}>
|
||||||
|
<T i18nKey="month">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={SortType.TopYear}>
|
||||||
|
<T i18nKey="year">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={SortType.TopAll}>
|
||||||
|
<T i18nKey="all">#</T>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSortChange(i: SortSelect, event: any) {
|
||||||
|
i.state.sort = Number(event.target.value);
|
||||||
|
i.setState(i.state);
|
||||||
|
i.props.onChange(i.state.sort);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,24 +3,21 @@ import { WebSocketService } from '../services';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
let general =
|
let general = ['riccardo', 'NotTooHighToHack'];
|
||||||
[
|
|
||||||
"Nathan J. Goode",
|
|
||||||
];
|
|
||||||
// let highlighted = [];
|
// let highlighted = [];
|
||||||
// let silver = [];
|
// let silver = [];
|
||||||
// let gold = [];
|
// let gold = [];
|
||||||
// let latinum = [];
|
// let latinum = [];
|
||||||
|
|
||||||
export class Sponsors extends Component<any, any> {
|
export class Sponsors extends Component<any, any> {
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.title = `${i18n.t('sponsors')} - ${WebSocketService.Instance.site.name}`;
|
document.title = `${i18n.t('sponsors')} - ${
|
||||||
|
WebSocketService.Instance.site.name
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -32,62 +29,85 @@ export class Sponsors extends Component<any, any> {
|
||||||
<hr />
|
<hr />
|
||||||
{this.bitcoin()}
|
{this.bitcoin()}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
topMessage() {
|
topMessage() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h5><T i18nKey="sponsors_of_lemmy">#</T></h5>
|
<h5>
|
||||||
|
<T i18nKey="sponsors_of_lemmy">#</T>
|
||||||
|
</h5>
|
||||||
<p>
|
<p>
|
||||||
<T i18nKey="sponsor_message">#<a href="https://github.com/dessalines/lemmy">#</a></T>
|
<T i18nKey="sponsor_message">
|
||||||
|
#<a href="https://github.com/dessalines/lemmy">#</a>
|
||||||
|
</T>
|
||||||
</p>
|
</p>
|
||||||
<a class="btn btn-secondary" href="https://www.patreon.com/dessalines"><T i18nKey="support_on_patreon">#</T></a>
|
<a class="btn btn-secondary" href="https://www.patreon.com/dessalines">
|
||||||
|
<T i18nKey="support_on_patreon">#</T>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
sponsors() {
|
sponsors() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h5><T i18nKey="sponsors">#</T></h5>
|
<h5>
|
||||||
<p><T i18nKey="general_sponsors">#</T></p>
|
<T i18nKey="sponsors">#</T>
|
||||||
|
</h5>
|
||||||
|
<p>
|
||||||
|
<T i18nKey="general_sponsors">#</T>
|
||||||
|
</p>
|
||||||
<div class="row card-columns">
|
<div class="row card-columns">
|
||||||
{general.map(s =>
|
{general.map(s => (
|
||||||
<div class="card col-12 col-md-2">
|
<div class="card col-12 col-md-2">
|
||||||
<div>{s}</div>
|
<div>{s}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bitcoin() {
|
bitcoin() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h5><T i18nKey="crypto">#</T></h5>
|
<h5>
|
||||||
<div class="table-responsive">
|
<T i18nKey="crypto">#</T>
|
||||||
<table class="table table-hover text-center">
|
</h5>
|
||||||
<tbody>
|
<div class="table-responsive">
|
||||||
<tr>
|
<table class="table table-hover text-center">
|
||||||
<td><T i18nKey="bitcoin">#</T></td>
|
<tbody>
|
||||||
<td><code>1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK</code></td>
|
<tr>
|
||||||
</tr>
|
<td>
|
||||||
<tr>
|
<T i18nKey="bitcoin">#</T>
|
||||||
<td><T i18nKey="ethereum">#</T></td>
|
</td>
|
||||||
<td><code>0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01</code></td>
|
<td>
|
||||||
</tr>
|
<code>1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK</code>
|
||||||
<tr>
|
</td>
|
||||||
<td><T i18nKey="monero">#</T></td>
|
</tr>
|
||||||
<td>
|
<tr>
|
||||||
<code>41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV</code>
|
<td>
|
||||||
</td>
|
<T i18nKey="ethereum">#</T>
|
||||||
</tr>
|
</td>
|
||||||
</tbody>
|
<td>
|
||||||
</table>
|
<code>0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<T i18nKey="monero">#</T>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>
|
||||||
|
41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV
|
||||||
|
</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
import { Component } from 'inferno';
|
import { Component } from 'inferno';
|
||||||
|
|
||||||
export class Symbols extends Component<any, any> {
|
export class Symbols extends Component<any, any> {
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
style="position: absolute; width: 0; height: 0; overflow: hidden;"
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<symbol id="icon-arrow-down" viewBox="0 0 26 28">
|
<symbol id="icon-arrow-down" viewBox="0 0 26 28">
|
||||||
<title>arrow-down</title>
|
<title>arrow-down</title>
|
||||||
|
@ -17,63 +22,69 @@ export class Symbols extends Component<any, any> {
|
||||||
<symbol id="icon-arrow-up" viewBox="0 0 26 28">
|
<symbol id="icon-arrow-up" viewBox="0 0 26 28">
|
||||||
<title>arrow-up</title>
|
<title>arrow-up</title>
|
||||||
<path d="M25.172 15.172c0 0.531-0.219 1.031-0.578 1.406l-1.172 1.172c-0.375 0.375-0.891 0.594-1.422 0.594s-1.047-0.219-1.406-0.594l-4.594-4.578v11c0 1.125-0.938 1.828-2 1.828h-2c-1.062 0-2-0.703-2-1.828v-11l-4.594 4.578c-0.359 0.375-0.875 0.594-1.406 0.594s-1.047-0.219-1.406-0.594l-1.172-1.172c-0.375-0.375-0.594-0.875-0.594-1.406s0.219-1.047 0.594-1.422l10.172-10.172c0.359-0.375 0.875-0.578 1.406-0.578s1.047 0.203 1.422 0.578l10.172 10.172c0.359 0.375 0.578 0.891 0.578 1.422z"></path>
|
<path d="M25.172 15.172c0 0.531-0.219 1.031-0.578 1.406l-1.172 1.172c-0.375 0.375-0.891 0.594-1.422 0.594s-1.047-0.219-1.406-0.594l-4.594-4.578v11c0 1.125-0.938 1.828-2 1.828h-2c-1.062 0-2-0.703-2-1.828v-11l-4.594 4.578c-0.359 0.375-0.875 0.594-1.406 0.594s-1.047-0.219-1.406-0.594l-1.172-1.172c-0.375-0.375-0.594-0.875-0.594-1.406s0.219-1.047 0.594-1.422l10.172-10.172c0.359-0.375 0.875-0.578 1.406-0.578s1.047 0.203 1.422 0.578l10.172 10.172c0.359 0.375 0.578 0.891 0.578 1.422z"></path>
|
||||||
</symbol>
|
</symbol>
|
||||||
<symbol id="icon-mail" viewBox="0 0 32 32">
|
<symbol id="icon-mail" viewBox="0 0 32 32">
|
||||||
<title>mail</title>
|
<title>mail</title>
|
||||||
<path d="M28 5h-24c-2.209 0-4 1.792-4 4v13c0 2.209 1.791 4 4 4h24c2.209 0 4-1.791 4-4v-13c0-2.208-1.791-4-4-4zM2 10.25l6.999 5.25-6.999 5.25v-10.5zM30 22c0 1.104-0.898 2-2 2h-24c-1.103 0-2-0.896-2-2l7.832-5.875 4.368 3.277c0.533 0.398 1.166 0.6 1.8 0.6 0.633 0 1.266-0.201 1.799-0.6l4.369-3.277 7.832 5.875zM30 20.75l-7-5.25 7-5.25v10.5zM17.199 18.602c-0.349 0.262-0.763 0.4-1.199 0.4s-0.851-0.139-1.2-0.4l-12.8-9.602c0-1.103 0.897-2 2-2h24c1.102 0 2 0.897 2 2l-12.801 9.602z"></path>
|
<path d="M28 5h-24c-2.209 0-4 1.792-4 4v13c0 2.209 1.791 4 4 4h24c2.209 0 4-1.791 4-4v-13c0-2.208-1.791-4-4-4zM2 10.25l6.999 5.25-6.999 5.25v-10.5zM30 22c0 1.104-0.898 2-2 2h-24c-1.103 0-2-0.896-2-2l7.832-5.875 4.368 3.277c0.533 0.398 1.166 0.6 1.8 0.6 0.633 0 1.266-0.201 1.799-0.6l4.369-3.277 7.832 5.875zM30 20.75l-7-5.25 7-5.25v10.5zM17.199 18.602c-0.349 0.262-0.763 0.4-1.199 0.4s-0.851-0.139-1.2-0.4l-12.8-9.602c0-1.103 0.897-2 2-2h24c1.102 0 2 0.897 2 2l-12.801 9.602z"></path>
|
||||||
</symbol>
|
</symbol>
|
||||||
<symbol id="icon-mouse" version="1.1" x="0px" y="0px"
|
<symbol
|
||||||
viewBox="0 0 1024 1024">
|
id="icon-mouse"
|
||||||
<g
|
version="1.1"
|
||||||
id="layer1"
|
x="0px"
|
||||||
transform="translate(0,-26.066658)"
|
y="0px"
|
||||||
style="display:inline">
|
viewBox="0 0 1024 1024"
|
||||||
<path
|
>
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
<g
|
||||||
d="m 167.03908,270.78735 c -0.94784,-0.002 -1.8939,0.004 -2.83789,0.0215 -4.31538,0.0778 -8.58934,0.3593 -12.8125,0.8457 -33.78522,3.89116 -64.215716,21.86394 -82.871086,53.27344 -18.27982,30.77718 -22.77749,64.66635 -13.46094,96.06837 9.31655,31.40203 31.88488,59.93174 65.296886,82.5332 0.20163,0.13618 0.40678,0.26709 0.61523,0.39258 28.65434,17.27768 57.18167,28.93179 87.74218,34.95508 -0.74566,12.61339 -0.72532,25.5717 0.082,38.84375 2.43989,40.10943 16.60718,77.03742 38.0957,109.67187 l -77.00781,31.4375 c -8.30605,3.25932 -12.34178,12.68234 -8.96967,20.94324 3.37211,8.2609 12.84919,12.16798 21.06342,8.68371 l 84.69727,-34.57617 c 15.70675,18.72702 33.75346,35.68305 53.12109,50.57032 0.74013,0.56891 1.4904,1.12236 2.23437,1.68554 l -49.61132,65.69141 c -5.45446,7.0474 -4.10058,17.19288 3.01098,22.5634 7.11156,5.37052 17.24028,3.89649 22.52612,-3.27824 l 50.38672,-66.71876 c 27.68572,17.53469 57.07524,31.20388 86.07227,40.25196 14.88153,27.28008 43.96965,44.64648 77.58789,44.64648 33.93762,0 63.04252,-18.68693 77.80082,-45.4375 28.7072,-9.21295 57.7527,-22.93196 85.1484,-40.40234 l 51.0977,67.66016 c 5.2858,7.17473 15.4145,8.64876 22.5261,3.27824 7.1115,-5.37052 8.4654,-15.516 3.011,-22.5634 l -50.3614,-66.68555 c 0.334,-0.25394 0.6727,-0.50077 1.0059,-0.75586 19.1376,-14.64919 37.0259,-31.28581 52.7031,-49.63476 l 82.5625,33.70507 c 8.2143,3.48427 17.6913,-0.42281 21.0634,-8.68371 3.3722,-8.2609 -0.6636,-17.68392 -8.9696,-20.94324 l -74.5391,-30.42773 c 22.1722,-32.82971 37.0383,-70.03397 40.1426,-110.46094 1.0253,-13.35251 1.2292,-26.42535 0.6387,-39.17578 30.3557,-6.05408 58.7164,-17.66833 87.2011,-34.84375 0.2085,-0.12549 0.4136,-0.2564 0.6153,-0.39258 33.412,-22.60147 55.9803,-51.13117 65.2968,-82.5332 9.3166,-31.40202 4.8189,-65.29118 -13.4609,-96.06837 -18.6553,-31.40951 -49.0859,-49.38228 -82.8711,-53.27344 -4.2231,-0.4864 -8.4971,-0.76791 -12.8125,-0.8457 -30.2077,-0.54448 -62.4407,8.82427 -93.4316,26.71484 -22.7976,13.16063 -43.3521,33.31423 -59.4375,55.30469 -44.9968,-25.75094 -103.5444,-40.25065 -175.4785,-41.43945 -6.4522,-0.10663 -13.0125,-0.10696 -19.67974,0.002 -80.18875,1.30929 -144.38284,16.5086 -192.87109,43.9922 -0.11914,-0.19111 -0.24287,-0.37932 -0.37109,-0.56446 -16.29,-22.764 -37.41085,-43.73706 -60.89649,-57.29493 -30.02247,-17.33149 -61.21051,-26.66489 -90.59375,-26.73633 z"
|
id="layer1"
|
||||||
id="path817-3"
|
transform="translate(0,-26.066658)"
|
||||||
/>
|
style="display:inline"
|
||||||
<path
|
>
|
||||||
id="path1087"
|
<path
|
||||||
style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||||
d="m 716.85595,362.96478 c 15.29075,-21.36763 35.36198,-41.10921 56.50979,-53.31749 66.66377,-38.48393 137.02617,-33.22172 170.08018,22.43043 33.09493,55.72093 14.98656,117.48866 -47.64399,159.85496 -31.95554,19.26819 -62.93318,30.92309 -97.22892,35.54473 M 307.14407,362.96478 C 291.85332,341.59715 271.78209,321.85557 250.63429,309.64729 183.97051,271.16336 113.60811,276.42557 80.554051,332.07772 47.459131,387.79865 65.56752,449.56638 128.19809,491.93268 c 31.95554,19.26819 62.93319,30.92309 97.22893,35.54473"
|
d="m 167.03908,270.78735 c -0.94784,-0.002 -1.8939,0.004 -2.83789,0.0215 -4.31538,0.0778 -8.58934,0.3593 -12.8125,0.8457 -33.78522,3.89116 -64.215716,21.86394 -82.871086,53.27344 -18.27982,30.77718 -22.77749,64.66635 -13.46094,96.06837 9.31655,31.40203 31.88488,59.93174 65.296886,82.5332 0.20163,0.13618 0.40678,0.26709 0.61523,0.39258 28.65434,17.27768 57.18167,28.93179 87.74218,34.95508 -0.74566,12.61339 -0.72532,25.5717 0.082,38.84375 2.43989,40.10943 16.60718,77.03742 38.0957,109.67187 l -77.00781,31.4375 c -8.30605,3.25932 -12.34178,12.68234 -8.96967,20.94324 3.37211,8.2609 12.84919,12.16798 21.06342,8.68371 l 84.69727,-34.57617 c 15.70675,18.72702 33.75346,35.68305 53.12109,50.57032 0.74013,0.56891 1.4904,1.12236 2.23437,1.68554 l -49.61132,65.69141 c -5.45446,7.0474 -4.10058,17.19288 3.01098,22.5634 7.11156,5.37052 17.24028,3.89649 22.52612,-3.27824 l 50.38672,-66.71876 c 27.68572,17.53469 57.07524,31.20388 86.07227,40.25196 14.88153,27.28008 43.96965,44.64648 77.58789,44.64648 33.93762,0 63.04252,-18.68693 77.80082,-45.4375 28.7072,-9.21295 57.7527,-22.93196 85.1484,-40.40234 l 51.0977,67.66016 c 5.2858,7.17473 15.4145,8.64876 22.5261,3.27824 7.1115,-5.37052 8.4654,-15.516 3.011,-22.5634 l -50.3614,-66.68555 c 0.334,-0.25394 0.6727,-0.50077 1.0059,-0.75586 19.1376,-14.64919 37.0259,-31.28581 52.7031,-49.63476 l 82.5625,33.70507 c 8.2143,3.48427 17.6913,-0.42281 21.0634,-8.68371 3.3722,-8.2609 -0.6636,-17.68392 -8.9696,-20.94324 l -74.5391,-30.42773 c 22.1722,-32.82971 37.0383,-70.03397 40.1426,-110.46094 1.0253,-13.35251 1.2292,-26.42535 0.6387,-39.17578 30.3557,-6.05408 58.7164,-17.66833 87.2011,-34.84375 0.2085,-0.12549 0.4136,-0.2564 0.6153,-0.39258 33.412,-22.60147 55.9803,-51.13117 65.2968,-82.5332 9.3166,-31.40202 4.8189,-65.29118 -13.4609,-96.06837 -18.6553,-31.40951 -49.0859,-49.38228 -82.8711,-53.27344 -4.2231,-0.4864 -8.4971,-0.76791 -12.8125,-0.8457 -30.2077,-0.54448 -62.4407,8.82427 -93.4316,26.71484 -22.7976,13.16063 -43.3521,33.31423 -59.4375,55.30469 -44.9968,-25.75094 -103.5444,-40.25065 -175.4785,-41.43945 -6.4522,-0.10663 -13.0125,-0.10696 -19.67974,0.002 -80.18875,1.30929 -144.38284,16.5086 -192.87109,43.9922 -0.11914,-0.19111 -0.24287,-0.37932 -0.37109,-0.56446 -16.29,-22.764 -37.41085,-43.73706 -60.89649,-57.29493 -30.02247,-17.33149 -61.21051,-26.66489 -90.59375,-26.73633 z"
|
||||||
/>
|
id="path817-3"
|
||||||
<path
|
/>
|
||||||
style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
<path
|
||||||
d="M 801.23205,576.8699 C 812.73478,427.06971 720.58431,321.98291 511.99999,325.38859 303.41568,328.79426 213.71393,428.0311 222.76794,576.8699 c 8.64289,142.08048 176.80223,246.40388 288.12038,246.40388 111.31815,0 279.45076,-104.5447 290.34373,-246.40388 z"
|
id="path1087"
|
||||||
id="path969"
|
style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
/>
|
d="m 716.85595,362.96478 c 15.29075,-21.36763 35.36198,-41.10921 56.50979,-53.31749 66.66377,-38.48393 137.02617,-33.22172 170.08018,22.43043 33.09493,55.72093 14.98656,117.48866 -47.64399,159.85496 -31.95554,19.26819 -62.93318,30.92309 -97.22892,35.54473 M 307.14407,362.96478 C 291.85332,341.59715 271.78209,321.85557 250.63429,309.64729 183.97051,271.16336 113.60811,276.42557 80.554051,332.07772 47.459131,387.79865 65.56752,449.56638 128.19809,491.93268 c 31.95554,19.26819 62.93319,30.92309 97.22893,35.54473"
|
||||||
<path
|
/>
|
||||||
id="path1084"
|
<path
|
||||||
style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
d="m 610.4991,644.28932 c 0,23.11198 18.70595,41.84795 41.78091,41.84795 23.07495,0 41.7809,-18.73597 41.7809,-41.84795 0,-23.112 -18.70594,-41.84796 -41.7809,-41.84796 -23.07496,0 -41.78091,18.73596 -41.78091,41.84796 z m -280.56002,0 c 0,23.32492 18.87829,42.23352 42.16586,42.23352 23.28755,0 42.16585,-18.9086 42.16585,-42.23352 0,-23.32494 -18.87829,-42.23353 -42.16585,-42.23353 -23.28757,0 -42.16586,18.90859 -42.16586,42.23353 z"
|
d="M 801.23205,576.8699 C 812.73478,427.06971 720.58431,321.98291 511.99999,325.38859 303.41568,328.79426 213.71393,428.0311 222.76794,576.8699 c 8.64289,142.08048 176.80223,246.40388 288.12038,246.40388 111.31815,0 279.45076,-104.5447 290.34373,-246.40388 z"
|
||||||
/>
|
id="path969"
|
||||||
<path
|
/>
|
||||||
id="path1008"
|
<path
|
||||||
style="display:inline;opacity:1;fill:none;stroke:#000000;stroke-width:32;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
id="path1084"
|
||||||
d="m 339.72919,769.2467 -54.54422,72.22481 m 399.08582,-72.22481 54.54423,72.22481 M 263.68341,697.82002 175.92752,733.64353 m 579.85765,-35.82351 87.7559,35.82351"
|
style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
/>
|
d="m 610.4991,644.28932 c 0,23.11198 18.70595,41.84795 41.78091,41.84795 23.07495,0 41.7809,-18.73597 41.7809,-41.84795 0,-23.112 -18.70594,-41.84796 -41.7809,-41.84796 -23.07496,0 -41.78091,18.73596 -41.78091,41.84796 z m -280.56002,0 c 0,23.32492 18.87829,42.23352 42.16586,42.23352 23.28755,0 42.16585,-18.9086 42.16585,-42.23352 0,-23.32494 -18.87829,-42.23353 -42.16585,-42.23353 -23.28757,0 -42.16586,18.90859 -42.16586,42.23353 z"
|
||||||
<path
|
/>
|
||||||
style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
<path
|
||||||
d="m 512.00082,713.08977 c -45.86417,0 -75.13006,31.84485 -74.14159,71.10084 1.07048,42.51275 32.46865,71.10323 74.14159,71.10323 41.67296,0 74.05118,-32.99608 74.14161,-71.10323 0.0932,-39.26839 -28.27742,-71.10084 -74.14161,-71.10084 z"
|
id="path1008"
|
||||||
id="path1115"
|
style="display:inline;opacity:1;fill:none;stroke:#000000;stroke-width:32;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
/>
|
d="m 339.72919,769.2467 -54.54422,72.22481 m 399.08582,-72.22481 54.54423,72.22481 M 263.68341,697.82002 175.92752,733.64353 m 579.85765,-35.82351 87.7559,35.82351"
|
||||||
</g>
|
/>
|
||||||
</symbol>
|
<path
|
||||||
<symbol id="icon-search" viewBox="0 0 32 32">
|
style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
<title>search</title>
|
d="m 512.00082,713.08977 c -45.86417,0 -75.13006,31.84485 -74.14159,71.10084 1.07048,42.51275 32.46865,71.10323 74.14159,71.10323 41.67296,0 74.05118,-32.99608 74.14161,-71.10323 0.0932,-39.26839 -28.27742,-71.10084 -74.14161,-71.10084 z"
|
||||||
<path d="M31.008 27.231l-7.58-6.447c-0.784-0.705-1.622-1.029-2.299-0.998 1.789-2.096 2.87-4.815 2.87-7.787 0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12c2.972 0 5.691-1.081 7.787-2.87-0.031 0.677 0.293 1.515 0.998 2.299l6.447 7.58c1.104 1.226 2.907 1.33 4.007 0.23s0.997-2.903-0.23-4.007zM12 20c-4.418 0-8-3.582-8-8s3.582-8 8-8 8 3.582 8 8-3.582 8-8 8z"></path>
|
id="path1115"
|
||||||
</symbol>
|
/>
|
||||||
<symbol id="icon-github" viewBox="0 0 32 32">
|
</g>
|
||||||
<title>github</title>
|
</symbol>
|
||||||
<path d="M16 0.395c-8.836 0-16 7.163-16 16 0 7.069 4.585 13.067 10.942 15.182 0.8 0.148 1.094-0.347 1.094-0.77 0-0.381-0.015-1.642-0.022-2.979-4.452 0.968-5.391-1.888-5.391-1.888-0.728-1.849-1.776-2.341-1.776-2.341-1.452-0.993 0.11-0.973 0.11-0.973 1.606 0.113 2.452 1.649 2.452 1.649 1.427 2.446 3.743 1.739 4.656 1.33 0.143-1.034 0.558-1.74 1.016-2.14-3.554-0.404-7.29-1.777-7.29-7.907 0-1.747 0.625-3.174 1.649-4.295-0.166-0.403-0.714-2.030 0.155-4.234 0 0 1.344-0.43 4.401 1.64 1.276-0.355 2.645-0.532 4.005-0.539 1.359 0.006 2.729 0.184 4.008 0.539 3.054-2.070 4.395-1.64 4.395-1.64 0.871 2.204 0.323 3.831 0.157 4.234 1.026 1.12 1.647 2.548 1.647 4.295 0 6.145-3.743 7.498-7.306 7.895 0.574 0.497 1.085 1.47 1.085 2.963 0 2.141-0.019 3.864-0.019 4.391 0 0.426 0.288 0.925 1.099 0.768 6.354-2.118 10.933-8.113 10.933-15.18 0-8.837-7.164-16-16-16z"></path>
|
<symbol id="icon-search" viewBox="0 0 32 32">
|
||||||
</symbol>
|
<title>search</title>
|
||||||
<symbol id="icon-spinner" viewBox="0 0 32 32">
|
<path d="M31.008 27.231l-7.58-6.447c-0.784-0.705-1.622-1.029-2.299-0.998 1.789-2.096 2.87-4.815 2.87-7.787 0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12c2.972 0 5.691-1.081 7.787-2.87-0.031 0.677 0.293 1.515 0.998 2.299l6.447 7.58c1.104 1.226 2.907 1.33 4.007 0.23s0.997-2.903-0.23-4.007zM12 20c-4.418 0-8-3.582-8-8s3.582-8 8-8 8 3.582 8 8-3.582 8-8 8z"></path>
|
||||||
<title>spinner</title>
|
</symbol>
|
||||||
<path d="M16 32c-4.274 0-8.292-1.664-11.314-4.686s-4.686-7.040-4.686-11.314c0-3.026 0.849-5.973 2.456-8.522 1.563-2.478 3.771-4.48 6.386-5.791l1.344 2.682c-2.126 1.065-3.922 2.693-5.192 4.708-1.305 2.069-1.994 4.462-1.994 6.922 0 7.168 5.832 13 13 13s13-5.832 13-13c0-2.459-0.69-4.853-1.994-6.922-1.271-2.015-3.066-3.643-5.192-4.708l1.344-2.682c2.615 1.31 4.824 3.313 6.386 5.791 1.607 2.549 2.456 5.495 2.456 8.522 0 4.274-1.664 8.292-4.686 11.314s-7.040 4.686-11.314 4.686z"></path>
|
<symbol id="icon-github" viewBox="0 0 32 32">
|
||||||
</symbol>
|
<title>github</title>
|
||||||
</defs>
|
<path d="M16 0.395c-8.836 0-16 7.163-16 16 0 7.069 4.585 13.067 10.942 15.182 0.8 0.148 1.094-0.347 1.094-0.77 0-0.381-0.015-1.642-0.022-2.979-4.452 0.968-5.391-1.888-5.391-1.888-0.728-1.849-1.776-2.341-1.776-2.341-1.452-0.993 0.11-0.973 0.11-0.973 1.606 0.113 2.452 1.649 2.452 1.649 1.427 2.446 3.743 1.739 4.656 1.33 0.143-1.034 0.558-1.74 1.016-2.14-3.554-0.404-7.29-1.777-7.29-7.907 0-1.747 0.625-3.174 1.649-4.295-0.166-0.403-0.714-2.030 0.155-4.234 0 0 1.344-0.43 4.401 1.64 1.276-0.355 2.645-0.532 4.005-0.539 1.359 0.006 2.729 0.184 4.008 0.539 3.054-2.070 4.395-1.64 4.395-1.64 0.871 2.204 0.323 3.831 0.157 4.234 1.026 1.12 1.647 2.548 1.647 4.295 0 6.145-3.743 7.498-7.306 7.895 0.574 0.497 1.085 1.47 1.085 2.963 0 2.141-0.019 3.864-0.019 4.391 0 0.426 0.288 0.925 1.099 0.768 6.354-2.118 10.933-8.113 10.933-15.18 0-8.837-7.164-16-16-16z"></path>
|
||||||
</svg>
|
</symbol>
|
||||||
|
<symbol id="icon-spinner" viewBox="0 0 32 32">
|
||||||
|
<title>spinner</title>
|
||||||
|
<path d="M16 32c-4.274 0-8.292-1.664-11.314-4.686s-4.686-7.040-4.686-11.314c0-3.026 0.849-5.973 2.456-8.522 1.563-2.478 3.771-4.48 6.386-5.791l1.344 2.682c-2.126 1.065-3.922 2.693-5.192 4.708-1.305 2.069-1.994 4.462-1.994 6.922 0 7.168 5.832 13 13 13s13-5.832 13-13c0-2.459-0.69-4.853-1.994-6.922-1.271-2.015-3.066-3.643-5.192-4.708l1.344-2.682c2.615 1.31 4.824 3.313 6.386 5.791 1.607 2.549 2.456 5.495 2.456 8.522 0 4.274-1.664 8.292-4.686 11.314s-7.040 4.686-11.314 4.686z"></path>
|
||||||
|
</symbol>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,46 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse, UserSettingsForm, LoginResponse, BanUserResponse, AddAdminResponse, DeleteAccountForm } from '../interfaces';
|
import {
|
||||||
|
UserOperation,
|
||||||
|
Post,
|
||||||
|
Comment,
|
||||||
|
CommunityUser,
|
||||||
|
GetUserDetailsForm,
|
||||||
|
SortType,
|
||||||
|
ListingType,
|
||||||
|
UserDetailsResponse,
|
||||||
|
UserView,
|
||||||
|
CommentResponse,
|
||||||
|
UserSettingsForm,
|
||||||
|
LoginResponse,
|
||||||
|
BanUserResponse,
|
||||||
|
AddAdminResponse,
|
||||||
|
DeleteAccountForm,
|
||||||
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter, themes, setTheme } from '../utils';
|
import {
|
||||||
|
msgOp,
|
||||||
|
fetchLimit,
|
||||||
|
routeSortTypeToEnum,
|
||||||
|
capitalizeFirstLetter,
|
||||||
|
themes,
|
||||||
|
setTheme,
|
||||||
|
} from '../utils';
|
||||||
import { PostListing } from './post-listing';
|
import { PostListing } from './post-listing';
|
||||||
|
import { SortSelect } from './sort-select';
|
||||||
|
import { ListingTypeSelect } from './listing-type-select';
|
||||||
import { CommentNodes } from './comment-nodes';
|
import { CommentNodes } from './comment-nodes';
|
||||||
import { MomentTime } from './moment-time';
|
import { MomentTime } from './moment-time';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
enum View {
|
enum View {
|
||||||
Overview, Comments, Posts, Saved
|
Overview,
|
||||||
|
Comments,
|
||||||
|
Posts,
|
||||||
|
Saved,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserState {
|
interface UserState {
|
||||||
|
@ -37,7 +65,6 @@ interface UserState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class User extends Component<any, UserState> {
|
export class User extends Component<any, UserState> {
|
||||||
|
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
private emptyState: UserState = {
|
private emptyState: UserState = {
|
||||||
user: {
|
user: {
|
||||||
|
@ -65,6 +92,8 @@ export class User extends Component<any, UserState> {
|
||||||
userSettingsForm: {
|
userSettingsForm: {
|
||||||
show_nsfw: null,
|
show_nsfw: null,
|
||||||
theme: null,
|
theme: null,
|
||||||
|
default_sort_type: null,
|
||||||
|
default_listing_type: null,
|
||||||
auth: null,
|
auth: null,
|
||||||
},
|
},
|
||||||
userSettingsLoading: null,
|
userSettingsLoading: null,
|
||||||
|
@ -72,46 +101,63 @@ export class User extends Component<any, UserState> {
|
||||||
deleteAccountShowConfirm: false,
|
deleteAccountShowConfirm: false,
|
||||||
deleteAccountForm: {
|
deleteAccountForm: {
|
||||||
password: null,
|
password: null,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
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.handleUserSettingsSortTypeChange = this.handleUserSettingsSortTypeChange.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
|
this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
|
|
||||||
this.state.user_id = Number(this.props.match.params.id);
|
this.state.user_id = Number(this.props.match.params.id);
|
||||||
this.state.username = this.props.match.params.username;
|
this.state.username = this.props.match.params.username;
|
||||||
|
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(
|
||||||
.subscribe(
|
retryWhen(errors =>
|
||||||
(msg) => this.parseMessage(msg),
|
errors.pipe(
|
||||||
(err) => console.error(err),
|
delay(3000),
|
||||||
|
take(10)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
msg => this.parseMessage(msg),
|
||||||
|
err => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
this.refetch();
|
this.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
get isCurrentUser() {
|
get isCurrentUser() {
|
||||||
return UserService.Instance.user && UserService.Instance.user.id == this.state.user.id;
|
return (
|
||||||
|
UserService.Instance.user &&
|
||||||
|
UserService.Instance.user.id == this.state.user.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewFromProps(props: any): View {
|
getViewFromProps(props: any): View {
|
||||||
return (props.match.params.view) ?
|
return props.match.params.view
|
||||||
View[capitalizeFirstLetter(props.match.params.view)] :
|
? View[capitalizeFirstLetter(props.match.params.view)]
|
||||||
View.Overview;
|
: View.Overview;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortTypeFromProps(props: any): SortType {
|
getSortTypeFromProps(props: any): SortType {
|
||||||
return (props.match.params.sort) ?
|
return props.match.params.sort
|
||||||
routeSortTypeToEnum(props.match.params.sort) :
|
? routeSortTypeToEnum(props.match.params.sort)
|
||||||
SortType.New;
|
: SortType.New;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPageFromProps(props: any): number {
|
getPageFromProps(props: any): number {
|
||||||
return (props.match.params.page) ? Number(props.match.params.page) : 1;
|
return props.match.params.page ? Number(props.match.params.page) : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -120,11 +166,14 @@ export class User extends Component<any, UserState> {
|
||||||
|
|
||||||
// Necessary for back button for some reason
|
// Necessary for back button for some reason
|
||||||
componentWillReceiveProps(nextProps: any) {
|
componentWillReceiveProps(nextProps: any) {
|
||||||
if (nextProps.history.action == 'POP') {
|
if (
|
||||||
this.state = this.emptyState;
|
nextProps.history.action == 'POP' ||
|
||||||
|
nextProps.history.action == 'PUSH'
|
||||||
|
) {
|
||||||
this.state.view = this.getViewFromProps(nextProps);
|
this.state.view = this.getViewFromProps(nextProps);
|
||||||
this.state.sort = this.getSortTypeFromProps(nextProps);
|
this.state.sort = this.getSortTypeFromProps(nextProps);
|
||||||
this.state.page = this.getPageFromProps(nextProps);
|
this.state.page = this.getPageFromProps(nextProps);
|
||||||
|
this.setState(this.state);
|
||||||
this.refetch();
|
this.refetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,68 +181,78 @@ export class User extends Component<any, UserState> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{this.state.loading ?
|
{this.state.loading ? (
|
||||||
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
<h5>
|
||||||
<div class="row">
|
<svg class="icon icon-spinner spin">
|
||||||
<div class="col-12 col-md-8">
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
<h5>/u/{this.state.user.name}</h5>
|
</svg>
|
||||||
{this.selects()}
|
</h5>
|
||||||
{this.state.view == View.Overview &&
|
) : (
|
||||||
this.overview()
|
<div class="row">
|
||||||
}
|
<div class="col-12 col-md-8">
|
||||||
{this.state.view == View.Comments &&
|
<h5>/u/{this.state.user.name}</h5>
|
||||||
this.comments()
|
{this.selects()}
|
||||||
}
|
{this.state.view == View.Overview && this.overview()}
|
||||||
{this.state.view == View.Posts &&
|
{this.state.view == View.Comments && this.comments()}
|
||||||
this.posts()
|
{this.state.view == View.Posts && this.posts()}
|
||||||
}
|
{this.state.view == View.Saved && this.overview()}
|
||||||
{this.state.view == View.Saved &&
|
{this.paginator()}
|
||||||
this.overview()
|
</div>
|
||||||
}
|
<div class="col-12 col-md-4">
|
||||||
{this.paginator()}
|
{this.userInfo()}
|
||||||
|
{this.isCurrentUser && this.userSettings()}
|
||||||
|
{this.moderates()}
|
||||||
|
{this.follows()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4">
|
)}
|
||||||
{this.userInfo()}
|
|
||||||
{this.isCurrentUser &&
|
|
||||||
this.userSettings()
|
|
||||||
}
|
|
||||||
{this.moderates()}
|
|
||||||
{this.follows()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
selects() {
|
selects() {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<select value={this.state.view} onChange={linkEvent(this, this.handleViewChange)} class="custom-select custom-select-sm w-auto">
|
<select
|
||||||
<option disabled><T i18nKey="view">#</T></option>
|
value={this.state.view}
|
||||||
<option value={View.Overview}><T i18nKey="overview">#</T></option>
|
onChange={linkEvent(this, this.handleViewChange)}
|
||||||
<option value={View.Comments}><T i18nKey="comments">#</T></option>
|
class="custom-select custom-select-sm w-auto"
|
||||||
<option value={View.Posts}><T i18nKey="posts">#</T></option>
|
>
|
||||||
<option value={View.Saved}><T i18nKey="saved">#</T></option>
|
<option disabled>
|
||||||
</select>
|
<T i18nKey="view">#</T>
|
||||||
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
|
</option>
|
||||||
<option disabled><T i18nKey="sort_type">#</T></option>
|
<option value={View.Overview}>
|
||||||
<option value={SortType.New}><T i18nKey="new">#</T></option>
|
<T i18nKey="overview">#</T>
|
||||||
<option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
|
</option>
|
||||||
<option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
|
<option value={View.Comments}>
|
||||||
<option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
|
<T i18nKey="comments">#</T>
|
||||||
<option value={SortType.TopYear}><T i18nKey="year">#</T></option>
|
</option>
|
||||||
<option value={SortType.TopAll}><T i18nKey="all">#</T></option>
|
<option value={View.Posts}>
|
||||||
|
<T i18nKey="posts">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={View.Saved}>
|
||||||
|
<T i18nKey="saved">#</T>
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
<span class="ml-2">
|
||||||
|
<SortSelect
|
||||||
|
sort={this.state.sort}
|
||||||
|
onChange={this.handleSortChange}
|
||||||
|
hideHot
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
overview() {
|
overview() {
|
||||||
let combined: Array<{type_: string, data: Comment | Post}> = [];
|
let combined: Array<{ type_: string; data: Comment | Post }> = [];
|
||||||
let comments = this.state.comments.map(e => {return {type_: "comments", data: e}});
|
let comments = this.state.comments.map(e => {
|
||||||
let posts = this.state.posts.map(e => {return {type_: "posts", data: e}});
|
return { type_: 'comments', data: e };
|
||||||
|
});
|
||||||
|
let posts = this.state.posts.map(e => {
|
||||||
|
return { type_: 'posts', data: e };
|
||||||
|
});
|
||||||
|
|
||||||
combined.push(...comments);
|
combined.push(...comments);
|
||||||
combined.push(...posts);
|
combined.push(...posts);
|
||||||
|
@ -207,35 +266,38 @@ export class User extends Component<any, UserState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{combined.map(i =>
|
{combined.map(i => (
|
||||||
<div>
|
<div>
|
||||||
{i.type_ == "posts"
|
{i.type_ == 'posts' ? (
|
||||||
? <PostListing
|
<PostListing
|
||||||
post={i.data as Post}
|
post={i.data as Post}
|
||||||
admins={this.state.admins}
|
|
||||||
showCommunity
|
|
||||||
viewOnly />
|
|
||||||
:
|
|
||||||
<CommentNodes
|
|
||||||
nodes={[{comment: i.data as Comment}]}
|
|
||||||
admins={this.state.admins}
|
admins={this.state.admins}
|
||||||
noIndent />
|
showCommunity
|
||||||
}
|
viewOnly
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CommentNodes
|
||||||
|
nodes={[{ comment: i.data as Comment }]}
|
||||||
|
admins={this.state.admins}
|
||||||
|
noIndent
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
comments() {
|
comments() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.comments.map(comment =>
|
{this.state.comments.map(comment => (
|
||||||
<CommentNodes nodes={[{comment: comment}]}
|
<CommentNodes
|
||||||
|
nodes={[{ comment: comment }]}
|
||||||
admins={this.state.admins}
|
admins={this.state.admins}
|
||||||
noIndent />
|
noIndent
|
||||||
)}
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -243,13 +305,14 @@ export class User extends Component<any, UserState> {
|
||||||
posts() {
|
posts() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.posts.map(post =>
|
{this.state.posts.map(post => (
|
||||||
<PostListing
|
<PostListing
|
||||||
post={post}
|
post={post}
|
||||||
admins={this.state.admins}
|
admins={this.state.admins}
|
||||||
showCommunity
|
showCommunity
|
||||||
viewOnly />
|
viewOnly
|
||||||
)}
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -263,124 +326,279 @@ export class User extends Component<any, UserState> {
|
||||||
<h5>
|
<h5>
|
||||||
<ul class="list-inline mb-0">
|
<ul class="list-inline mb-0">
|
||||||
<li className="list-inline-item">{user.name}</li>
|
<li className="list-inline-item">{user.name}</li>
|
||||||
{user.banned &&
|
{user.banned && (
|
||||||
<li className="list-inline-item badge badge-danger"><T i18nKey="banned">#</T></li>
|
<li className="list-inline-item badge badge-danger">
|
||||||
}
|
<T i18nKey="banned">#</T>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</h5>
|
</h5>
|
||||||
<div>{i18n.t('joined')} <MomentTime data={user} /></div>
|
<div>
|
||||||
|
{i18n.t('joined')} <MomentTime data={user} />
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered table-sm mt-2 mb-0">
|
<table class="table table-bordered table-sm mt-2 mb-0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><T i18nKey="number_of_points" interpolation={{count: user.post_score}}>#</T></td>
|
<td>
|
||||||
<td><T i18nKey="number_of_posts" interpolation={{count: user.number_of_posts}}>#</T></td>
|
<T
|
||||||
|
i18nKey="number_of_points"
|
||||||
|
interpolation={{ count: user.post_score }}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<T
|
||||||
|
i18nKey="number_of_posts"
|
||||||
|
interpolation={{ count: user.number_of_posts }}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><T i18nKey="number_of_points" interpolation={{count: user.comment_score}}>#</T></td>
|
<td>
|
||||||
<td><T i18nKey="number_of_comments" interpolation={{count: user.number_of_comments}}>#</T></td>
|
<T
|
||||||
|
i18nKey="number_of_points"
|
||||||
|
interpolation={{ count: user.comment_score }}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<T
|
||||||
|
i18nKey="number_of_comments"
|
||||||
|
interpolation={{ count: user.number_of_comments }}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</T>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
userSettings() {
|
userSettings() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div class="card border-secondary mb-3">
|
<div class="card border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5><T i18nKey="settings">#</T></h5>
|
<h5>
|
||||||
|
<T i18nKey="settings">#</T>
|
||||||
|
</h5>
|
||||||
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label><T i18nKey="theme">#</T></label>
|
<label>
|
||||||
<select value={this.state.userSettingsForm.theme} onChange={linkEvent(this, this.handleUserSettingsThemeChange)} class="ml-2 custom-select custom-select-sm w-auto">
|
<T i18nKey="theme">#</T>
|
||||||
<option disabled><T i18nKey="theme">#</T></option>
|
</label>
|
||||||
{themes.map(theme =>
|
<select
|
||||||
<option value={theme}>{theme}</option>
|
value={this.state.userSettingsForm.theme}
|
||||||
|
onChange={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleUserSettingsThemeChange
|
||||||
)}
|
)}
|
||||||
|
class="ml-2 custom-select custom-select-sm w-auto"
|
||||||
|
>
|
||||||
|
<option disabled>
|
||||||
|
<T i18nKey="theme">#</T>
|
||||||
|
</option>
|
||||||
|
{themes.map(theme => (
|
||||||
|
<option value={theme}>{theme}</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<form className="form-group">
|
||||||
|
<div class="col-12">
|
||||||
|
<label>
|
||||||
|
<T i18nKey="sort_type" class="mr-2">
|
||||||
|
#
|
||||||
|
</T>
|
||||||
|
</label>
|
||||||
|
<ListingTypeSelect
|
||||||
|
type_={this.state.userSettingsForm.default_listing_type}
|
||||||
|
onChange={this.handleUserSettingsListingTypeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<form className="form-group">
|
||||||
|
<div class="col-12">
|
||||||
|
<label>
|
||||||
|
<T i18nKey="type" class="mr-2">
|
||||||
|
#
|
||||||
|
</T>
|
||||||
|
</label>
|
||||||
|
<SortSelect
|
||||||
|
sort={this.state.userSettingsForm.default_sort_type}
|
||||||
|
onChange={this.handleUserSettingsSortTypeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={this.state.userSettingsForm.show_nsfw}
|
||||||
|
onChange={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleUserSettingsShowNsfwChange
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<label class="form-check-label">
|
||||||
|
<T i18nKey="show_nsfw">#</T>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="form-check">
|
<button
|
||||||
<input class="form-check-input" type="checkbox" checked={this.state.userSettingsForm.show_nsfw} onChange={linkEvent(this, this.handleUserSettingsShowNsfwChange)}/>
|
type="submit"
|
||||||
<label class="form-check-label"><T i18nKey="show_nsfw">#</T></label>
|
class="btn btn-block btn-secondary mr-4"
|
||||||
</div>
|
>
|
||||||
|
{this.state.userSettingsLoading ? (
|
||||||
|
<svg class="icon icon-spinner spin">
|
||||||
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
capitalizeFirstLetter(i18n.t('save'))
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row mb-0">
|
<hr />
|
||||||
|
<div class="form-group mb-0">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<button type="submit" class="btn btn-secondary mr-4">{this.state.userSettingsLoading ?
|
<button
|
||||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : capitalizeFirstLetter(i18n.t('save'))}</button>
|
class="btn btn-block btn-danger"
|
||||||
<button class="btn btn-danger" onClick={linkEvent(this, this.handleDeleteAccountShowConfirmToggle)}><T i18nKey="delete_account">#</T></button>
|
onClick={linkEvent(
|
||||||
{this.state.deleteAccountShowConfirm &&
|
this,
|
||||||
|
this.handleDeleteAccountShowConfirmToggle
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="delete_account">#</T>
|
||||||
|
</button>
|
||||||
|
{this.state.deleteAccountShowConfirm && (
|
||||||
<>
|
<>
|
||||||
<div class="my-2 alert alert-danger" role="alert"><T i18nKey="delete_account_confirm">#</T></div>
|
<div class="my-2 alert alert-danger" role="alert">
|
||||||
<input type="password" value={this.state.deleteAccountForm.password} onInput={linkEvent(this, this.handleDeleteAccountPasswordChange)} class="form-control my-2" />
|
<T i18nKey="delete_account_confirm">#</T>
|
||||||
<button class="btn btn-danger mr-4" disabled={!this.state.deleteAccountForm.password} onClick={linkEvent(this, this.handleDeleteAccount)}>{this.state.deleteAccountLoading ?
|
</div>
|
||||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : capitalizeFirstLetter(i18n.t('delete'))}</button>
|
<input
|
||||||
<button class="btn btn-secondary" onClick={linkEvent(this, this.handleDeleteAccountShowConfirmToggle)}><T i18nKey="cancel">#</T></button>
|
type="password"
|
||||||
|
value={this.state.deleteAccountForm.password}
|
||||||
|
onInput={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleDeleteAccountPasswordChange
|
||||||
|
)}
|
||||||
|
class="form-control my-2"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-danger mr-4"
|
||||||
|
disabled={!this.state.deleteAccountForm.password}
|
||||||
|
onClick={linkEvent(this, this.handleDeleteAccount)}
|
||||||
|
>
|
||||||
|
{this.state.deleteAccountLoading ? (
|
||||||
|
<svg class="icon icon-spinner spin">
|
||||||
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
capitalizeFirstLetter(i18n.t('delete'))
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleDeleteAccountShowConfirmToggle
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="cancel">#</T>
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
moderates() {
|
moderates() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.moderates.length > 0 &&
|
{this.state.moderates.length > 0 && (
|
||||||
<div class="card border-secondary mb-3">
|
<div class="card border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5><T i18nKey="moderates">#</T></h5>
|
<h5>
|
||||||
<ul class="list-unstyled mb-0">
|
<T i18nKey="moderates">#</T>
|
||||||
{this.state.moderates.map(community =>
|
</h5>
|
||||||
<li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
|
<ul class="list-unstyled mb-0">
|
||||||
)}
|
{this.state.moderates.map(community => (
|
||||||
|
<li>
|
||||||
|
<Link to={`/c/${community.community_name}`}>
|
||||||
|
{community.community_name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
follows() {
|
follows() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.follows.length > 0 &&
|
{this.state.follows.length > 0 && (
|
||||||
<div class="card border-secondary mb-3">
|
<div class="card border-secondary mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5><T i18nKey="subscribed">#</T></h5>
|
<h5>
|
||||||
<ul class="list-unstyled mb-0">
|
<T i18nKey="subscribed">#</T>
|
||||||
{this.state.follows.map(community =>
|
</h5>
|
||||||
<li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
|
<ul class="list-unstyled mb-0">
|
||||||
)}
|
{this.state.follows.map(community => (
|
||||||
|
<li>
|
||||||
|
<Link to={`/c/${community.community_name}`}>
|
||||||
|
{community.community_name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
paginator() {
|
paginator() {
|
||||||
return (
|
return (
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
{this.state.page > 1 &&
|
{this.state.page > 1 && (
|
||||||
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
|
<button
|
||||||
}
|
class="btn btn-sm btn-secondary mr-1"
|
||||||
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
|
onClick={linkEvent(this, this.prevPage)}
|
||||||
|
>
|
||||||
|
<T i18nKey="prev">#</T>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-secondary"
|
||||||
|
onClick={linkEvent(this, this.nextPage)}
|
||||||
|
>
|
||||||
|
<T i18nKey="next">#</T>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -388,17 +606,19 @@ export class User extends Component<any, UserState> {
|
||||||
updateUrl() {
|
updateUrl() {
|
||||||
let viewStr = View[this.state.view].toLowerCase();
|
let viewStr = View[this.state.view].toLowerCase();
|
||||||
let sortStr = SortType[this.state.sort].toLowerCase();
|
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||||
this.props.history.push(`/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`);
|
this.props.history.push(
|
||||||
|
`/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPage(i: User) {
|
nextPage(i: User) {
|
||||||
i.state.page++;
|
i.state.page++;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.refetch();
|
i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPage(i: User) {
|
prevPage(i: User) {
|
||||||
i.state.page--;
|
i.state.page--;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
|
@ -417,12 +637,12 @@ export class User extends Component<any, UserState> {
|
||||||
WebSocketService.Instance.getUserDetails(form);
|
WebSocketService.Instance.getUserDetails(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortChange(i: User, event: any) {
|
handleSortChange(val: SortType) {
|
||||||
i.state.sort = Number(event.target.value);
|
this.state.sort = val;
|
||||||
i.state.page = 1;
|
this.state.page = 1;
|
||||||
i.setState(i.state);
|
this.setState(this.state);
|
||||||
i.updateUrl();
|
this.updateUrl();
|
||||||
i.refetch();
|
this.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleViewChange(i: User, event: any) {
|
handleViewChange(i: User, event: any) {
|
||||||
|
@ -444,6 +664,16 @@ export class User extends Component<any, UserState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleUserSettingsSortTypeChange(val: SortType) {
|
||||||
|
this.state.userSettingsForm.default_sort_type = val;
|
||||||
|
this.setState(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUserSettingsListingTypeChange(val: ListingType) {
|
||||||
|
this.state.userSettingsForm.default_listing_type = val;
|
||||||
|
this.setState(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
handleUserSettingsSubmit(i: User, event: any) {
|
handleUserSettingsSubmit(i: User, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.state.userSettingsLoading = true;
|
i.state.userSettingsLoading = true;
|
||||||
|
@ -489,11 +719,18 @@ export class User extends Component<any, UserState> {
|
||||||
this.state.admins = res.admins;
|
this.state.admins = res.admins;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
if (this.isCurrentUser) {
|
if (this.isCurrentUser) {
|
||||||
this.state.userSettingsForm.show_nsfw = UserService.Instance.user.show_nsfw;
|
this.state.userSettingsForm.show_nsfw =
|
||||||
this.state.userSettingsForm.theme = UserService.Instance.user.theme ? UserService.Instance.user.theme : 'darkly';
|
UserService.Instance.user.show_nsfw;
|
||||||
|
this.state.userSettingsForm.theme = UserService.Instance.user.theme
|
||||||
|
? UserService.Instance.user.theme
|
||||||
|
: 'darkly';
|
||||||
|
this.state.userSettingsForm.default_sort_type =
|
||||||
|
UserService.Instance.user.default_sort_type;
|
||||||
|
this.state.userSettingsForm.default_listing_type =
|
||||||
|
UserService.Instance.user.default_listing_type;
|
||||||
}
|
}
|
||||||
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
|
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.EditComment) {
|
} else if (op == UserOperation.EditComment) {
|
||||||
let res: CommentResponse = msg;
|
let res: CommentResponse = msg;
|
||||||
|
@ -520,36 +757,38 @@ export class User extends Component<any, UserState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.CreateCommentLike) {
|
} else if (op == UserOperation.CreateCommentLike) {
|
||||||
let res: CommentResponse = msg;
|
let res: CommentResponse = msg;
|
||||||
let found: Comment = this.state.comments.find(c => c.id === res.comment.id);
|
let found: Comment = this.state.comments.find(
|
||||||
|
c => c.id === res.comment.id
|
||||||
|
);
|
||||||
found.score = res.comment.score;
|
found.score = res.comment.score;
|
||||||
found.upvotes = res.comment.upvotes;
|
found.upvotes = res.comment.upvotes;
|
||||||
found.downvotes = res.comment.downvotes;
|
found.downvotes = res.comment.downvotes;
|
||||||
if (res.comment.my_vote !== null)
|
if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
|
||||||
found.my_vote = res.comment.my_vote;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.BanUser) {
|
} else if (op == UserOperation.BanUser) {
|
||||||
let res: BanUserResponse = msg;
|
let res: BanUserResponse = msg;
|
||||||
this.state.comments.filter(c => c.creator_id == res.user.id)
|
this.state.comments
|
||||||
.forEach(c => c.banned = res.banned);
|
.filter(c => c.creator_id == res.user.id)
|
||||||
this.state.posts.filter(c => c.creator_id == res.user.id)
|
.forEach(c => (c.banned = res.banned));
|
||||||
.forEach(c => c.banned = res.banned);
|
this.state.posts
|
||||||
|
.filter(c => c.creator_id == res.user.id)
|
||||||
|
.forEach(c => (c.banned = res.banned));
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.AddAdmin) {
|
} else if (op == UserOperation.AddAdmin) {
|
||||||
let res: AddAdminResponse = msg;
|
let res: AddAdminResponse = msg;
|
||||||
this.state.admins = res.admins;
|
this.state.admins = res.admins;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.SaveUserSettings) {
|
} else if (op == UserOperation.SaveUserSettings) {
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
this.state.userSettingsLoading = false;
|
this.state.userSettingsLoading = false;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
let res: LoginResponse = msg;
|
let res: LoginResponse = msg;
|
||||||
UserService.Instance.login(res);
|
UserService.Instance.login(res);
|
||||||
} else if (op == UserOperation.DeleteAccount) {
|
} else if (op == UserOperation.DeleteAccount) {
|
||||||
this.state.deleteAccountLoading = false;
|
this.state.deleteAccountLoading = false;
|
||||||
this.state.deleteAccountShowConfirm = false;
|
this.state.deleteAccountShowConfirm = false;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.context.router.history.push('/');
|
this.context.router.history.push('/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
let host = `${window.location.hostname}`;
|
let host = `${window.location.hostname}`;
|
||||||
let port = `${window.location.port == "4444" ? '8536' : window.location.port}`;
|
let port = `${window.location.port == '4444' ? '8536' : window.location.port}`;
|
||||||
let endpoint = `${host}:${port}`;
|
let endpoint = `${host}:${port}`;
|
||||||
export let wsUri = `${(window.location.protocol=='https:') ? 'wss://' : 'ws://'}${endpoint}/api/v1/ws`;
|
export let wsUri = `${
|
||||||
|
window.location.protocol == 'https:' ? 'wss://' : 'ws://'
|
||||||
|
}${endpoint}/api/v1/ws`;
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { nl } from './translations/nl';
|
||||||
import { it } from './translations/it';
|
import { it } from './translations/it';
|
||||||
|
|
||||||
// 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
|
||||||
// TODO don't forget to add moment locales for new languages.
|
|
||||||
const resources = {
|
const resources = {
|
||||||
en,
|
en,
|
||||||
eo,
|
eo,
|
||||||
|
@ -24,25 +23,23 @@ const resources = {
|
||||||
ru,
|
ru,
|
||||||
nl,
|
nl,
|
||||||
it,
|
it,
|
||||||
}
|
};
|
||||||
|
|
||||||
function format(value: any, format: any, lng: any) {
|
function format(value: any, format: any, lng: any) {
|
||||||
if (format === 'uppercase') return value.toUpperCase();
|
if (format === 'uppercase') return value.toUpperCase();
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
i18n
|
i18n.init({
|
||||||
.init({
|
debug: false,
|
||||||
debug: true,
|
|
||||||
// load: 'languageOnly',
|
// load: 'languageOnly',
|
||||||
|
|
||||||
// initImmediate: false,
|
// initImmediate: false,
|
||||||
lng: getLanguage(),
|
lng: getLanguage(),
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
resources,
|
resources,
|
||||||
interpolation: {
|
interpolation: {
|
||||||
format: format
|
format: format,
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ import { WebSocketService, UserService } from './services';
|
||||||
const container = document.getElementById('app');
|
const container = document.getElementById('app');
|
||||||
|
|
||||||
class Index extends Component<any, any> {
|
class Index extends Component<any, any> {
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
WebSocketService.Instance;
|
WebSocketService.Instance;
|
||||||
|
@ -38,7 +37,10 @@ class Index extends Component<any, any> {
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div class="mt-4 p-0">
|
<div class="mt-4 p-0">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`/home/type/:type/sort/:sort/page/:page`} component={Main} />
|
<Route
|
||||||
|
path={`/home/type/:type/sort/:sort/page/:page`}
|
||||||
|
component={Main}
|
||||||
|
/>
|
||||||
<Route exact path={`/`} component={Main} />
|
<Route exact path={`/`} component={Main} />
|
||||||
<Route path={`/login`} component={Login} />
|
<Route path={`/login`} component={Login} />
|
||||||
<Route path={`/create_post`} component={CreatePost} />
|
<Route path={`/create_post`} component={CreatePost} />
|
||||||
|
@ -47,17 +49,29 @@ class Index extends Component<any, any> {
|
||||||
<Route path={`/communities`} component={Communities} />
|
<Route path={`/communities`} component={Communities} />
|
||||||
<Route path={`/post/:id/comment/:comment_id`} component={Post} />
|
<Route path={`/post/:id/comment/:comment_id`} component={Post} />
|
||||||
<Route path={`/post/:id`} component={Post} />
|
<Route path={`/post/:id`} component={Post} />
|
||||||
<Route path={`/c/:name/sort/:sort/page/:page`} component={Community} />
|
<Route
|
||||||
|
path={`/c/:name/sort/:sort/page/:page`}
|
||||||
|
component={Community}
|
||||||
|
/>
|
||||||
<Route path={`/community/:id`} component={Community} />
|
<Route path={`/community/:id`} component={Community} />
|
||||||
<Route path={`/c/:name`} component={Community} />
|
<Route path={`/c/:name`} component={Community} />
|
||||||
<Route path={`/u/:username/view/:view/sort/:sort/page/:page`} component={User} />
|
<Route
|
||||||
|
path={`/u/:username/view/:view/sort/:sort/page/:page`}
|
||||||
|
component={User}
|
||||||
|
/>
|
||||||
<Route path={`/user/:id`} component={User} />
|
<Route path={`/user/:id`} component={User} />
|
||||||
<Route path={`/u/:username`} component={User} />
|
<Route path={`/u/:username`} component={User} />
|
||||||
<Route path={`/inbox`} component={Inbox} />
|
<Route path={`/inbox`} component={Inbox} />
|
||||||
<Route path={`/modlog/community/:community_id`} component={Modlog} />
|
<Route
|
||||||
|
path={`/modlog/community/:community_id`}
|
||||||
|
component={Modlog}
|
||||||
|
/>
|
||||||
<Route path={`/modlog`} component={Modlog} />
|
<Route path={`/modlog`} component={Modlog} />
|
||||||
<Route path={`/setup`} component={Setup} />
|
<Route path={`/setup`} component={Setup} />
|
||||||
<Route path={`/search/q/:q/type/:type/sort/:sort/page/:page`} component={Search} />
|
<Route
|
||||||
|
path={`/search/q/:q/type/:type/sort/:sort/page/:page`}
|
||||||
|
component={Search}
|
||||||
|
/>
|
||||||
<Route path={`/search`} component={Search} />
|
<Route path={`/search`} component={Search} />
|
||||||
<Route path={`/sponsors`} component={Sponsors} />
|
<Route path={`/sponsors`} component={Sponsors} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
@ -68,7 +82,6 @@ class Index extends Component<any, any> {
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render(<Index />, container);
|
render(<Index />, container);
|
||||||
|
|
|
@ -1,21 +1,72 @@
|
||||||
export enum UserOperation {
|
export enum UserOperation {
|
||||||
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead, SaveUserSettings, TransferCommunity, TransferSite, DeleteAccount
|
Login,
|
||||||
|
Register,
|
||||||
|
CreateCommunity,
|
||||||
|
CreatePost,
|
||||||
|
ListCommunities,
|
||||||
|
ListCategories,
|
||||||
|
GetPost,
|
||||||
|
GetCommunity,
|
||||||
|
CreateComment,
|
||||||
|
EditComment,
|
||||||
|
SaveComment,
|
||||||
|
CreateCommentLike,
|
||||||
|
GetPosts,
|
||||||
|
CreatePostLike,
|
||||||
|
EditPost,
|
||||||
|
SavePost,
|
||||||
|
EditCommunity,
|
||||||
|
FollowCommunity,
|
||||||
|
GetFollowedCommunities,
|
||||||
|
GetUserDetails,
|
||||||
|
GetReplies,
|
||||||
|
GetUserMentions,
|
||||||
|
EditUserMention,
|
||||||
|
GetModlog,
|
||||||
|
BanFromCommunity,
|
||||||
|
AddModToCommunity,
|
||||||
|
CreateSite,
|
||||||
|
EditSite,
|
||||||
|
GetSite,
|
||||||
|
AddAdmin,
|
||||||
|
BanUser,
|
||||||
|
Search,
|
||||||
|
MarkAllAsRead,
|
||||||
|
SaveUserSettings,
|
||||||
|
TransferCommunity,
|
||||||
|
TransferSite,
|
||||||
|
DeleteAccount,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CommentSortType {
|
export enum CommentSortType {
|
||||||
Hot, Top, New
|
Hot,
|
||||||
|
Top,
|
||||||
|
New,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ListingType {
|
export enum ListingType {
|
||||||
All, Subscribed, Community
|
All,
|
||||||
|
Subscribed,
|
||||||
|
Community,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SortType {
|
export enum SortType {
|
||||||
Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll
|
Hot,
|
||||||
|
New,
|
||||||
|
TopDay,
|
||||||
|
TopWeek,
|
||||||
|
TopMonth,
|
||||||
|
TopYear,
|
||||||
|
TopAll,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SearchType {
|
export enum SearchType {
|
||||||
All, Comments, Posts, Communities, Users, Url
|
All,
|
||||||
|
Comments,
|
||||||
|
Posts,
|
||||||
|
Communities,
|
||||||
|
Users,
|
||||||
|
Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
|
@ -24,6 +75,8 @@ export interface User {
|
||||||
username: string;
|
username: string;
|
||||||
show_nsfw: boolean;
|
show_nsfw: boolean;
|
||||||
theme: string;
|
theme: string;
|
||||||
|
default_sort_type: SortType;
|
||||||
|
default_listing_type: ListingType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserView {
|
export interface UserView {
|
||||||
|
@ -104,7 +157,7 @@ export interface Post {
|
||||||
export interface Comment {
|
export interface Comment {
|
||||||
id: number;
|
id: number;
|
||||||
creator_id: number;
|
creator_id: number;
|
||||||
post_id: number,
|
post_id: number;
|
||||||
parent_id?: number;
|
parent_id?: number;
|
||||||
content: string;
|
content: string;
|
||||||
removed: boolean;
|
removed: boolean;
|
||||||
|
@ -112,7 +165,7 @@ export interface Comment {
|
||||||
read: boolean;
|
read: boolean;
|
||||||
published: string;
|
published: string;
|
||||||
updated?: string;
|
updated?: string;
|
||||||
community_id: number,
|
community_id: number;
|
||||||
banned: boolean;
|
banned: boolean;
|
||||||
banned_from_community: boolean;
|
banned_from_community: boolean;
|
||||||
creator_name: string;
|
creator_name: string;
|
||||||
|
@ -122,6 +175,8 @@ export interface Comment {
|
||||||
user_id?: number;
|
user_id?: number;
|
||||||
my_vote?: number;
|
my_vote?: number;
|
||||||
saved?: boolean;
|
saved?: boolean;
|
||||||
|
user_mention_id?: number; // For mention type
|
||||||
|
recipient_id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
|
@ -143,7 +198,10 @@ export interface Site {
|
||||||
number_of_communities: number;
|
number_of_communities: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BanType {Community, Site};
|
export enum BanType {
|
||||||
|
Community,
|
||||||
|
Site,
|
||||||
|
}
|
||||||
|
|
||||||
export interface FollowCommunityForm {
|
export interface FollowCommunityForm {
|
||||||
community_id: number;
|
community_id: number;
|
||||||
|
@ -177,7 +235,7 @@ export interface UserDetailsResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetRepliesForm {
|
export interface GetRepliesForm {
|
||||||
sort: string; // TODO figure this one out
|
sort: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
unread_only: boolean;
|
unread_only: boolean;
|
||||||
|
@ -189,19 +247,43 @@ export interface GetRepliesResponse {
|
||||||
replies: Array<Comment>;
|
replies: Array<Comment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetUserMentionsForm {
|
||||||
|
sort: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
unread_only: boolean;
|
||||||
|
auth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetUserMentionsResponse {
|
||||||
|
op: string;
|
||||||
|
mentions: Array<Comment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditUserMentionForm {
|
||||||
|
user_mention_id: number;
|
||||||
|
read?: boolean;
|
||||||
|
auth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserMentionResponse {
|
||||||
|
op: string;
|
||||||
|
mention: Comment;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BanFromCommunityForm {
|
export interface BanFromCommunityForm {
|
||||||
community_id: number;
|
community_id: number;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
ban: boolean;
|
ban: boolean;
|
||||||
reason?: string,
|
reason?: string;
|
||||||
expires?: number,
|
expires?: number;
|
||||||
auth?: string;
|
auth?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BanFromCommunityResponse {
|
export interface BanFromCommunityResponse {
|
||||||
op: string;
|
op: string;
|
||||||
user: UserView,
|
user: UserView;
|
||||||
banned: boolean,
|
banned: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddModToCommunityForm {
|
export interface AddModToCommunityForm {
|
||||||
|
@ -236,15 +318,15 @@ export interface GetModlogForm {
|
||||||
|
|
||||||
export interface GetModlogResponse {
|
export interface GetModlogResponse {
|
||||||
op: string;
|
op: string;
|
||||||
removed_posts: Array<ModRemovePost>,
|
removed_posts: Array<ModRemovePost>;
|
||||||
locked_posts: Array<ModLockPost>,
|
locked_posts: Array<ModLockPost>;
|
||||||
stickied_posts: Array<ModStickyPost>,
|
stickied_posts: Array<ModStickyPost>;
|
||||||
removed_comments: Array<ModRemoveComment>,
|
removed_comments: Array<ModRemoveComment>;
|
||||||
removed_communities: Array<ModRemoveCommunity>,
|
removed_communities: Array<ModRemoveCommunity>;
|
||||||
banned_from_community: Array<ModBanFromCommunity>,
|
banned_from_community: Array<ModBanFromCommunity>;
|
||||||
banned: Array<ModBan>,
|
banned: Array<ModBan>;
|
||||||
added_to_community: Array<ModAddCommunity>,
|
added_to_community: Array<ModAddCommunity>;
|
||||||
added: Array<ModAdd>,
|
added: Array<ModAdd>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModRemovePost {
|
export interface ModRemovePost {
|
||||||
|
@ -253,7 +335,7 @@ export interface ModRemovePost {
|
||||||
post_id: number;
|
post_id: number;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
removed?: boolean;
|
removed?: boolean;
|
||||||
when_: string
|
when_: string;
|
||||||
mod_user_name: string;
|
mod_user_name: string;
|
||||||
post_name: string;
|
post_name: string;
|
||||||
community_id: number;
|
community_id: number;
|
||||||
|
@ -261,104 +343,104 @@ export interface ModRemovePost {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModLockPost {
|
export interface ModLockPost {
|
||||||
id: number,
|
id: number;
|
||||||
mod_user_id: number,
|
mod_user_id: number;
|
||||||
post_id: number,
|
post_id: number;
|
||||||
locked?: boolean,
|
locked?: boolean;
|
||||||
when_: string,
|
when_: string;
|
||||||
mod_user_name: string,
|
mod_user_name: string;
|
||||||
post_name: string,
|
post_name: string;
|
||||||
community_id: number,
|
community_id: number;
|
||||||
community_name: string,
|
community_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModStickyPost {
|
export interface ModStickyPost {
|
||||||
id: number,
|
id: number;
|
||||||
mod_user_id: number,
|
mod_user_id: number;
|
||||||
post_id: number,
|
post_id: number;
|
||||||
stickied?: boolean,
|
stickied?: boolean;
|
||||||
when_: string,
|
when_: string;
|
||||||
mod_user_name: string,
|
mod_user_name: string;
|
||||||
post_name: string,
|
post_name: string;
|
||||||
community_id: number,
|
community_id: number;
|
||||||
community_name: string,
|
community_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModRemoveComment {
|
export interface ModRemoveComment {
|
||||||
id: number,
|
id: number;
|
||||||
mod_user_id: number,
|
mod_user_id: number;
|
||||||
comment_id: number,
|
comment_id: number;
|
||||||
reason?: string,
|
reason?: string;
|
||||||
removed?: boolean,
|
removed?: boolean;
|
||||||
when_: string,
|
when_: string;
|
||||||
mod_user_name: string,
|
mod_user_name: string;
|
||||||
comment_user_id: number,
|
comment_user_id: number;
|
||||||
comment_user_name: string,
|
comment_user_name: string;
|
||||||
comment_content: string,
|
comment_content: string;
|
||||||
post_id: number,
|
post_id: number;
|
||||||
post_name: string,
|
post_name: string;
|
||||||
community_id: number,
|
community_id: number;
|
||||||
community_name: string,
|
community_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModRemoveCommunity {
|
export interface ModRemoveCommunity {
|
||||||
id: number,
|
id: number;
|
||||||
mod_user_id: number,
|
mod_user_id: number;
|
||||||
community_id: number,
|
community_id: number;
|
||||||
reason?: string,
|
reason?: string;
|
||||||
removed?: boolean,
|
removed?: boolean;
|
||||||
expires?: number,
|
expires?: number;
|
||||||
when_: string,
|
when_: string;
|
||||||
mod_user_name: string,
|
mod_user_name: string;
|
||||||
community_name: string,
|
community_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModBanFromCommunity {
|
export interface ModBanFromCommunity {
|
||||||
id: number,
|
id: number;
|
||||||
mod_user_id: number,
|
mod_user_id: number;
|
||||||
other_user_id: number,
|
other_user_id: number;
|
||||||
community_id: number,
|
community_id: number;
|
||||||
reason?: string,
|
reason?: string;
|
||||||
banned?: boolean,
|
banned?: boolean;
|
||||||
expires?: number,
|
expires?: number;
|
||||||
when_: string,
|
when_: string;
|
||||||
mod_user_name: string,
|
mod_user_name: string;
|
||||||
other_user_name: string,
|
other_user_name: string;
|
||||||
community_name: string,
|
community_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModBan {
|
export interface ModBan {
|
||||||
id: number,
|
id: number;
|
||||||
mod_user_id: number,
|
mod_user_id: number;
|
||||||
other_user_id: number,
|
other_user_id: number;
|
||||||
reason?: string,
|
reason?: string;
|
||||||
banned?: boolean,
|
banned?: boolean;
|
||||||
expires?: number,
|
expires?: number;
|
||||||
when_: string,
|
when_: string;
|
||||||
mod_user_name: string,
|
mod_user_name: string;
|
||||||
other_user_name: string,
|
other_user_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModAddCommunity {
|
export interface ModAddCommunity {
|
||||||
id: number,
|
id: number;
|
||||||
mod_user_id: number,
|
mod_user_id: number;
|
||||||
other_user_id: number,
|
other_user_id: number;
|
||||||
community_id: number,
|
community_id: number;
|
||||||
removed?: boolean,
|
removed?: boolean;
|
||||||
when_: string,
|
when_: string;
|
||||||
mod_user_name: string,
|
mod_user_name: string;
|
||||||
other_user_name: string,
|
other_user_name: string;
|
||||||
community_name: string,
|
community_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModAdd {
|
export interface ModAdd {
|
||||||
id: number,
|
id: number;
|
||||||
mod_user_id: number,
|
mod_user_id: number;
|
||||||
other_user_id: number,
|
other_user_id: number;
|
||||||
removed?: boolean,
|
removed?: boolean;
|
||||||
when_: string,
|
when_: string;
|
||||||
mod_user_name: string,
|
mod_user_name: string;
|
||||||
other_user_name: string,
|
other_user_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginForm {
|
export interface LoginForm {
|
||||||
|
@ -383,14 +465,16 @@ export interface LoginResponse {
|
||||||
export interface UserSettingsForm {
|
export interface UserSettingsForm {
|
||||||
show_nsfw: boolean;
|
show_nsfw: boolean;
|
||||||
theme: string;
|
theme: string;
|
||||||
|
default_sort_type: SortType;
|
||||||
|
default_listing_type: ListingType;
|
||||||
auth: string;
|
auth: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommunityForm {
|
export interface CommunityForm {
|
||||||
name: string;
|
name: string;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string,
|
description?: string;
|
||||||
category_id: number,
|
category_id: number;
|
||||||
edit_id?: number;
|
edit_id?: number;
|
||||||
removed?: boolean;
|
removed?: boolean;
|
||||||
deleted?: boolean;
|
deleted?: boolean;
|
||||||
|
@ -407,7 +491,6 @@ export interface GetCommunityResponse {
|
||||||
admins: Array<UserView>;
|
admins: Array<UserView>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface CommunityResponse {
|
export interface CommunityResponse {
|
||||||
op: string;
|
op: string;
|
||||||
community: Community;
|
community: Community;
|
||||||
|
@ -537,7 +620,7 @@ export interface CreatePostLikeResponse {
|
||||||
|
|
||||||
export interface SiteForm {
|
export interface SiteForm {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string,
|
description?: string;
|
||||||
removed?: boolean;
|
removed?: boolean;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
expires?: number;
|
expires?: number;
|
||||||
|
@ -552,7 +635,6 @@ export interface GetSiteResponse {
|
||||||
online: number;
|
online: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface SiteResponse {
|
export interface SiteResponse {
|
||||||
op: string;
|
op: string;
|
||||||
site: Site;
|
site: Site;
|
||||||
|
@ -561,15 +643,15 @@ export interface SiteResponse {
|
||||||
export interface BanUserForm {
|
export interface BanUserForm {
|
||||||
user_id: number;
|
user_id: number;
|
||||||
ban: boolean;
|
ban: boolean;
|
||||||
reason?: string,
|
reason?: string;
|
||||||
expires?: number,
|
expires?: number;
|
||||||
auth?: string;
|
auth?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BanUserResponse {
|
export interface BanUserResponse {
|
||||||
op: string;
|
op: string;
|
||||||
user: UserView,
|
user: UserView;
|
||||||
banned: boolean,
|
banned: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddAdminForm {
|
export interface AddAdminForm {
|
||||||
|
@ -597,7 +679,7 @@ export interface SearchResponse {
|
||||||
type_: string;
|
type_: string;
|
||||||
posts?: Array<Post>;
|
posts?: Array<Post>;
|
||||||
comments?: Array<Comment>;
|
comments?: Array<Comment>;
|
||||||
communities: Array<Community>;
|
communities: Array<Community>;
|
||||||
users: Array<UserView>;
|
users: Array<UserView>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,15 @@ import * as jwt_decode from 'jwt-decode';
|
||||||
import { Subject } from 'rxjs';
|
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<{user: User, unreadCount: number}>();
|
public sub: Subject<{ user: User; unreadCount: number }> = new Subject<{
|
||||||
|
user: User;
|
||||||
|
unreadCount: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
let jwt = Cookies.get("jwt");
|
let jwt = Cookies.get('jwt');
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
this.setUser(jwt);
|
this.setUser(jwt);
|
||||||
} else {
|
} else {
|
||||||
|
@ -22,30 +24,30 @@ export class UserService {
|
||||||
|
|
||||||
public login(res: LoginResponse) {
|
public login(res: LoginResponse) {
|
||||||
this.setUser(res.jwt);
|
this.setUser(res.jwt);
|
||||||
Cookies.set("jwt", res.jwt, { expires: 365 });
|
Cookies.set('jwt', res.jwt, { expires: 365 });
|
||||||
console.log("jwt cookie set");
|
console.log('jwt cookie set');
|
||||||
}
|
}
|
||||||
|
|
||||||
public logout() {
|
public logout() {
|
||||||
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, unreadCount: 0 });
|
||||||
console.log("Logged out.");
|
console.log('Logged out.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public get auth(): string {
|
public get auth(): string {
|
||||||
return Cookies.get("jwt");
|
return Cookies.get('jwt');
|
||||||
}
|
}
|
||||||
|
|
||||||
private setUser(jwt: string) {
|
private setUser(jwt: string) {
|
||||||
this.user = jwt_decode(jwt);
|
this.user = jwt_decode(jwt);
|
||||||
setTheme(this.user.theme);
|
setTheme(this.user.theme);
|
||||||
this.sub.next({user: this.user, unreadCount: 0});
|
this.sub.next({ user: this.user, unreadCount: 0 });
|
||||||
console.log(this.user);
|
console.log(this.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get Instance(){
|
public static get Instance() {
|
||||||
return this._instance || (this._instance = new this());
|
return this._instance || (this._instance = new this());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,36 @@
|
||||||
import { wsUri } from '../env';
|
import { wsUri } from '../env';
|
||||||
import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, SavePostForm, CommentForm, SaveCommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, TransferCommunityForm, AddAdminForm, TransferSiteForm, BanUserForm, SiteForm, Site, UserView, GetRepliesForm, SearchForm, UserSettingsForm, DeleteAccountForm } from '../interfaces';
|
import {
|
||||||
|
LoginForm,
|
||||||
|
RegisterForm,
|
||||||
|
UserOperation,
|
||||||
|
CommunityForm,
|
||||||
|
PostForm,
|
||||||
|
SavePostForm,
|
||||||
|
CommentForm,
|
||||||
|
SaveCommentForm,
|
||||||
|
CommentLikeForm,
|
||||||
|
GetPostsForm,
|
||||||
|
CreatePostLikeForm,
|
||||||
|
FollowCommunityForm,
|
||||||
|
GetUserDetailsForm,
|
||||||
|
ListCommunitiesForm,
|
||||||
|
GetModlogForm,
|
||||||
|
BanFromCommunityForm,
|
||||||
|
AddModToCommunityForm,
|
||||||
|
TransferCommunityForm,
|
||||||
|
AddAdminForm,
|
||||||
|
TransferSiteForm,
|
||||||
|
BanUserForm,
|
||||||
|
SiteForm,
|
||||||
|
Site,
|
||||||
|
UserView,
|
||||||
|
GetRepliesForm,
|
||||||
|
GetUserMentionsForm,
|
||||||
|
EditUserMentionForm,
|
||||||
|
SearchForm,
|
||||||
|
UserSettingsForm,
|
||||||
|
DeleteAccountForm,
|
||||||
|
} from '../interfaces';
|
||||||
import { webSocket } from 'rxjs/webSocket';
|
import { webSocket } from 'rxjs/webSocket';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
|
@ -19,16 +50,23 @@ export class WebSocketService {
|
||||||
|
|
||||||
// Necessary to not keep reconnecting
|
// Necessary to not keep reconnecting
|
||||||
this.subject
|
this.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(60000), take(999))))
|
.pipe(
|
||||||
|
retryWhen(errors =>
|
||||||
|
errors.pipe(
|
||||||
|
delay(1000)
|
||||||
|
// take(999)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
|
||||||
console.log(`Connected to ${wsUri}`);
|
console.log(`Connected to ${wsUri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get Instance(){
|
public static get Instance() {
|
||||||
return this._instance || (this._instance = new this());
|
return this._instance || (this._instance = new this());
|
||||||
}
|
}
|
||||||
|
|
||||||
public login(loginForm: LoginForm) {
|
public login(loginForm: LoginForm) {
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.Login, loginForm));
|
this.subject.next(this.wsSendWrapper(UserOperation.Login, loginForm));
|
||||||
}
|
}
|
||||||
|
@ -39,17 +77,23 @@ export class WebSocketService {
|
||||||
|
|
||||||
public createCommunity(communityForm: CommunityForm) {
|
public createCommunity(communityForm: CommunityForm) {
|
||||||
this.setAuth(communityForm);
|
this.setAuth(communityForm);
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.CreateCommunity, communityForm));
|
this.subject.next(
|
||||||
|
this.wsSendWrapper(UserOperation.CreateCommunity, communityForm)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public editCommunity(communityForm: CommunityForm) {
|
public editCommunity(communityForm: CommunityForm) {
|
||||||
this.setAuth(communityForm);
|
this.setAuth(communityForm);
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.EditCommunity, communityForm));
|
this.subject.next(
|
||||||
|
this.wsSendWrapper(UserOperation.EditCommunity, communityForm)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public followCommunity(followCommunityForm: FollowCommunityForm) {
|
public followCommunity(followCommunityForm: FollowCommunityForm) {
|
||||||
this.setAuth(followCommunityForm);
|
this.setAuth(followCommunityForm);
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.FollowCommunity, followCommunityForm));
|
this.subject.next(
|
||||||
|
this.wsSendWrapper(UserOperation.FollowCommunity, followCommunityForm)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public listCommunities(form: ListCommunitiesForm) {
|
public listCommunities(form: ListCommunitiesForm) {
|
||||||
|
@ -58,12 +102,16 @@ export class WebSocketService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getFollowedCommunities() {
|
public getFollowedCommunities() {
|
||||||
let data = {auth: UserService.Instance.auth };
|
let data = { auth: UserService.Instance.auth };
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetFollowedCommunities, data));
|
this.subject.next(
|
||||||
|
this.wsSendWrapper(UserOperation.GetFollowedCommunities, data)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public listCategories() {
|
public listCategories() {
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.ListCategories, undefined));
|
this.subject.next(
|
||||||
|
this.wsSendWrapper(UserOperation.ListCategories, undefined)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public createPost(postForm: PostForm) {
|
public createPost(postForm: PostForm) {
|
||||||
|
@ -72,33 +120,39 @@ export class WebSocketService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPost(postId: number) {
|
public getPost(postId: number) {
|
||||||
let data = {id: postId, auth: UserService.Instance.auth };
|
let data = { id: postId, auth: UserService.Instance.auth };
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetPost, data));
|
this.subject.next(this.wsSendWrapper(UserOperation.GetPost, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCommunity(communityId: number) {
|
public getCommunity(communityId: number) {
|
||||||
let data = {id: communityId, auth: UserService.Instance.auth };
|
let data = { id: communityId, auth: UserService.Instance.auth };
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetCommunity, data));
|
this.subject.next(this.wsSendWrapper(UserOperation.GetCommunity, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCommunityByName(name: string) {
|
public getCommunityByName(name: string) {
|
||||||
let data = {name: name, auth: UserService.Instance.auth };
|
let data = { name: name, auth: UserService.Instance.auth };
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetCommunity, data));
|
this.subject.next(this.wsSendWrapper(UserOperation.GetCommunity, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
public createComment(commentForm: CommentForm) {
|
public createComment(commentForm: CommentForm) {
|
||||||
this.setAuth(commentForm);
|
this.setAuth(commentForm);
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.CreateComment, commentForm));
|
this.subject.next(
|
||||||
|
this.wsSendWrapper(UserOperation.CreateComment, commentForm)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public editComment(commentForm: CommentForm) {
|
public editComment(commentForm: CommentForm) {
|
||||||
this.setAuth(commentForm);
|
this.setAuth(commentForm);
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.EditComment, commentForm));
|
this.subject.next(
|
||||||
|
this.wsSendWrapper(UserOperation.EditComment, commentForm)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public likeComment(form: CommentLikeForm) {
|
public likeComment(form: CommentLikeForm) {
|
||||||
this.setAuth(form);
|
this.setAuth(form);
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.CreateCommentLike, form));
|
this.subject.next(
|
||||||
|
this.wsSendWrapper(UserOperation.CreateCommentLike, form)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public saveComment(form: SaveCommentForm) {
|
public saveComment(form: SaveCommentForm) {
|
||||||
|
@ -133,19 +187,23 @@ export class WebSocketService {
|
||||||
|
|
||||||
public addModToCommunity(form: AddModToCommunityForm) {
|
public addModToCommunity(form: AddModToCommunityForm) {
|
||||||
this.setAuth(form);
|
this.setAuth(form);
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.AddModToCommunity, form));
|
this.subject.next(
|
||||||
|
this.wsSendWrapper(UserOperation.AddModToCommunity, form)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public transferCommunity(form: TransferCommunityForm) {
|
public transferCommunity(form: TransferCommunityForm) {
|
||||||
this.setAuth(form);
|
this.setAuth(form);
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.TransferCommunity, form));
|
this.subject.next(
|
||||||
|
this.wsSendWrapper(UserOperation.TransferCommunity, form)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public transferSite(form: TransferSiteForm) {
|
public transferSite(form: TransferSiteForm) {
|
||||||
this.setAuth(form);
|
this.setAuth(form);
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.TransferSite, form));
|
this.subject.next(this.wsSendWrapper(UserOperation.TransferSite, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
public banUser(form: BanUserForm) {
|
public banUser(form: BanUserForm) {
|
||||||
this.setAuth(form);
|
this.setAuth(form);
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.BanUser, form));
|
this.subject.next(this.wsSendWrapper(UserOperation.BanUser, form));
|
||||||
|
@ -166,6 +224,16 @@ export class WebSocketService {
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetReplies, form));
|
this.subject.next(this.wsSendWrapper(UserOperation.GetReplies, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getUserMentions(form: GetUserMentionsForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.subject.next(this.wsSendWrapper(UserOperation.GetUserMentions, form));
|
||||||
|
}
|
||||||
|
|
||||||
|
public editUserMention(form: EditUserMentionForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.subject.next(this.wsSendWrapper(UserOperation.EditUserMention, form));
|
||||||
|
}
|
||||||
|
|
||||||
public getModlog(form: GetModlogForm) {
|
public getModlog(form: GetModlogForm) {
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, form));
|
this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, form));
|
||||||
}
|
}
|
||||||
|
@ -196,7 +264,9 @@ export class WebSocketService {
|
||||||
|
|
||||||
public saveUserSettings(userSettingsForm: UserSettingsForm) {
|
public saveUserSettings(userSettingsForm: UserSettingsForm) {
|
||||||
this.setAuth(userSettingsForm);
|
this.setAuth(userSettingsForm);
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.SaveUserSettings, userSettingsForm));
|
this.subject.next(
|
||||||
|
this.wsSendWrapper(UserOperation.SaveUserSettings, userSettingsForm)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteAccount(form: DeleteAccountForm) {
|
public deleteAccount(form: DeleteAccountForm) {
|
||||||
|
@ -214,13 +284,12 @@ export class WebSocketService {
|
||||||
obj.auth = UserService.Instance.auth;
|
obj.auth = UserService.Instance.auth;
|
||||||
if (obj.auth == null && throwErr) {
|
if (obj.auth == null && throwErr) {
|
||||||
alert(i18n.t('not_logged_in'));
|
alert(i18n.t('not_logged_in'));
|
||||||
throw "Not logged in";
|
throw 'Not logged in';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onbeforeunload = (() => {
|
window.onbeforeunload = () => {
|
||||||
WebSocketService.Instance.subject.unsubscribe();
|
WebSocketService.Instance.subject.unsubscribe();
|
||||||
WebSocketService.Instance.subject = null;
|
WebSocketService.Instance.subject = null;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,18 +5,18 @@ export const de = {
|
||||||
no_posts: 'Keine Beiträge.',
|
no_posts: 'Keine Beiträge.',
|
||||||
create_a_post: 'Einen Beitrag anlegen',
|
create_a_post: 'Einen Beitrag anlegen',
|
||||||
create_post: 'Beitrag anlegen',
|
create_post: 'Beitrag anlegen',
|
||||||
number_of_posts:'{{count}} Beiträge',
|
number_of_posts: '{{count}} Beiträge',
|
||||||
posts: 'Beiträge',
|
posts: 'Beiträge',
|
||||||
related_posts: 'Diese Beiträge könnten verwandt sein',
|
related_posts: 'Diese Beiträge könnten verwandt sein',
|
||||||
comments: 'Kommentare',
|
comments: 'Kommentare',
|
||||||
number_of_comments:'{{count}} Kommentare',
|
number_of_comments: '{{count}} Kommentare',
|
||||||
remove_comment: 'Kommentar löschen',
|
remove_comment: 'Kommentar löschen',
|
||||||
communities: 'Communities',
|
communities: 'Communities',
|
||||||
create_a_community: 'Eine community anlegen',
|
create_a_community: 'Eine community anlegen',
|
||||||
create_community: 'Community anlegen',
|
create_community: 'Community anlegen',
|
||||||
remove_community: 'Community entfernen',
|
remove_community: 'Community entfernen',
|
||||||
subscribed_to_communities:'Abonnierte <1>communities</1>',
|
subscribed_to_communities: 'Abonnierte <1>communities</1>',
|
||||||
trending_communities:'Trending <1>communities</1>',
|
trending_communities: 'Trending <1>communities</1>',
|
||||||
list_of_communities: 'Liste von communities',
|
list_of_communities: 'Liste von communities',
|
||||||
community_reqs: 'Kleinbuchstaben, Großbuchstaben und keine Leerzeichen.',
|
community_reqs: 'Kleinbuchstaben, Großbuchstaben und keine Leerzeichen.',
|
||||||
edit: 'editieren',
|
edit: 'editieren',
|
||||||
|
@ -53,9 +53,9 @@ export const de = {
|
||||||
create: 'anlegen',
|
create: 'anlegen',
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
email_or_username: 'Email oder Username',
|
email_or_username: 'Email oder Username',
|
||||||
number_of_users:'{{count}} Benutzer',
|
number_of_users: '{{count}} Benutzer',
|
||||||
number_of_subscribers:'{{count}} Abonnenten',
|
number_of_subscribers: '{{count}} Abonnenten',
|
||||||
number_of_points:'{{count}} Punkte',
|
number_of_points: '{{count}} Punkte',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
title: 'Titel',
|
title: 'Titel',
|
||||||
category: 'Kategorie',
|
category: 'Kategorie',
|
||||||
|
@ -88,7 +88,8 @@ export const de = {
|
||||||
view: 'Ansicht',
|
view: 'Ansicht',
|
||||||
logout: 'Ausloggen',
|
logout: 'Ausloggen',
|
||||||
login_sign_up: 'Einloggen / Registrieren',
|
login_sign_up: 'Einloggen / Registrieren',
|
||||||
notifications_error: 'Desktop-Benachrichtigungen sind in deinem browser nicht verfügbar. Versuche Firefox oder Chrome.',
|
notifications_error:
|
||||||
|
'Desktop-Benachrichtigungen sind in deinem browser nicht verfügbar. Versuche Firefox oder Chrome.',
|
||||||
unread_messages: 'Ungelesene Nachrichten',
|
unread_messages: 'Ungelesene Nachrichten',
|
||||||
password: 'Passwort',
|
password: 'Passwort',
|
||||||
verify_password: 'Passwort überprüfen',
|
verify_password: 'Passwort überprüfen',
|
||||||
|
@ -111,14 +112,17 @@ export const de = {
|
||||||
modified: 'verändert',
|
modified: 'verändert',
|
||||||
sponsors: 'Sponsoren',
|
sponsors: 'Sponsoren',
|
||||||
sponsors_of_lemmy: 'Sponsoren von Lemmy',
|
sponsors_of_lemmy: 'Sponsoren von Lemmy',
|
||||||
sponsor_message: 'Lemmy ist freie <1>Open-Source</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:',
|
sponsor_message:
|
||||||
|
'Lemmy ist freie <1>Open-Source</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:',
|
||||||
support_on_patreon: 'Auf Patreon unterstützen',
|
support_on_patreon: 'Auf Patreon unterstützen',
|
||||||
general_sponsors:'Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.',
|
general_sponsors:
|
||||||
|
'Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.',
|
||||||
bitcoin: 'Bitcoin',
|
bitcoin: 'Bitcoin',
|
||||||
ethereum: 'Ethereum',
|
ethereum: 'Ethereum',
|
||||||
code: 'Code',
|
code: 'Code',
|
||||||
powered_by: 'Bereitgestellt durch',
|
powered_by: 'Bereitgestellt durch',
|
||||||
landing_0: 'Lemmy ist ein <1>Link Aggregator</1> / Reddit Alternative im <2>Fediverse</2>.<3></3>Es ist selbst-hostbar, hat live-updates von Kommentar-threads und ist winzig (<4>~80kB</4>). Federation in das ActivityPub Netzwerk ist geplant. <5></5>Dies ist eine <6>sehr frühe Beta Version</6>, und viele Features funktionieren zurzeit nicht richtig oder fehlen. <7></7>Schlage neue Features vor oder melde Bugs <8>hier.</8><9></9>Gebaut mit <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
landing_0:
|
||||||
|
'Lemmy ist ein <1>Link Aggregator</1> / Reddit Alternative im <2>Fediverse</2>.<3></3>Es ist selbst-hostbar, hat live-updates von Kommentar-threads und ist winzig (<4>~80kB</4>). Federation in das ActivityPub Netzwerk ist geplant. <5></5>Dies ist eine <6>sehr frühe Beta Version</6>, und viele Features funktionieren zurzeit nicht richtig oder fehlen. <7></7>Schlage neue Features vor oder melde Bugs <8>hier.</8><9></9>Gebaut mit <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||||
not_logged_in: 'Nicht eingeloggt.',
|
not_logged_in: 'Nicht eingeloggt.',
|
||||||
community_ban: 'Du wurdest von dieser Community gebannt.',
|
community_ban: 'Du wurdest von dieser Community gebannt.',
|
||||||
site_ban: 'Du wurdest von dieser Seite gebannt',
|
site_ban: 'Du wurdest von dieser Seite gebannt',
|
||||||
|
@ -132,7 +136,8 @@ export const de = {
|
||||||
couldnt_find_community: 'Konnte Community nicht finden.',
|
couldnt_find_community: 'Konnte Community nicht finden.',
|
||||||
couldnt_update_community: 'Konnte Community nicht aktualisieren.',
|
couldnt_update_community: 'Konnte Community nicht aktualisieren.',
|
||||||
community_already_exists: 'Community existiert bereits.',
|
community_already_exists: 'Community existiert bereits.',
|
||||||
community_moderator_already_exists: 'Community Moderator existiert bereits.',
|
community_moderator_already_exists:
|
||||||
|
'Community Moderator existiert bereits.',
|
||||||
community_follower_already_exists: 'Community Follower existiert bereits.',
|
community_follower_already_exists: 'Community Follower existiert bereits.',
|
||||||
community_user_already_banned: 'Community Nutzer schon gebannt.',
|
community_user_already_banned: 'Community Nutzer schon gebannt.',
|
||||||
couldnt_create_post: 'Konnte Beitrag nicht anlegen.',
|
couldnt_create_post: 'Konnte Beitrag nicht anlegen.',
|
||||||
|
@ -145,13 +150,14 @@ export const de = {
|
||||||
not_an_admin: 'Kein Administrator.',
|
not_an_admin: 'Kein Administrator.',
|
||||||
site_already_exists: 'Seite existiert bereits.',
|
site_already_exists: 'Seite existiert bereits.',
|
||||||
couldnt_update_site: 'Konnte Seite nicht aktualisieren.',
|
couldnt_update_site: 'Konnte Seite nicht aktualisieren.',
|
||||||
couldnt_find_that_username_or_email: 'Konnte Username oder E-Mail nicht finden.',
|
couldnt_find_that_username_or_email:
|
||||||
|
'Konnte Username oder E-Mail nicht finden.',
|
||||||
password_incorrect: 'Passwort falsch.',
|
password_incorrect: 'Passwort falsch.',
|
||||||
passwords_dont_match: 'Passwörter stimmen nicht überein.',
|
passwords_dont_match: 'Passwörter stimmen nicht überein.',
|
||||||
admin_already_created: 'Entschuldigung, es gibt schon einen Administrator.',
|
admin_already_created: 'Entschuldigung, es gibt schon einen Administrator.',
|
||||||
user_already_exists: 'Nutzer existiert bereits.',
|
user_already_exists: 'Nutzer existiert bereits.',
|
||||||
couldnt_update_user: 'Konnte Nutzer nicht aktualisieren',
|
couldnt_update_user: 'Konnte Nutzer nicht aktualisieren',
|
||||||
system_err_login: 'Systemfehler. Versuche dich aus- und wieder einzuloggen.',
|
system_err_login:
|
||||||
|
'Systemfehler. Versuche dich aus- und wieder einzuloggen.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,8 @@ export const en = {
|
||||||
delete: 'delete',
|
delete: 'delete',
|
||||||
deleted: 'deleted',
|
deleted: 'deleted',
|
||||||
delete_account: 'Delete Account',
|
delete_account: 'Delete Account',
|
||||||
delete_account_confirm: 'Warning: this will permanently delete all your data. Enter your password to confirm.',
|
delete_account_confirm:
|
||||||
|
'Warning: this will permanently delete all your data. Enter your password to confirm.',
|
||||||
restore: 'restore',
|
restore: 'restore',
|
||||||
ban: 'ban',
|
ban: 'ban',
|
||||||
ban_from_site: 'ban from site',
|
ban_from_site: 'ban from site',
|
||||||
|
@ -100,6 +101,8 @@ export const en = {
|
||||||
mark_all_as_read: 'mark all as read',
|
mark_all_as_read: 'mark all as read',
|
||||||
type: 'Type',
|
type: 'Type',
|
||||||
unread: 'Unread',
|
unread: 'Unread',
|
||||||
|
replies: 'Replies',
|
||||||
|
mentions: 'Mentions',
|
||||||
reply_sent: 'Reply sent',
|
reply_sent: 'Reply sent',
|
||||||
search: 'Search',
|
search: 'Search',
|
||||||
overview: 'Overview',
|
overview: 'Overview',
|
||||||
|
@ -108,7 +111,8 @@ export const en = {
|
||||||
login_sign_up: 'Login / Sign up',
|
login_sign_up: 'Login / Sign up',
|
||||||
login: 'Login',
|
login: 'Login',
|
||||||
sign_up: 'Sign Up',
|
sign_up: 'Sign Up',
|
||||||
notifications_error: 'Desktop notifications not available in your browser. Try Firefox or Chrome.',
|
notifications_error:
|
||||||
|
'Desktop notifications not available in your browser. Try Firefox or Chrome.',
|
||||||
unread_messages: 'Unread Messages',
|
unread_messages: 'Unread Messages',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
verify_password: 'Verify Password',
|
verify_password: 'Verify Password',
|
||||||
|
@ -134,9 +138,11 @@ export const en = {
|
||||||
theme: 'Theme',
|
theme: 'Theme',
|
||||||
sponsors: 'Sponsors',
|
sponsors: 'Sponsors',
|
||||||
sponsors_of_lemmy: 'Sponsors of Lemmy',
|
sponsors_of_lemmy: 'Sponsors of Lemmy',
|
||||||
sponsor_message: 'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
|
sponsor_message:
|
||||||
|
'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
|
||||||
support_on_patreon: 'Support on Patreon',
|
support_on_patreon: 'Support on Patreon',
|
||||||
general_sponsors: 'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
general_sponsors:
|
||||||
|
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
||||||
crypto: 'Crypto',
|
crypto: 'Crypto',
|
||||||
bitcoin: 'Bitcoin',
|
bitcoin: 'Bitcoin',
|
||||||
ethereum: 'Ethereum',
|
ethereum: 'Ethereum',
|
||||||
|
@ -151,40 +157,41 @@ export const en = {
|
||||||
yes: 'yes',
|
yes: 'yes',
|
||||||
no: 'no',
|
no: 'no',
|
||||||
powered_by: 'Powered by',
|
powered_by: 'Powered by',
|
||||||
landing_0: 'Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It\'s self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
landing_0:
|
||||||
|
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
|
||||||
not_logged_in: 'Not logged in.',
|
not_logged_in: 'Not logged in.',
|
||||||
community_ban: 'You have been banned from this community.',
|
community_ban: 'You have been banned from this community.',
|
||||||
site_ban: 'You have been banned from the site',
|
site_ban: 'You have been banned from the site',
|
||||||
couldnt_create_comment: 'Couldn\'t create comment.',
|
couldnt_create_comment: "Couldn't create comment.",
|
||||||
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.",
|
||||||
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.',
|
||||||
couldnt_find_community: 'Couldn\'t find community.',
|
couldnt_find_community: "Couldn't find community.",
|
||||||
couldnt_update_community: 'Couldn\'t update Community.',
|
couldnt_update_community: "Couldn't update Community.",
|
||||||
community_already_exists: 'Community already exists.',
|
community_already_exists: 'Community already exists.',
|
||||||
community_moderator_already_exists: 'Community moderator already exists.',
|
community_moderator_already_exists: 'Community moderator already exists.',
|
||||||
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.",
|
||||||
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",
|
||||||
couldnt_update_post: 'Couldn\'t update post',
|
couldnt_update_post: "Couldn't update post",
|
||||||
couldnt_save_post: 'Couldn\'t save post.',
|
couldnt_save_post: "Couldn't save post.",
|
||||||
no_slurs: 'No slurs.',
|
no_slurs: 'No slurs.',
|
||||||
not_an_admin: 'Not an admin.',
|
not_an_admin: 'Not an admin.',
|
||||||
site_already_exists: 'Site already exists.',
|
site_already_exists: 'Site already exists.',
|
||||||
couldnt_update_site: 'Couldn\'t update site.',
|
couldnt_update_site: "Couldn't update site.",
|
||||||
couldnt_find_that_username_or_email: 'Couldn\'t find that username or email.',
|
couldnt_find_that_username_or_email:
|
||||||
|
"Couldn't find that username or email.",
|
||||||
password_incorrect: 'Password incorrect.',
|
password_incorrect: 'Password incorrect.',
|
||||||
passwords_dont_match: 'Passwords do not match.',
|
passwords_dont_match: 'Passwords do not match.',
|
||||||
admin_already_created: 'Sorry, there\'s already an admin.',
|
admin_already_created: "Sorry, there's already an admin.",
|
||||||
user_already_exists: 'User already exists.',
|
user_already_exists: 'User already exists.',
|
||||||
couldnt_update_user: 'Couldn\'t update user.',
|
couldnt_update_user: "Couldn't update user.",
|
||||||
system_err_login: 'System error. Try logging out and back in.',
|
system_err_login: 'System error. Try logging out and back in.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,21 +5,21 @@ export const eo = {
|
||||||
no_posts: 'Ne Poŝtoj.',
|
no_posts: 'Ne Poŝtoj.',
|
||||||
create_a_post: 'Verki Poŝton',
|
create_a_post: 'Verki Poŝton',
|
||||||
create_post: 'Verki Poŝton',
|
create_post: 'Verki Poŝton',
|
||||||
number_of_posts:'{{count}} Poŝtoj',
|
number_of_posts: '{{count}} Poŝtoj',
|
||||||
posts: 'Poŝtoj',
|
posts: 'Poŝtoj',
|
||||||
related_posts: 'Tiuj poŝtoj eble rilatas',
|
related_posts: 'Tiuj poŝtoj eble rilatas',
|
||||||
cross_posts: 'Tiuj ligilo ankaŭ estas poŝtinta al:',
|
cross_posts: 'Tiuj ligilo ankaŭ estas poŝtinta al:',
|
||||||
cross_post: 'laŭapoŝto',
|
cross_post: 'laŭapoŝto',
|
||||||
comments: 'Komentoj',
|
comments: 'Komentoj',
|
||||||
number_of_comments:'{{count}} Komentoj',
|
number_of_comments: '{{count}} Komentoj',
|
||||||
remove_comment: 'Fortiri Komentojn',
|
remove_comment: 'Fortiri Komentojn',
|
||||||
communities: 'Komunumoj',
|
communities: 'Komunumoj',
|
||||||
users: 'Uzantoj',
|
users: 'Uzantoj',
|
||||||
create_a_community: 'Krei komunumon',
|
create_a_community: 'Krei komunumon',
|
||||||
create_community: 'Krei Komunumon',
|
create_community: 'Krei Komunumon',
|
||||||
remove_community: 'Forigi Komunumon',
|
remove_community: 'Forigi Komunumon',
|
||||||
subscribed_to_communities:'Abonita al <1>komunumoj</1>',
|
subscribed_to_communities: 'Abonita al <1>komunumoj</1>',
|
||||||
trending_communities:'Furora <1>komunumoj</1>',
|
trending_communities: 'Furora <1>komunumoj</1>',
|
||||||
list_of_communities: 'Listo de komunumoj',
|
list_of_communities: 'Listo de komunumoj',
|
||||||
community_reqs: 'minusklaj leteroj, substrekoj, kaj ne spacetoj.',
|
community_reqs: 'minusklaj leteroj, substrekoj, kaj ne spacetoj.',
|
||||||
edit: 'redakti',
|
edit: 'redakti',
|
||||||
|
@ -57,9 +57,9 @@ export const eo = {
|
||||||
create: 'krei',
|
create: 'krei',
|
||||||
username: 'Uzantnomo',
|
username: 'Uzantnomo',
|
||||||
email_or_username: 'Retadreso aŭ Uzantnomo',
|
email_or_username: 'Retadreso aŭ Uzantnomo',
|
||||||
number_of_users:'{{count}} Uzantoj',
|
number_of_users: '{{count}} Uzantoj',
|
||||||
number_of_subscribers:'{{count}} Abonantoj',
|
number_of_subscribers: '{{count}} Abonantoj',
|
||||||
number_of_points:'{{count}} Voĉdonoj',
|
number_of_points: '{{count}} Voĉdonoj',
|
||||||
name: 'Nomo',
|
name: 'Nomo',
|
||||||
title: 'Titolo',
|
title: 'Titolo',
|
||||||
category: 'Kategorio',
|
category: 'Kategorio',
|
||||||
|
@ -95,7 +95,8 @@ export const eo = {
|
||||||
login_sign_up: 'Ensaluti / Registriĝi',
|
login_sign_up: 'Ensaluti / Registriĝi',
|
||||||
login: 'Ensaluti',
|
login: 'Ensaluti',
|
||||||
sign_up: 'Registriĝi',
|
sign_up: 'Registriĝi',
|
||||||
notifications_error: 'Labortablaj avizoj estas nehavebla en via retumilo. Provu Firefox-on aŭ Chrome-on.',
|
notifications_error:
|
||||||
|
'Labortablaj avizoj estas nehavebla en via retumilo. Provu Firefox-on aŭ Chrome-on.',
|
||||||
unread_messages: 'Nelegitaj Mesaĝoj',
|
unread_messages: 'Nelegitaj Mesaĝoj',
|
||||||
password: 'Pasvorto',
|
password: 'Pasvorto',
|
||||||
verify_password: 'Konfirmu Vian Pasvorton',
|
verify_password: 'Konfirmu Vian Pasvorton',
|
||||||
|
@ -120,9 +121,11 @@ export const eo = {
|
||||||
show_nsfw: 'Vidigi NSFW-an enhavon',
|
show_nsfw: 'Vidigi NSFW-an enhavon',
|
||||||
sponsors: 'Subtenantoj',
|
sponsors: 'Subtenantoj',
|
||||||
sponsors_of_lemmy: 'Subtenantoj de Lemmy',
|
sponsors_of_lemmy: 'Subtenantoj de Lemmy',
|
||||||
sponsor_message: 'Lemmy estas senpaga, <1>liberkoda</1> programaro. Tio signifas ne reklami, pagigi, aŭ riska kapitalo, ĉiam. Viaj donacoj rekte subtenas plentempan evoluon de la projekto. Dankon al tiuj homoj:',
|
sponsor_message:
|
||||||
|
'Lemmy estas senpaga, <1>liberkoda</1> programaro. Tio signifas ne reklami, pagigi, aŭ riska kapitalo, ĉiam. Viaj donacoj rekte subtenas plentempan evoluon de la projekto. Dankon al tiuj homoj:',
|
||||||
support_on_patreon: 'Subteni per Patreon',
|
support_on_patreon: 'Subteni per Patreon',
|
||||||
general_sponsors:'Ĝeneralaj Subtenantoj estas tiuj ke donacis inter $10 kaj $39 al Lemmy.',
|
general_sponsors:
|
||||||
|
'Ĝeneralaj Subtenantoj estas tiuj ke donacis inter $10 kaj $39 al Lemmy.',
|
||||||
crypto: 'Crypto',
|
crypto: 'Crypto',
|
||||||
bitcoin: 'Bitcoin',
|
bitcoin: 'Bitcoin',
|
||||||
ethereum: 'Ethereum',
|
ethereum: 'Ethereum',
|
||||||
|
@ -134,7 +137,8 @@ export const eo = {
|
||||||
transfer_community: 'transdoni la komunumon',
|
transfer_community: 'transdoni la komunumon',
|
||||||
transfer_site: 'transdoni la retejon',
|
transfer_site: 'transdoni la retejon',
|
||||||
powered_by: 'Konstruis per',
|
powered_by: 'Konstruis per',
|
||||||
landing_0: 'Lemmy estas <1>ligila agregatilo</1> / Reddit anstataŭo ke intenciĝas funkci en la <2>federacio-universo</2>.<3></3>ĝi estas mem-gastigebla, havas nuna-ĝisdatigajn komentarojn, kaj estas malgrandega (<4>~80kB</4>). Federacio en la ActivityPub-an reton estas planizita. <5></5>Estas <6>fruega beta versio</6>, kaj multaj trajtoj estas nune difektaj aŭ mankaj. <7></7>Sugestias novajn trajtojn aŭ raportas cimojn <8>ĉi tie.</8><9></9>Faris per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
landing_0:
|
||||||
|
'Lemmy estas <1>ligila agregatilo</1> / Reddit anstataŭo ke intenciĝas funkci en la <2>federacio-universo</2>.<3></3>ĝi estas mem-gastigebla, havas nuna-ĝisdatigajn komentarojn, kaj estas malgrandega (<4>~80kB</4>). Federacio en la ActivityPub-an reton estas planizita. <5></5>Estas <6>fruega beta versio</6>, kaj multaj trajtoj estas nune difektaj aŭ mankaj. <7></7>Sugestias novajn trajtojn aŭ raportas cimojn <8>ĉi tie.</8><9></9>Faris per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||||
not_logged_in: 'Ne estas ensalutinta.',
|
not_logged_in: 'Ne estas ensalutinta.',
|
||||||
community_ban: 'Vi estas forbarita de la komunumo.',
|
community_ban: 'Vi estas forbarita de la komunumo.',
|
||||||
site_ban: 'Vi estas forbarita de la retejo',
|
site_ban: 'Vi estas forbarita de la retejo',
|
||||||
|
@ -161,7 +165,8 @@ export const eo = {
|
||||||
not_an_admin: 'Ne estas administranto.',
|
not_an_admin: 'Ne estas administranto.',
|
||||||
site_already_exists: 'Retejo jam ekzistas.',
|
site_already_exists: 'Retejo jam ekzistas.',
|
||||||
couldnt_update_site: 'Ne povis ĝisdatigi la retejon.',
|
couldnt_update_site: 'Ne povis ĝisdatigi la retejon.',
|
||||||
couldnt_find_that_username_or_email: 'Ne povis trovi tiun uzantnomon aŭ retadreson.',
|
couldnt_find_that_username_or_email:
|
||||||
|
'Ne povis trovi tiun uzantnomon aŭ retadreson.',
|
||||||
password_incorrect: 'Pasvorto malĝustas.',
|
password_incorrect: 'Pasvorto malĝustas.',
|
||||||
passwords_dont_match: 'Pasvortoj ne samas.',
|
passwords_dont_match: 'Pasvortoj ne samas.',
|
||||||
admin_already_created: 'Pardonu, jam estas administranto.',
|
admin_already_created: 'Pardonu, jam estas administranto.',
|
||||||
|
@ -169,5 +174,4 @@ export const eo = {
|
||||||
couldnt_update_user: 'Ne povis ĝisdatigi la uzanton.',
|
couldnt_update_user: 'Ne povis ĝisdatigi la uzanton.',
|
||||||
system_err_login: 'Sistema eraro. Provu elsaluti kaj ensaluti.',
|
system_err_login: 'Sistema eraro. Provu elsaluti kaj ensaluti.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,23 +5,23 @@ export const es = {
|
||||||
no_posts: 'Sin publicaciones.',
|
no_posts: 'Sin publicaciones.',
|
||||||
create_a_post: 'Crear una publicación',
|
create_a_post: 'Crear una publicación',
|
||||||
create_post: 'Crear Publicación',
|
create_post: 'Crear Publicación',
|
||||||
number_of_posts:'{{count}} Publicaciones',
|
number_of_posts: '{{count}} Publicaciones',
|
||||||
posts: 'Publicaciones',
|
posts: 'Publicaciones',
|
||||||
related_posts: 'Estas publicaciones podrían estar relacionadas',
|
related_posts: 'Estas publicaciones podrían estar relacionadas',
|
||||||
cross_posts: 'Este link también ha sido publicado en:',
|
cross_posts: 'Este link también ha sido publicado en:',
|
||||||
cross_post: 'cross-post',
|
cross_post: 'cross-post',
|
||||||
comments: 'Comentarios',
|
comments: 'Comentarios',
|
||||||
number_of_comments:'{{count}} Comentarios',
|
number_of_comments: '{{count}} Comentarios',
|
||||||
remove_comment: 'Remover Comentarios',
|
remove_comment: 'Remover Comentarios',
|
||||||
communities: 'Comunidades',
|
communities: 'Comunidades',
|
||||||
users: 'Usuarios',
|
users: 'Usuarios',
|
||||||
create_a_community: 'Crear una comunidad',
|
create_a_community: 'Crear una comunidad',
|
||||||
create_community: 'Crear Comunidad',
|
create_community: 'Crear Comunidad',
|
||||||
remove_community: 'Remover Comunidad',
|
remove_community: 'Remover Comunidad',
|
||||||
subscribed_to_communities:'Suscrito a <1>comunidades</1>',
|
subscribed_to_communities: 'Suscrito a <1>comunidades</1>',
|
||||||
trending_communities:'<1>Comunidades</1> en tendencia',
|
trending_communities: '<1>Comunidades</1> en tendencia',
|
||||||
list_of_communities: 'Lista de comunidades',
|
list_of_communities: 'Lista de comunidades',
|
||||||
number_of_communities:'{{count}} Comunidades',
|
number_of_communities: '{{count}} Comunidades',
|
||||||
community_reqs: 'minúsculas, guión bajo, y sin espacios.',
|
community_reqs: 'minúsculas, guión bajo, y sin espacios.',
|
||||||
edit: 'editar',
|
edit: 'editar',
|
||||||
reply: 'responder',
|
reply: 'responder',
|
||||||
|
@ -56,7 +56,8 @@ export const es = {
|
||||||
delete: 'eliminar',
|
delete: 'eliminar',
|
||||||
deleted: 'eliminado',
|
deleted: 'eliminado',
|
||||||
delete_account: 'Eliminar Cuenta',
|
delete_account: 'Eliminar Cuenta',
|
||||||
delete_account_confirm: 'Peligro: esta acción eliminará permanentemente tu información. ¿Estás seguro?',
|
delete_account_confirm:
|
||||||
|
'Peligro: esta acción eliminará permanentemente tu información. ¿Estás seguro?',
|
||||||
restore: 'restaurar',
|
restore: 'restaurar',
|
||||||
ban: 'expulsar',
|
ban: 'expulsar',
|
||||||
ban_from_site: 'expulsar del sitio',
|
ban_from_site: 'expulsar del sitio',
|
||||||
|
@ -69,9 +70,9 @@ export const es = {
|
||||||
creator: 'creador',
|
creator: 'creador',
|
||||||
username: 'Nombre de Usuario',
|
username: 'Nombre de Usuario',
|
||||||
email_or_username: 'Correo electrónico o Nombre de Usuario',
|
email_or_username: 'Correo electrónico o Nombre de Usuario',
|
||||||
number_of_users:'{{count}} Usuarios',
|
number_of_users: '{{count}} Usuarios',
|
||||||
number_of_subscribers:'{{count}} Suscriptores',
|
number_of_subscribers: '{{count}} Suscriptores',
|
||||||
number_of_points:'{{count}} Puntos',
|
number_of_points: '{{count}} Puntos',
|
||||||
number_online: '{{count}} Usaurios En Línea',
|
number_online: '{{count}} Usaurios En Línea',
|
||||||
name: 'Nombre',
|
name: 'Nombre',
|
||||||
title: 'Titulo',
|
title: 'Titulo',
|
||||||
|
@ -108,7 +109,8 @@ export const es = {
|
||||||
login_sign_up: 'Iniciar sesión / Crear cuenta',
|
login_sign_up: 'Iniciar sesión / Crear cuenta',
|
||||||
login: 'Iniciar sesión',
|
login: 'Iniciar sesión',
|
||||||
sign_up: 'Crear cuenta',
|
sign_up: 'Crear cuenta',
|
||||||
notifications_error: 'Notificaciones de escritorio no disponibles en tu navegador. Prueba Firefox o Chrome.',
|
notifications_error:
|
||||||
|
'Notificaciones de escritorio no disponibles en tu navegador. Prueba Firefox o Chrome.',
|
||||||
unread_messages: 'Mensajes no leídos',
|
unread_messages: 'Mensajes no leídos',
|
||||||
password: 'Contraseña',
|
password: 'Contraseña',
|
||||||
verify_password: 'Verificar contraseña',
|
verify_password: 'Verificar contraseña',
|
||||||
|
@ -134,9 +136,11 @@ export const es = {
|
||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
sponsors: 'Patrocinadores',
|
sponsors: 'Patrocinadores',
|
||||||
sponsors_of_lemmy: 'Patrocinadores de Lemmy',
|
sponsors_of_lemmy: 'Patrocinadores de Lemmy',
|
||||||
sponsor_message: 'Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:',
|
sponsor_message:
|
||||||
|
'Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:',
|
||||||
support_on_patreon: 'Apoyo en Patreon',
|
support_on_patreon: 'Apoyo en Patreon',
|
||||||
general_sponsors:'Patrocinadores Generales son aquellos que señaron entre $10 y $39 a Lemmy.',
|
general_sponsors:
|
||||||
|
'Patrocinadores Generales son aquellos que señaron entre $10 y $39 a Lemmy.',
|
||||||
crypto: 'Crypto',
|
crypto: 'Crypto',
|
||||||
bitcoin: 'Bitcoin',
|
bitcoin: 'Bitcoin',
|
||||||
ethereum: 'Ethereum',
|
ethereum: 'Ethereum',
|
||||||
|
@ -151,7 +155,8 @@ export const es = {
|
||||||
yes: 'sí',
|
yes: 'sí',
|
||||||
no: 'no',
|
no: 'no',
|
||||||
powered_by: 'Impulsado por',
|
powered_by: 'Impulsado por',
|
||||||
landing_0: 'Lemmy es un <1>agregador de links</1> / alternativa a reddit, con la intención de funcionar en el <2>fediverso</2>.<3></3>Es alojable por uno mismo (sin necesidad de grandes compañías), tiene actualización en vivo de cadenas de comentarios, y es pequeño (<4>~80kB</4>). Federar con el sistema de redes ActivityPub forma parte de los objetivos del proyecto. <5></5>Esta es una <6>version beta muy prematura</6>, y actualmente muchas de las características están rotas o faltan. <7></7>Sugiere nuevas características o reporta errores <8>aquí</8>.<9></9>Hecho con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
landing_0:
|
||||||
|
'Lemmy es un <1>agregador de links</1> / alternativa a reddit, con la intención de funcionar en el <2>fediverso</2>.<3></3>Es alojable por uno mismo (sin necesidad de grandes compañías), tiene actualización en vivo de cadenas de comentarios, y es pequeño (<4>~80kB</4>). Federar con el sistema de redes ActivityPub forma parte de los objetivos del proyecto. <5></5>Esta es una <6>version beta muy prematura</6>, y actualmente muchas de las características están rotas o faltan. <7></7>Sugiere nuevas características o reporta errores <8>aquí</8>.<9></9>Hecho con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||||
not_logged_in: 'No has iniciado sesión.',
|
not_logged_in: 'No has iniciado sesión.',
|
||||||
community_ban: 'Has sido expulsado de esta comunidad.',
|
community_ban: 'Has sido expulsado de esta comunidad.',
|
||||||
site_ban: 'Has sido expulsado del sitio',
|
site_ban: 'Has sido expulsado del sitio',
|
||||||
|
@ -165,9 +170,12 @@ export const es = {
|
||||||
couldnt_find_community: 'No se pudo encontrar la comunidad.',
|
couldnt_find_community: 'No se pudo encontrar la comunidad.',
|
||||||
couldnt_update_community: 'No se pudo actualizar la comunidad.',
|
couldnt_update_community: 'No se pudo actualizar la comunidad.',
|
||||||
community_already_exists: 'Esta comunidad ya existe.',
|
community_already_exists: 'Esta comunidad ya existe.',
|
||||||
community_moderator_already_exists: 'Este moderador de la comunidad ya existe.',
|
community_moderator_already_exists:
|
||||||
community_follower_already_exists: 'Este seguidor de la comunidad ya existe.',
|
'Este moderador de la comunidad ya existe.',
|
||||||
community_user_already_banned: 'Este usuario de la comunidad ya fue expulsado.',
|
community_follower_already_exists:
|
||||||
|
'Este seguidor de la comunidad ya existe.',
|
||||||
|
community_user_already_banned:
|
||||||
|
'Este usuario de la comunidad ya fue expulsado.',
|
||||||
couldnt_create_post: 'No se pudo crear la publicación.',
|
couldnt_create_post: 'No se pudo crear la publicación.',
|
||||||
couldnt_like_post: 'No se pudo gustar la publicación.',
|
couldnt_like_post: 'No se pudo gustar la publicación.',
|
||||||
couldnt_find_post: 'No se pudo encontrar la publicación.',
|
couldnt_find_post: 'No se pudo encontrar la publicación.',
|
||||||
|
@ -178,13 +186,14 @@ export const es = {
|
||||||
not_an_admin: 'No es un administrador.',
|
not_an_admin: 'No es un administrador.',
|
||||||
site_already_exists: 'El sitio ya existe.',
|
site_already_exists: 'El sitio ya existe.',
|
||||||
couldnt_update_site: 'No se pudo actualizar el sitio.',
|
couldnt_update_site: 'No se pudo actualizar el sitio.',
|
||||||
couldnt_find_that_username_or_email: 'No se pudo encontrar ese nombre de usuario o correo electrónico.',
|
couldnt_find_that_username_or_email:
|
||||||
|
'No se pudo encontrar ese nombre de usuario o correo electrónico.',
|
||||||
password_incorrect: 'Contraseña incorrecta.',
|
password_incorrect: 'Contraseña incorrecta.',
|
||||||
passwords_dont_match: 'Las contraseñas no coinciden.',
|
passwords_dont_match: 'Las contraseñas no coinciden.',
|
||||||
admin_already_created: 'Lo sentimos, ya hay un adminisitrador.',
|
admin_already_created: 'Lo sentimos, ya hay un adminisitrador.',
|
||||||
user_already_exists: 'El usuario ya existe.',
|
user_already_exists: 'El usuario ya existe.',
|
||||||
couldnt_update_user: 'No se pudo actualizar el usuario.',
|
couldnt_update_user: 'No se pudo actualizar el usuario.',
|
||||||
system_err_login: 'Error del sistema. Intente cerrar sesión e ingresar de nuevo.',
|
system_err_login:
|
||||||
|
'Error del sistema. Intente cerrar sesión e ingresar de nuevo.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,21 +5,21 @@ export const fr = {
|
||||||
no_posts: 'Pas de sujets.',
|
no_posts: 'Pas de sujets.',
|
||||||
create_a_post: 'Créer un sujet',
|
create_a_post: 'Créer un sujet',
|
||||||
create_post: 'Créer le sujet',
|
create_post: 'Créer le sujet',
|
||||||
number_of_posts:'{{count}} Sujets',
|
number_of_posts: '{{count}} Sujets',
|
||||||
posts: 'Sujets',
|
posts: 'Sujets',
|
||||||
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: 'crossposter',
|
cross_post: 'crossposter',
|
||||||
comments: 'Commentaires',
|
comments: 'Commentaires',
|
||||||
number_of_comments:'{{count}} Commentaires',
|
number_of_comments: '{{count}} Commentaires',
|
||||||
remove_comment: 'Supprimer le commentaire',
|
remove_comment: 'Supprimer le commentaire',
|
||||||
communities: 'Communautés',
|
communities: 'Communautés',
|
||||||
users: 'Utilisateurs',
|
users: 'Utilisateurs',
|
||||||
create_a_community: 'Créer une communauté',
|
create_a_community: 'Créer une communauté',
|
||||||
create_community: 'Créer la communauté',
|
create_community: 'Créer la communauté',
|
||||||
remove_community: 'Supprimer la Communauté',
|
remove_community: 'Supprimer la Communauté',
|
||||||
subscribed_to_communities:'Abonné à ces <1>communautés</1>',
|
subscribed_to_communities: 'Abonné à ces <1>communautés</1>',
|
||||||
trending_communities:'<1>Communauté</1> en vogue',
|
trending_communities: '<1>Communauté</1> en vogue',
|
||||||
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.',
|
||||||
|
@ -29,8 +29,11 @@ export const fr = {
|
||||||
preview: 'prévisualiser',
|
preview: 'prévisualiser',
|
||||||
upload_image: 'téléverser une image',
|
upload_image: 'téléverser une image',
|
||||||
formatting_help: 'aide de formattage',
|
formatting_help: 'aide de formattage',
|
||||||
|
view_source: 'voir les sources',
|
||||||
unlock: 'débloquer',
|
unlock: 'débloquer',
|
||||||
lock: 'bloquer',
|
lock: 'bloquer',
|
||||||
|
sticky: 'épingler',
|
||||||
|
unsticky: 'désépingler',
|
||||||
link: 'lien',
|
link: 'lien',
|
||||||
mod: 'modérateur',
|
mod: 'modérateur',
|
||||||
mods: 'modérateurs',
|
mods: 'modérateurs',
|
||||||
|
@ -46,11 +49,15 @@ export const fr = {
|
||||||
remove: 'retirer',
|
remove: 'retirer',
|
||||||
removed: 'retiré',
|
removed: 'retiré',
|
||||||
locked: 'bloqué',
|
locked: 'bloqué',
|
||||||
|
stickied: 'épinglé',
|
||||||
reason: 'Raison',
|
reason: 'Raison',
|
||||||
mark_as_read: 'marquer comme lu',
|
mark_as_read: 'marquer comme lu',
|
||||||
mark_as_unread: 'marquer comme non-lu',
|
mark_as_unread: 'marquer comme non-lu',
|
||||||
delete: 'supprimer',
|
delete: 'supprimer',
|
||||||
deleted: 'supprimé',
|
deleted: 'supprimé',
|
||||||
|
delete_account: 'Supprimer le compte',
|
||||||
|
delete_account_confirm:
|
||||||
|
'Attention: cette action supprime toutes vos données de façons permanente. Entrez votre mot de passe pour confirmer.',
|
||||||
restore: 'restaurer',
|
restore: 'restaurer',
|
||||||
ban: 'bannir',
|
ban: 'bannir',
|
||||||
ban_from_site: 'bannir du site',
|
ban_from_site: 'bannir du site',
|
||||||
|
@ -60,11 +67,13 @@ export const fr = {
|
||||||
save: 'sauvegarder',
|
save: 'sauvegarder',
|
||||||
unsave: 'retirer',
|
unsave: 'retirer',
|
||||||
create: 'créer',
|
create: 'créer',
|
||||||
username: 'Nom d\'utilisateur',
|
creator: 'createur',
|
||||||
email_or_username: 'Email ou Nom d\'utilisateur',
|
username: "Nom d'utilisateur",
|
||||||
number_of_users:'{{count}} Utilisateurs',
|
email_or_username: "Email ou Nom d'utilisateur",
|
||||||
number_of_subscribers:'{{count}} Abonnés',
|
number_of_users: '{{count}} Utilisateurs',
|
||||||
number_of_points:'{{count}} Points',
|
number_of_subscribers: '{{count}} Abonnés',
|
||||||
|
number_of_points: '{{count}} Points',
|
||||||
|
number_online: '{{count}} Utilisateurs en ligne',
|
||||||
name: 'Nom',
|
name: 'Nom',
|
||||||
title: 'Titre',
|
title: 'Titre',
|
||||||
category: 'Catégorie',
|
category: 'Catégorie',
|
||||||
|
@ -72,7 +81,7 @@ export const fr = {
|
||||||
both: 'Les deux',
|
both: 'Les deux',
|
||||||
saved: 'Sauvegardé',
|
saved: 'Sauvegardé',
|
||||||
unsubscribe: 'Se désincrire',
|
unsubscribe: 'Se désincrire',
|
||||||
subscribe: 'S\'inscrire',
|
subscribe: "S'inscrire",
|
||||||
subscribed: 'Inscris',
|
subscribed: 'Inscris',
|
||||||
prev: 'Précédent',
|
prev: 'Précédent',
|
||||||
next: 'Suivant',
|
next: 'Suivant',
|
||||||
|
@ -97,10 +106,11 @@ export const fr = {
|
||||||
overview: 'Général',
|
overview: 'Général',
|
||||||
view: 'Voir',
|
view: 'Voir',
|
||||||
logout: 'Se déconnecter',
|
logout: 'Se déconnecter',
|
||||||
login_sign_up: 'Se connecter / S\'inscrire',
|
login_sign_up: "Se connecter / S'inscrire",
|
||||||
login: 'Se connecter',
|
login: 'Se connecter',
|
||||||
sign_up: 'S\'inscrire',
|
sign_up: "S'inscrire",
|
||||||
notifications_error: 'Les notifications de bureau ne sont pas discponibles sur votre navigateur. Essayez Firefox ou Chrome.',
|
notifications_error:
|
||||||
|
'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',
|
||||||
password: 'Mot de passe',
|
password: 'Mot de passe',
|
||||||
verify_password: 'Vérifiez le mot de passe',
|
verify_password: 'Vérifiez le mot de passe',
|
||||||
|
@ -112,22 +122,25 @@ export const fr = {
|
||||||
copy_suggested_title: 'Ajouter le titre suggéré: {{title}}',
|
copy_suggested_title: 'Ajouter le titre suggéré: {{title}}',
|
||||||
community: 'Communauté',
|
community: 'Communauté',
|
||||||
expand_here: 'Développer ici',
|
expand_here: 'Développer ici',
|
||||||
subscribe_to_communities: 'S\'abonner à quelques <1>communautés</1>.',
|
subscribe_to_communities: "S'abonner à quelques <1>communautés</1>.",
|
||||||
chat: 'Chat',
|
chat: 'Chat',
|
||||||
recent_comments: 'Commentaires récents',
|
recent_comments: 'Commentaires récents',
|
||||||
no_results: 'Pas de résultats.',
|
no_results: 'Pas de résultats.',
|
||||||
setup: 'Installation',
|
setup: 'Installation',
|
||||||
lemmy_instance_setup: 'Installation d\'une instance Lemmy',
|
lemmy_instance_setup: "Installation d'une instance Lemmy",
|
||||||
setup_admin: 'Créer un administrateur',
|
setup_admin: 'Créer un administrateur',
|
||||||
your_site: 'votre site',
|
your_site: 'votre site',
|
||||||
modified: 'modifié',
|
modified: 'modifié',
|
||||||
nsfw: 'Pas sûr pour le travail',
|
nsfw: 'Pas sûr pour le travail',
|
||||||
show_nsfw: 'Afficher le contenu NSFW',
|
show_nsfw: 'Afficher le contenu NSFW',
|
||||||
|
theme: 'Thème',
|
||||||
sponsors: 'Sponsors',
|
sponsors: 'Sponsors',
|
||||||
sponsors_of_lemmy: 'Sponsors de Lemmy',
|
sponsors_of_lemmy: 'Sponsors de Lemmy',
|
||||||
sponsor_message: 'Lemmy est gratuit et <1>open-source</1>, c\'est à dire sans publicité et sans monétisation. Pour toujours. Vos dons soutiennent directement le développement du projet. Merci à nos soutiens.',
|
sponsor_message:
|
||||||
|
"Lemmy est gratuit et <1>open-source</1>, c'est à dire sans publicité et sans monétisation. Pour toujours. Vos dons soutiennent directement le développement du projet. Merci à nos soutiens.",
|
||||||
support_on_patreon: 'Soutenir sur Patreon',
|
support_on_patreon: 'Soutenir sur Patreon',
|
||||||
general_sponsors:'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
general_sponsors:
|
||||||
|
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
||||||
crypto: 'Crypto',
|
crypto: 'Crypto',
|
||||||
bitcoin: 'Bitcoin',
|
bitcoin: 'Bitcoin',
|
||||||
ethereum: 'Ethereum',
|
ethereum: 'Ethereum',
|
||||||
|
@ -142,39 +155,44 @@ export const fr = {
|
||||||
yes: 'oui',
|
yes: 'oui',
|
||||||
no: 'non',
|
no: 'non',
|
||||||
powered_by: 'Propulsé par',
|
powered_by: 'Propulsé par',
|
||||||
landing_0: 'Lemmy est un <1>aggrégateur de lien</1>, similaire à reddit et conçu pour fonctionner sur le <2>fédiverse</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via Activitypub est prévue sur la feuille de route. <5></5>Lemmy est une <6>version beta très précoce</6>, et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez rapporter des bugs et suggérez de nouvelles fonctionnalités <8>ici.</8><9></9>Crée avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
landing_0:
|
||||||
not_logged_in: 'Vous n\'êtes pas connecté.',
|
'Lemmy est un <1>aggrégateur de lien</1>, similaire à reddit et conçu pour fonctionner sur le <2>fédiverse</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via Activitypub est prévue sur la feuille de route. <5></5>Lemmy est une <6>version beta très précoce</6>, et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez rapporter des bugs et suggérez de nouvelles fonctionnalités <8>ici.</8><9></9>Crée avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||||
|
not_logged_in: "Vous n'êtes pas connecté.",
|
||||||
community_ban: 'Vous avez été banni de cette communauté.',
|
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.',
|
||||||
no_comment_edit_allowed: 'Vous n\'êtes pas autorisé à éditer ce commentaire.',
|
no_comment_edit_allowed:
|
||||||
no_post_edit_allowed: 'ous n\'êtes pas autorisé à éditer sujet.',
|
"Vous n'êtes pas autorisé à éditer ce commentaire.",
|
||||||
no_community_edit_allowed: 'ous n\'êtes pas autorisé à éditer cette communauté.',
|
no_post_edit_allowed: "ous n'êtes pas autorisé à éditer sujet.",
|
||||||
|
no_community_edit_allowed:
|
||||||
|
"ous n'êtes pas autorisé à éditer cette communauté.",
|
||||||
couldnt_find_community: 'Impossible de trouver cette communauté.',
|
couldnt_find_community: 'Impossible de trouver cette communauté.',
|
||||||
couldnt_update_community: 'Impossible d\'éditer cette communauté.',
|
couldnt_update_community: "Impossible d'éditer cette communauté.",
|
||||||
community_already_exists: 'Cette communauté existe déjà.',
|
community_already_exists: 'Cette communauté existe déjà.',
|
||||||
community_moderator_already_exists: 'Ce membre est déjà modérateur.',
|
community_moderator_already_exists: 'Ce membre est déjà modérateur.',
|
||||||
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 dae créer le sujet.',
|
couldnt_create_post: 'Impossible dae créer le sujet.',
|
||||||
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",
|
||||||
couldnt_update_post: 'Impossible de mettre à jour le sujet',
|
couldnt_update_post: 'Impossible de mettre à jour le sujet',
|
||||||
couldnt_save_post: 'Impossible de sauvegarder le sujet.',
|
couldnt_save_post: 'Impossible de sauvegarder le sujet.',
|
||||||
no_slurs: 'Pas d\'insultes.',
|
no_slurs: "Pas d'insultes.",
|
||||||
not_an_admin: 'Pas administrateur.',
|
not_an_admin: 'Pas administrateur.',
|
||||||
site_already_exists: 'Le site existe déjà.',
|
site_already_exists: 'Le site existe déjà.',
|
||||||
couldnt_update_site: 'Impossible de mettre à jour le site.',
|
couldnt_update_site: 'Impossible de mettre à jour le site.',
|
||||||
couldnt_find_that_username_or_email: 'Impossible de trouver cet utilisateur ou cet email.',
|
couldnt_find_that_username_or_email:
|
||||||
|
'Impossible de trouver cet utilisateur ou cet email.',
|
||||||
password_incorrect: 'Mot de passe incorrect.',
|
password_incorrect: 'Mot de passe incorrect.',
|
||||||
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à.",
|
||||||
couldnt_update_user: 'Impossible de mettre à jour l\'utilisateur.',
|
couldnt_update_user: "Impossible de mettre à jour l'utilisateur.",
|
||||||
system_err_login: 'Erreur système. Essayez de vous déconneter puis de vous reconnecter.',
|
system_err_login:
|
||||||
|
'Erreur système. Essayez de vous déconneter puis de vous reconnecter.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -5,23 +5,23 @@ export const nl = {
|
||||||
no_posts: 'Geen posts.',
|
no_posts: 'Geen posts.',
|
||||||
create_a_post: 'Plaats een post',
|
create_a_post: 'Plaats een post',
|
||||||
create_post: 'Plaats post',
|
create_post: 'Plaats post',
|
||||||
number_of_posts:'{{count}} posts',
|
number_of_posts: '{{count}} posts',
|
||||||
posts: 'posts',
|
posts: 'posts',
|
||||||
related_posts: 'Deze posts kunnen gerelateerd zijn',
|
related_posts: 'Deze posts kunnen gerelateerd zijn',
|
||||||
cross_posts: 'Deze link is ook geplaatst in:',
|
cross_posts: 'Deze link is ook geplaatst in:',
|
||||||
cross_post: 'cross-post',
|
cross_post: 'cross-post',
|
||||||
comments: 'Reacties',
|
comments: 'Reacties',
|
||||||
number_of_comments:'{{count}} reacties',
|
number_of_comments: '{{count}} reacties',
|
||||||
remove_comment: 'Verwijder reactie',
|
remove_comment: 'Verwijder reactie',
|
||||||
communities: 'Communities',
|
communities: 'Communities',
|
||||||
users: 'Gebruikers',
|
users: 'Gebruikers',
|
||||||
create_a_community: 'Maak een community',
|
create_a_community: 'Maak een community',
|
||||||
create_community: 'Maak community',
|
create_community: 'Maak community',
|
||||||
remove_community: 'Verwijder community',
|
remove_community: 'Verwijder community',
|
||||||
subscribed_to_communities:'Geabonneerd op <1>communities</1>',
|
subscribed_to_communities: 'Geabonneerd op <1>communities</1>',
|
||||||
trending_communities:'Populaire <1>communities</1>',
|
trending_communities: 'Populaire <1>communities</1>',
|
||||||
list_of_communities: 'Lijst van communities',
|
list_of_communities: 'Lijst van communities',
|
||||||
number_of_communities:'{{count}} communities',
|
number_of_communities: '{{count}} communities',
|
||||||
community_reqs: 'kleine letters, onderstrepingsteken en geen spaties',
|
community_reqs: 'kleine letters, onderstrepingsteken en geen spaties',
|
||||||
edit: 'bewerk',
|
edit: 'bewerk',
|
||||||
reply: 'reageer',
|
reply: 'reageer',
|
||||||
|
@ -58,9 +58,9 @@ export const nl = {
|
||||||
create: 'maak',
|
create: 'maak',
|
||||||
username: 'Gebruikersnaam',
|
username: 'Gebruikersnaam',
|
||||||
email_or_username: 'E-mail of gebruikersnaam',
|
email_or_username: 'E-mail of gebruikersnaam',
|
||||||
number_of_users:'{{count}} gebruikers',
|
number_of_users: '{{count}} gebruikers',
|
||||||
number_of_subscribers:'{{count}} abonnees',
|
number_of_subscribers: '{{count}} abonnees',
|
||||||
number_of_points:'{{count}} punten',
|
number_of_points: '{{count}} punten',
|
||||||
name: 'Naam',
|
name: 'Naam',
|
||||||
title: 'Titel',
|
title: 'Titel',
|
||||||
category: 'Categorie',
|
category: 'Categorie',
|
||||||
|
@ -96,7 +96,8 @@ export const nl = {
|
||||||
login_sign_up: 'Log in / Aanmelden',
|
login_sign_up: 'Log in / Aanmelden',
|
||||||
login: 'Log in',
|
login: 'Log in',
|
||||||
sign_up: 'Aanmelden',
|
sign_up: 'Aanmelden',
|
||||||
notifications_error: 'Bureabladberichten niet beschikbaar in je browser. Probeer Firefox of Chrome.',
|
notifications_error:
|
||||||
|
'Bureabladberichten niet beschikbaar in je browser. Probeer Firefox of Chrome.',
|
||||||
unread_messages: 'Ongelezen berichten',
|
unread_messages: 'Ongelezen berichten',
|
||||||
password: 'Wachtwoord',
|
password: 'Wachtwoord',
|
||||||
verify_password: 'Herhaal wachtwoord',
|
verify_password: 'Herhaal wachtwoord',
|
||||||
|
@ -121,9 +122,11 @@ export const nl = {
|
||||||
show_nsfw: 'Laat NSFW-inhoud zien',
|
show_nsfw: 'Laat NSFW-inhoud zien',
|
||||||
sponsors: 'Sponsoren',
|
sponsors: 'Sponsoren',
|
||||||
sponsors_of_lemmy: 'Sponsoren van Lemmy',
|
sponsors_of_lemmy: 'Sponsoren van Lemmy',
|
||||||
sponsor_message: 'Lemmy is vrije, <1>open-source</1> software, dus zonder reclame, winstoogmerk en durfkapitaal, punt. Jouw donaties gaan direct naar de full-time-ontwikkeling van het project. Met veel dank aan de volgende mensen:',
|
sponsor_message:
|
||||||
|
'Lemmy is vrije, <1>open-source</1> software, dus zonder reclame, winstoogmerk en durfkapitaal, punt. Jouw donaties gaan direct naar de full-time-ontwikkeling van het project. Met veel dank aan de volgende mensen:',
|
||||||
support_on_patreon: 'Ondersteun op Patreon',
|
support_on_patreon: 'Ondersteun op Patreon',
|
||||||
general_sponsors:'Algemene sponsors zijn sponsors die tussen de $10 en $39 hebben gegeven aan Lemmy.',
|
general_sponsors:
|
||||||
|
'Algemene sponsors zijn sponsors die tussen de $10 en $39 hebben gegeven aan Lemmy.',
|
||||||
crypto: 'Cryptovaluta',
|
crypto: 'Cryptovaluta',
|
||||||
bitcoin: 'Bitcoin',
|
bitcoin: 'Bitcoin',
|
||||||
ethereum: 'Ethereum',
|
ethereum: 'Ethereum',
|
||||||
|
@ -138,7 +141,8 @@ export const nl = {
|
||||||
yes: 'ja',
|
yes: 'ja',
|
||||||
no: 'nee',
|
no: 'nee',
|
||||||
powered_by: 'Mogelijk gemaakt door',
|
powered_by: 'Mogelijk gemaakt door',
|
||||||
landing_0: 'Lemmy is een <1>linkverzameler</1> / reddit-alternatief, bedoeld om in de <2>fediverse</2> te werken.<3></3>Lemmy kan door om het even wie gehost worden, heeft live-bijgewerkte reacties en is superklein (<4>ca. 80 kB</4>). Federatie in hte ActivityPub-netwerk is gepland. <5></5>Dit is een <6>erg vroege bèta-versie</6>, en een hoop functies zijn stuk of afwezig. <7></7>Stel nieuwe functies voor of meldt fouten <8>hier</8>.<9></9>Gemaakt met <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> en <13>Typescript</13>.',
|
landing_0:
|
||||||
|
'Lemmy is een <1>linkverzameler</1> / reddit-alternatief, bedoeld om in de <2>fediverse</2> te werken.<3></3>Lemmy kan door om het even wie gehost worden, heeft live-bijgewerkte reacties en is superklein (<4>ca. 80 kB</4>). Federatie in hte ActivityPub-netwerk is gepland. <5></5>Dit is een <6>erg vroege bèta-versie</6>, en een hoop functies zijn stuk of afwezig. <7></7>Stel nieuwe functies voor of meldt fouten <8>hier</8>.<9></9>Gemaakt met <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> en <13>Typescript</13>.',
|
||||||
not_logged_in: 'Niet ingelogd.',
|
not_logged_in: 'Niet ingelogd.',
|
||||||
community_ban: 'Je bent verbannen uit deze community.',
|
community_ban: 'Je bent verbannen uit deze community.',
|
||||||
site_ban: 'Je bent verbannen van deze site.',
|
site_ban: 'Je bent verbannen van deze site.',
|
||||||
|
@ -165,12 +169,14 @@ export const nl = {
|
||||||
not_an_admin: 'Niet een beheerder.',
|
not_an_admin: 'Niet een beheerder.',
|
||||||
site_already_exists: 'Site bestaat al.',
|
site_already_exists: 'Site bestaat al.',
|
||||||
couldnt_update_site: 'Kon site niet bijwerken.',
|
couldnt_update_site: 'Kon site niet bijwerken.',
|
||||||
couldnt_find_that_username_or_email: 'Kon gebruikersnaam of e-mailadres niet vinden.',
|
couldnt_find_that_username_or_email:
|
||||||
|
'Kon gebruikersnaam of e-mailadres niet vinden.',
|
||||||
password_incorrect: 'Wachtwoord incorrect.',
|
password_incorrect: 'Wachtwoord incorrect.',
|
||||||
passwords_dont_match: 'Wachtwoorden zijn niet gelijk.',
|
passwords_dont_match: 'Wachtwoorden zijn niet gelijk.',
|
||||||
admin_already_created: 'Sorry, er is al een beheerder.',
|
admin_already_created: 'Sorry, er is al een beheerder.',
|
||||||
user_already_exists: 'Gebruiker bestaat al.',
|
user_already_exists: 'Gebruiker bestaat al.',
|
||||||
couldnt_update_user: 'Kon gebruiker niet bijwerken.',
|
couldnt_update_user: 'Kon gebruiker niet bijwerken.',
|
||||||
system_err_login: 'Systeemfout. Probeer uit te loggen en weer in te loggen.',
|
system_err_login:
|
||||||
|
'Systeemfout. Probeer uit te loggen en weer in te loggen.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -5,19 +5,19 @@ export const ru = {
|
||||||
no_posts: 'Нет записей.',
|
no_posts: 'Нет записей.',
|
||||||
create_a_post: 'Создать запись',
|
create_a_post: 'Создать запись',
|
||||||
create_post: 'Создать запись',
|
create_post: 'Создать запись',
|
||||||
number_of_posts:'{{count}} записей',
|
number_of_posts: '{{count}} записей',
|
||||||
posts: 'Записи',
|
posts: 'Записи',
|
||||||
related_posts: 'Эти записи могут быть связаны',
|
related_posts: 'Эти записи могут быть связаны',
|
||||||
comments: 'Комментарии',
|
comments: 'Комментарии',
|
||||||
number_of_comments:'{{count}} комментариев',
|
number_of_comments: '{{count}} комментариев',
|
||||||
remove_comment: 'Удалить комментарий',
|
remove_comment: 'Удалить комментарий',
|
||||||
communities: 'Сообщества',
|
communities: 'Сообщества',
|
||||||
users: 'Пользователи',
|
users: 'Пользователи',
|
||||||
create_a_community: 'Создать сообщество',
|
create_a_community: 'Создать сообщество',
|
||||||
create_community: 'Создать сообщество',
|
create_community: 'Создать сообщество',
|
||||||
remove_community: 'Удалить сообщество',
|
remove_community: 'Удалить сообщество',
|
||||||
subscribed_to_communities:'Подписаны на <1>сообщества</1>',
|
subscribed_to_communities: 'Подписаны на <1>сообщества</1>',
|
||||||
trending_communities:'<1>Сообщества</1> в тренде',
|
trending_communities: '<1>Сообщества</1> в тренде',
|
||||||
list_of_communities: 'Список сообществ',
|
list_of_communities: 'Список сообществ',
|
||||||
community_reqs: 'строчными буквами, подчеркиваниями и без пробелов.',
|
community_reqs: 'строчными буквами, подчеркиваниями и без пробелов.',
|
||||||
edit: 'редактировать',
|
edit: 'редактировать',
|
||||||
|
@ -55,9 +55,9 @@ export const ru = {
|
||||||
create: 'создать',
|
create: 'создать',
|
||||||
username: 'Имя пользователя',
|
username: 'Имя пользователя',
|
||||||
email_or_username: 'Электронная почта или имя пользователя',
|
email_or_username: 'Электронная почта или имя пользователя',
|
||||||
number_of_users:'{{count}} пользователей',
|
number_of_users: '{{count}} пользователей',
|
||||||
number_of_subscribers:'{{count}} подписчиков',
|
number_of_subscribers: '{{count}} подписчиков',
|
||||||
number_of_points:'{{count}} баллов',
|
number_of_points: '{{count}} баллов',
|
||||||
name: 'Имя',
|
name: 'Имя',
|
||||||
title: 'Название',
|
title: 'Название',
|
||||||
category: 'Категория',
|
category: 'Категория',
|
||||||
|
@ -93,7 +93,8 @@ export const ru = {
|
||||||
login_sign_up: 'Войти / Регистрация',
|
login_sign_up: 'Войти / Регистрация',
|
||||||
login: 'Авторизация',
|
login: 'Авторизация',
|
||||||
sign_up: 'Регистрация',
|
sign_up: 'Регистрация',
|
||||||
notifications_error: 'Уведомления на рабочем столе недоступны в вашем браузере. Попробуйте Firefox или Chrome.',
|
notifications_error:
|
||||||
|
'Уведомления на рабочем столе недоступны в вашем браузере. Попробуйте Firefox или Chrome.',
|
||||||
unread_messages: 'Непрочитанные сообщения',
|
unread_messages: 'Непрочитанные сообщения',
|
||||||
password: 'Пароль',
|
password: 'Пароль',
|
||||||
verify_password: 'Повторите пароль',
|
verify_password: 'Повторите пароль',
|
||||||
|
@ -117,16 +118,19 @@ export const ru = {
|
||||||
show_nsfw: 'Показывать NSFW-контент',
|
show_nsfw: 'Показывать NSFW-контент',
|
||||||
sponsors: 'Спонсоры',
|
sponsors: 'Спонсоры',
|
||||||
sponsors_of_lemmy: 'Спонсоры Lemmy',
|
sponsors_of_lemmy: 'Спонсоры Lemmy',
|
||||||
sponsor_message: 'Lemmy это бесплатное, <1>открытое</1> программное обеспечение, что означает отсутствие рекламы, монетизации или венчурного капитала, никогда. Ваши пожертвования напрямую поддерживают развитие проекта. Спасибо нижеуказанным людям:',
|
sponsor_message:
|
||||||
|
'Lemmy это бесплатное, <1>открытое</1> программное обеспечение, что означает отсутствие рекламы, монетизации или венчурного капитала, никогда. Ваши пожертвования напрямую поддерживают развитие проекта. Спасибо нижеуказанным людям:',
|
||||||
support_on_patreon: 'Поддержать на Patreon',
|
support_on_patreon: 'Поддержать на Patreon',
|
||||||
general_sponsors:'Генеральные спонсоры - это те, кто пообещал Lemmy от $10 до $39.',
|
general_sponsors:
|
||||||
|
'Генеральные спонсоры - это те, кто пообещал Lemmy от $10 до $39.',
|
||||||
crypto: 'Крипто',
|
crypto: 'Крипто',
|
||||||
bitcoin: 'Bitcoin',
|
bitcoin: 'Bitcoin',
|
||||||
ethereum: 'Ethereum',
|
ethereum: 'Ethereum',
|
||||||
code: 'Код',
|
code: 'Код',
|
||||||
joined: 'Присоединился',
|
joined: 'Присоединился',
|
||||||
powered_by: 'Работает на',
|
powered_by: 'Работает на',
|
||||||
landing_0: 'Lemmy - это <1>агрегатор ссылок</1> / альтернатива reddit, предназначенный для работы в <2>федиверсе</2>.<3></3>Это самодостаточная система, с обновляемыми комментариями, и эта система крошечная (<4>~80 Кб</4>). Федерация в сети ActivityPub находится в разработке. <5></5>Это <6>очень ранняя бета-версия</6>, и многие функции в настоящее время сломаны или отсутствуют. <7></7>Предлагать новые функции или сообщать об ошибках можно <8>здесь.</8><9></9>Сделано на <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
landing_0:
|
||||||
|
'Lemmy - это <1>агрегатор ссылок</1> / альтернатива reddit, предназначенный для работы в <2>федиверсе</2>.<3></3>Это самодостаточная система, с обновляемыми комментариями, и эта система крошечная (<4>~80 Кб</4>). Федерация в сети ActivityPub находится в разработке. <5></5>Это <6>очень ранняя бета-версия</6>, и многие функции в настоящее время сломаны или отсутствуют. <7></7>Предлагать новые функции или сообщать об ошибках можно <8>здесь.</8><9></9>Сделано на <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||||
not_logged_in: 'Не авторизованы.',
|
not_logged_in: 'Не авторизованы.',
|
||||||
community_ban: 'Вы были заблокированы на данном сообществе.',
|
community_ban: 'Вы были заблокированы на данном сообществе.',
|
||||||
site_ban: 'Вы были заблокированы на данном сайте',
|
site_ban: 'Вы были заблокированы на данном сайте',
|
||||||
|
@ -153,13 +157,14 @@ export const ru = {
|
||||||
not_an_admin: 'Не администратор.',
|
not_an_admin: 'Не администратор.',
|
||||||
site_already_exists: 'Сайт уже существует.',
|
site_already_exists: 'Сайт уже существует.',
|
||||||
couldnt_update_site: 'Не получилось обновить сайт.',
|
couldnt_update_site: 'Не получилось обновить сайт.',
|
||||||
couldnt_find_that_username_or_email: 'Не получилось найти данное имя пользователя или электронную почту.',
|
couldnt_find_that_username_or_email:
|
||||||
|
'Не получилось найти данное имя пользователя или электронную почту.',
|
||||||
password_incorrect: 'Неверный пароль.',
|
password_incorrect: 'Неверный пароль.',
|
||||||
passwords_dont_match: 'Пароли не совпадают.',
|
passwords_dont_match: 'Пароли не совпадают.',
|
||||||
admin_already_created: 'Извините, уже есть администратор.',
|
admin_already_created: 'Извините, уже есть администратор.',
|
||||||
user_already_exists: 'Пользователь уже существует.',
|
user_already_exists: 'Пользователь уже существует.',
|
||||||
couldnt_update_user: 'Не получилось обновить пользователя.',
|
couldnt_update_user: 'Не получилось обновить пользователя.',
|
||||||
system_err_login: 'Системная ошибка. Попробуйте выйти из системы и вернуться обратно.',
|
system_err_login:
|
||||||
|
'Системная ошибка. Попробуйте выйти из системы и вернуться обратно.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,8 @@ export const sv = {
|
||||||
delete: 'radera',
|
delete: 'radera',
|
||||||
deleted: 'raderad',
|
deleted: 'raderad',
|
||||||
delete_account: 'Ta bort konto',
|
delete_account: 'Ta bort konto',
|
||||||
delete_account_confirm: 'Varning: den här åtgärden kommer radera alla dina data permanent. Är du säker?',
|
delete_account_confirm:
|
||||||
|
'Varning: den här åtgärden kommer radera alla dina data permanent. Är du säker?',
|
||||||
restore: 'återställ',
|
restore: 'återställ',
|
||||||
ban: 'blockera',
|
ban: 'blockera',
|
||||||
ban_from_site: 'blockera från webbplats',
|
ban_from_site: 'blockera från webbplats',
|
||||||
|
@ -108,7 +109,8 @@ export const sv = {
|
||||||
login_sign_up: 'Logga in eller skapa konto',
|
login_sign_up: 'Logga in eller skapa konto',
|
||||||
login: 'Logga in',
|
login: 'Logga in',
|
||||||
sign_up: 'Skapa konto',
|
sign_up: 'Skapa konto',
|
||||||
notifications_error: 'Din webbläsare har inte stöd för skrivbordsaviseringar. Testa Firefox eller Chrome.',
|
notifications_error:
|
||||||
|
'Din webbläsare har inte stöd för skrivbordsaviseringar. Testa Firefox eller Chrome.',
|
||||||
unread_messages: 'Olästa meddelanden',
|
unread_messages: 'Olästa meddelanden',
|
||||||
password: 'Lösenord',
|
password: 'Lösenord',
|
||||||
verify_password: 'Bekräfta lösenord',
|
verify_password: 'Bekräfta lösenord',
|
||||||
|
@ -134,9 +136,11 @@ export const sv = {
|
||||||
theme: 'Utseende',
|
theme: 'Utseende',
|
||||||
sponsors: 'Sponsorer',
|
sponsors: 'Sponsorer',
|
||||||
sponsors_of_lemmy: 'Lemmys sponsorer',
|
sponsors_of_lemmy: 'Lemmys sponsorer',
|
||||||
sponsor_message: 'Lemmy är fri mjukvara med <1>öppen källkod</1>, vilket innebär att ingen reklam, vinstindrivning eller venturekapital förekommer, någonsin. Dina donationer går direkt till att stöda utvecklingen av projektet. Stort tack till följande personer:',
|
sponsor_message:
|
||||||
|
'Lemmy är fri mjukvara med <1>öppen källkod</1>, vilket innebär att ingen reklam, vinstindrivning eller venturekapital förekommer, någonsin. Dina donationer går direkt till att stöda utvecklingen av projektet. Stort tack till följande personer:',
|
||||||
support_on_patreon: 'Stöd på Patreon',
|
support_on_patreon: 'Stöd på Patreon',
|
||||||
general_sponsors: 'Allmänna sponsorer är dem som givit mellan 10 och 39\u00a0dollar till Lemmy.',
|
general_sponsors:
|
||||||
|
'Allmänna sponsorer är dem som givit mellan 10 och 39\u00a0dollar till Lemmy.',
|
||||||
crypto: 'Kryptovaluta',
|
crypto: 'Kryptovaluta',
|
||||||
bitcoin: 'Bitcoin',
|
bitcoin: 'Bitcoin',
|
||||||
ethereum: 'Ethereum',
|
ethereum: 'Ethereum',
|
||||||
|
@ -151,7 +155,8 @@ export const sv = {
|
||||||
yes: 'ja',
|
yes: 'ja',
|
||||||
no: 'nej',
|
no: 'nej',
|
||||||
powered_by: 'Drivs av',
|
powered_by: 'Drivs av',
|
||||||
landing_0: 'Lemmy är en <1>länksamlare</1> och alternativ till reddit, ämnad att fungera i <2>Fediversumet</2>.<3></3>Lemmy kan drivas av vem som helst, har kommentarstrådar som updateras i realid och är mycket liten (<4>ca 80\u00a0kB</4>). Federering med ActivityPub-nätverket är planerat. <5></5>Detta är en <6>väldigt tidig betaversion</6> och många funktioner saknas därför eller är trasiga.<7></7>Föreslå nya funktioner eller anmäl buggar <8>här</8>.<9></9>Skapad i <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> och <13>Typescript</13>.',
|
landing_0:
|
||||||
|
'Lemmy är en <1>länksamlare</1> och alternativ till reddit, ämnad att fungera i <2>Fediversumet</2>.<3></3>Lemmy kan drivas av vem som helst, har kommentarstrådar som updateras i realid och är mycket liten (<4>ca 80\u00a0kB</4>). Federering med ActivityPub-nätverket är planerat. <5></5>Detta är en <6>väldigt tidig betaversion</6> och många funktioner saknas därför eller är trasiga.<7></7>Föreslå nya funktioner eller anmäl buggar <8>här</8>.<9></9>Skapad i <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> och <13>Typescript</13>.',
|
||||||
not_logged_in: 'Inte inloggad.',
|
not_logged_in: 'Inte inloggad.',
|
||||||
community_ban: 'Du har blockerats från den här gemenskapen.',
|
community_ban: 'Du har blockerats från den här gemenskapen.',
|
||||||
site_ban: 'Du har blockerats från webbplatsen.',
|
site_ban: 'Du har blockerats från webbplatsen.',
|
||||||
|
@ -178,7 +183,8 @@ export const sv = {
|
||||||
not_an_admin: 'Inte en administratör.',
|
not_an_admin: 'Inte en administratör.',
|
||||||
site_already_exists: 'Webbplatsen finns redan.',
|
site_already_exists: 'Webbplatsen finns redan.',
|
||||||
couldnt_update_site: 'Kunde inte uppdatera webbplats.',
|
couldnt_update_site: 'Kunde inte uppdatera webbplats.',
|
||||||
couldnt_find_that_username_or_email: 'Kunde inte hitta det användarnamnet eller e-postadressen.',
|
couldnt_find_that_username_or_email:
|
||||||
|
'Kunde inte hitta det användarnamnet eller e-postadressen.',
|
||||||
password_incorrect: 'Ogiltigt lösenord.',
|
password_incorrect: 'Ogiltigt lösenord.',
|
||||||
passwords_dont_match: 'Lösenorden stämmer inte överens.',
|
passwords_dont_match: 'Lösenorden stämmer inte överens.',
|
||||||
admin_already_created: 'Beklagar, men det finns redan en administratör.',
|
admin_already_created: 'Beklagar, men det finns redan en administratör.',
|
||||||
|
@ -186,4 +192,4 @@ export const sv = {
|
||||||
couldnt_update_user: 'Kunde inte uppdatera användare.',
|
couldnt_update_user: 'Kunde inte uppdatera användare.',
|
||||||
system_err_login: 'Systemfel. Försök att logga ut och sedan in igen.',
|
system_err_login: 'Systemfel. Försök att logga ut och sedan in igen.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -5,18 +5,18 @@ export const zh = {
|
||||||
no_posts: '没有帖子.',
|
no_posts: '没有帖子.',
|
||||||
create_a_post: '创建新帖子',
|
create_a_post: '创建新帖子',
|
||||||
create_post: '创建帖子',
|
create_post: '创建帖子',
|
||||||
number_of_posts:'{{count}} 帖子',
|
number_of_posts: '{{count}} 帖子',
|
||||||
posts: '帖子',
|
posts: '帖子',
|
||||||
related_posts: '相关的帖子',
|
related_posts: '相关的帖子',
|
||||||
comments: '评论',
|
comments: '评论',
|
||||||
number_of_comments:'{{count}} 评论',
|
number_of_comments: '{{count}} 评论',
|
||||||
remove_comment: '移除评论',
|
remove_comment: '移除评论',
|
||||||
communities: '节点',
|
communities: '节点',
|
||||||
create_a_community: '创建新节点',
|
create_a_community: '创建新节点',
|
||||||
create_community: '创建节点',
|
create_community: '创建节点',
|
||||||
remove_community: '移除节点',
|
remove_community: '移除节点',
|
||||||
subscribed_to_communities:'订阅新 <1>节点</1>',
|
subscribed_to_communities: '订阅新 <1>节点</1>',
|
||||||
trending_communities:'<1>节点</1>趋势',
|
trending_communities: '<1>节点</1>趋势',
|
||||||
list_of_communities: '节点列表',
|
list_of_communities: '节点列表',
|
||||||
community_reqs: '包含小写与下划线且没有空格的字符串.',
|
community_reqs: '包含小写与下划线且没有空格的字符串.',
|
||||||
edit: '编辑',
|
edit: '编辑',
|
||||||
|
@ -53,9 +53,9 @@ export const zh = {
|
||||||
create: '创建',
|
create: '创建',
|
||||||
username: '用户名',
|
username: '用户名',
|
||||||
email_or_username: '邮箱或用户名',
|
email_or_username: '邮箱或用户名',
|
||||||
number_of_users:'{{count}} 用户',
|
number_of_users: '{{count}} 用户',
|
||||||
number_of_subscribers:'{{count}} 订阅',
|
number_of_subscribers: '{{count}} 订阅',
|
||||||
number_of_points:'{{count}} 分',
|
number_of_points: '{{count}} 分',
|
||||||
name: '名字',
|
name: '名字',
|
||||||
title: '标题',
|
title: '标题',
|
||||||
category: '分类',
|
category: '分类',
|
||||||
|
@ -113,16 +113,19 @@ export const zh = {
|
||||||
modified: '修改',
|
modified: '修改',
|
||||||
sponsors: 'Sponsors',
|
sponsors: 'Sponsors',
|
||||||
sponsors_of_lemmy: 'Sponsors of Lemmy',
|
sponsors_of_lemmy: 'Sponsors of Lemmy',
|
||||||
sponsor_message: 'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
|
sponsor_message:
|
||||||
|
'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
|
||||||
support_on_patreon: 'Support on Patreon',
|
support_on_patreon: 'Support on Patreon',
|
||||||
general_sponsors:'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
general_sponsors:
|
||||||
|
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
||||||
crypto: '加密',
|
crypto: '加密',
|
||||||
bitcoin: '比特币',
|
bitcoin: '比特币',
|
||||||
ethereum: '以太币',
|
ethereum: '以太币',
|
||||||
code: '代码',
|
code: '代码',
|
||||||
joined: '已加入',
|
joined: '已加入',
|
||||||
powered_by: '保留所有权利',
|
powered_by: '保留所有权利',
|
||||||
landing_0: 'Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It\'s self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
landing_0:
|
||||||
|
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
|
||||||
not_logged_in: '未登录.',
|
not_logged_in: '未登录.',
|
||||||
community_ban: '你被此节点禁止.',
|
community_ban: '你被此节点禁止.',
|
||||||
site_ban: '你被此站点禁止',
|
site_ban: '你被此站点禁止',
|
||||||
|
@ -157,5 +160,4 @@ export const zh = {
|
||||||
couldnt_update_user: '不可以更新用户.',
|
couldnt_update_user: '不可以更新用户.',
|
||||||
system_err_login: '系统错误. 尝试注销再登录',
|
system_err_login: '系统错误. 尝试注销再登录',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,14 @@ import 'moment/locale/ru';
|
||||||
import 'moment/locale/nl';
|
import 'moment/locale/nl';
|
||||||
import 'moment/locale/it';
|
import 'moment/locale/it';
|
||||||
|
|
||||||
import { UserOperation, Comment, User, SortType, ListingType, SearchType } from './interfaces';
|
import {
|
||||||
|
UserOperation,
|
||||||
|
Comment,
|
||||||
|
User,
|
||||||
|
SortType,
|
||||||
|
ListingType,
|
||||||
|
SearchType,
|
||||||
|
} from './interfaces';
|
||||||
import * as markdown_it from 'markdown-it';
|
import * as markdown_it from 'markdown-it';
|
||||||
import * as markdownitEmoji from 'markdown-it-emoji/light';
|
import * as markdownitEmoji from 'markdown-it-emoji/light';
|
||||||
import * as markdown_it_container from 'markdown-it-container';
|
import * as markdown_it_container from 'markdown-it-container';
|
||||||
|
@ -18,11 +25,16 @@ import * as emojiShortName from 'emoji-short-name';
|
||||||
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 = 'https://commonmark.org/help/';
|
||||||
|
|
||||||
export const postRefetchSeconds: number = 60*1000;
|
export const postRefetchSeconds: number = 60 * 1000;
|
||||||
export const fetchLimit: number = 20;
|
export const fetchLimit: number = 20;
|
||||||
export const mentionDropdownFetchLimit = 6;
|
export const mentionDropdownFetchLimit = 6;
|
||||||
|
|
||||||
export function randomStr() {return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10)}
|
export function randomStr() {
|
||||||
|
return Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.replace(/[^a-z]+/g, '')
|
||||||
|
.substr(2, 10);
|
||||||
|
}
|
||||||
|
|
||||||
export function msgOp(msg: any): UserOperation {
|
export function msgOp(msg: any): UserOperation {
|
||||||
let opStr: string = msg.op;
|
let opStr: string = msg.op;
|
||||||
|
@ -32,27 +44,30 @@ export function msgOp(msg: any): UserOperation {
|
||||||
export const md = new markdown_it({
|
export const md = new markdown_it({
|
||||||
html: false,
|
html: false,
|
||||||
linkify: true,
|
linkify: true,
|
||||||
typographer: true
|
typographer: true,
|
||||||
}).use(markdown_it_container, 'spoiler', {
|
})
|
||||||
validate: function(params: any) {
|
.use(markdown_it_container, 'spoiler', {
|
||||||
return params.trim().match(/^spoiler\s+(.*)$/);
|
validate: function(params: any) {
|
||||||
},
|
return params.trim().match(/^spoiler\s+(.*)$/);
|
||||||
|
},
|
||||||
|
|
||||||
render: function (tokens: any, idx: any) {
|
render: function(tokens: any, idx: any) {
|
||||||
var m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/);
|
var m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/);
|
||||||
|
|
||||||
if (tokens[idx].nesting === 1) {
|
if (tokens[idx].nesting === 1) {
|
||||||
// opening tag
|
// opening tag
|
||||||
return '<details><summary>' + md.utils.escapeHtml(m[1]) + '</summary>\n';
|
return (
|
||||||
|
'<details><summary>' + md.utils.escapeHtml(m[1]) + '</summary>\n'
|
||||||
} else {
|
);
|
||||||
// closing tag
|
} else {
|
||||||
return '</details>\n';
|
// closing tag
|
||||||
}
|
return '</details>\n';
|
||||||
}
|
}
|
||||||
}).use(markdownitEmoji, {
|
},
|
||||||
defs: objectFlip(emojiShortName)
|
})
|
||||||
});
|
.use(markdownitEmoji, {
|
||||||
|
defs: objectFlip(emojiShortName),
|
||||||
|
});
|
||||||
|
|
||||||
md.renderer.rules.emoji = function(token, idx) {
|
md.renderer.rules.emoji = function(token, idx) {
|
||||||
return twemoji.parse(token[idx].content);
|
return twemoji.parse(token[idx].content);
|
||||||
|
@ -65,7 +80,9 @@ export function hotRank(comment: Comment): number {
|
||||||
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 = (10000 * Math.log10(Math.max(1, 3 + comment.score))) / Math.pow(hoursElapsed + 2, 1.8);
|
let rank =
|
||||||
|
(10000 * Math.log10(Math.max(1, 3 + comment.score))) /
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
@ -73,26 +90,36 @@ export function hotRank(comment: Comment): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mdToHtml(text: string) {
|
export function mdToHtml(text: string) {
|
||||||
return {__html: md.render(text)};
|
return { __html: md.render(text) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUnixTime(text: string): number {
|
export function getUnixTime(text: string): number {
|
||||||
return text ? new Date(text).getTime()/1000 : undefined;
|
return text ? new Date(text).getTime() / 1000 : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addTypeInfo<T>(arr: Array<T>, name: string): Array<{type_: string, data: T}> {
|
export function addTypeInfo<T>(
|
||||||
return arr.map(e => {return {type_: name, data: e}});
|
arr: Array<T>,
|
||||||
|
name: string
|
||||||
|
): Array<{ type_: string; data: T }> {
|
||||||
|
return arr.map(e => {
|
||||||
|
return { type_: name, data: e };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canMod(user: User, modIds: Array<number>, creator_id: number, onSelf: boolean = false): boolean {
|
export function canMod(
|
||||||
|
user: User,
|
||||||
|
modIds: Array<number>,
|
||||||
|
creator_id: number,
|
||||||
|
onSelf: boolean = false
|
||||||
|
): boolean {
|
||||||
// You can do moderator actions only on the mods added after you.
|
// You can do moderator actions only on the mods added after you.
|
||||||
if (user) {
|
if (user) {
|
||||||
let yourIndex = modIds.findIndex(id => id == user.id);
|
let yourIndex = modIds.findIndex(id => id == user.id);
|
||||||
if (yourIndex == -1) {
|
if (yourIndex == -1) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// onSelf +1 on mod actions not for yourself, IE ban, remove, etc
|
// onSelf +1 on mod actions not for yourself, IE ban, remove, etc
|
||||||
modIds = modIds.slice(0, yourIndex+(onSelf ? 0 : 1));
|
modIds = modIds.slice(0, yourIndex + (onSelf ? 0 : 1));
|
||||||
return !modIds.includes(creator_id);
|
return !modIds.includes(creator_id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -104,8 +131,9 @@ export function isMod(modIds: Array<number>, creator_id: number): boolean {
|
||||||
return modIds.includes(creator_id);
|
return modIds.includes(creator_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var imageRegex = new RegExp(
|
||||||
var imageRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:png|jpg|jpeg|gif|png|svg))`);
|
`(http)?s?:?(\/\/[^"']*\.(?:png|jpg|jpeg|gif|png|svg))`
|
||||||
|
);
|
||||||
var videoRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:mp4))`);
|
var videoRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:mp4))`);
|
||||||
|
|
||||||
export function isImage(url: string) {
|
export function isImage(url: string) {
|
||||||
|
@ -128,7 +156,6 @@ export function capitalizeFirstLetter(str: string): string {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function routeSortTypeToEnum(sort: string): SortType {
|
export function routeSortTypeToEnum(sort: string): SortType {
|
||||||
if (sort == 'new') {
|
if (sort == 'new') {
|
||||||
return SortType.New;
|
return SortType.New;
|
||||||
|
@ -140,6 +167,8 @@ export function routeSortTypeToEnum(sort: string): SortType {
|
||||||
return SortType.TopWeek;
|
return SortType.TopWeek;
|
||||||
} else if (sort == 'topmonth') {
|
} else if (sort == 'topmonth') {
|
||||||
return SortType.TopMonth;
|
return SortType.TopMonth;
|
||||||
|
} else if (sort == 'topyear') {
|
||||||
|
return SortType.TopYear;
|
||||||
} else if (sort == 'topall') {
|
} else if (sort == 'topall') {
|
||||||
return SortType.TopAll;
|
return SortType.TopAll;
|
||||||
}
|
}
|
||||||
|
@ -159,7 +188,11 @@ export async function getPageTitle(url: string) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function debounce(func: any, wait: number = 500, immediate: boolean = false) {
|
export function debounce(
|
||||||
|
func: any,
|
||||||
|
wait: number = 500,
|
||||||
|
immediate: boolean = false
|
||||||
|
) {
|
||||||
// 'private' variable for instance
|
// 'private' variable for instance
|
||||||
// The returned function will be able to reference this due to closure.
|
// The returned function will be able to reference this due to closure.
|
||||||
// Each call to the returned function will share this common timer.
|
// Each call to the returned function will share this common timer.
|
||||||
|
@ -169,46 +202,45 @@ export function debounce(func: any, wait: number = 500, immediate: boolean = fal
|
||||||
return function() {
|
return function() {
|
||||||
// reference the context and args for the setTimeout function
|
// reference the context and args for the setTimeout function
|
||||||
var context = this,
|
var context = this,
|
||||||
args = arguments;
|
args = arguments;
|
||||||
|
|
||||||
// Should the function be called now? If immediate is true
|
// Should the function be called now? If immediate is true
|
||||||
// and not already in a timeout then the answer is: Yes
|
// and not already in a timeout then the answer is: Yes
|
||||||
var callNow = immediate && !timeout;
|
var callNow = immediate && !timeout;
|
||||||
|
|
||||||
// This is the basic debounce behaviour where you can call this
|
// This is the basic debounce behaviour where you can call this
|
||||||
// function several times, but it will only execute once
|
// function several times, but it will only execute once
|
||||||
// [before or after imposing a delay].
|
// [before or after imposing a delay].
|
||||||
// Each time the returned function is called, the timer starts over.
|
// Each time the returned function is called, the timer starts over.
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
|
||||||
// Set the new timeout
|
// Set the new timeout
|
||||||
timeout = setTimeout(function() {
|
timeout = setTimeout(function() {
|
||||||
|
// Inside the timeout function, clear the timeout variable
|
||||||
|
// which will let the next execution run when in 'immediate' mode
|
||||||
|
timeout = null;
|
||||||
|
|
||||||
// Inside the timeout function, clear the timeout variable
|
// Check if the function already ran with the immediate flag
|
||||||
// which will let the next execution run when in 'immediate' mode
|
if (!immediate) {
|
||||||
timeout = null;
|
// Call the original function with apply
|
||||||
|
// apply lets you define the 'this' object as well as the arguments
|
||||||
|
// (both captured before setTimeout)
|
||||||
|
func.apply(context, args);
|
||||||
|
}
|
||||||
|
}, wait);
|
||||||
|
|
||||||
// Check if the function already ran with the immediate flag
|
// Immediate mode and no wait timer? Execute the function..
|
||||||
if (!immediate) {
|
if (callNow) func.apply(context, args);
|
||||||
// Call the original function with apply
|
};
|
||||||
// apply lets you define the 'this' object as well as the arguments
|
|
||||||
// (both captured before setTimeout)
|
|
||||||
func.apply(context, args);
|
|
||||||
}
|
|
||||||
}, wait);
|
|
||||||
|
|
||||||
// Immediate mode and no wait timer? Execute the function..
|
|
||||||
if (callNow) func.apply(context, args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLanguage(): string {
|
export function getLanguage(): string {
|
||||||
return (navigator.language || navigator.userLanguage);
|
return navigator.language || navigator.userLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function objectFlip(obj: any) {
|
export function objectFlip(obj: any) {
|
||||||
const ret = {};
|
const ret = {};
|
||||||
Object.keys(obj).forEach((key) => {
|
Object.keys(obj).forEach(key => {
|
||||||
ret[obj[key]] = key;
|
ret[obj[key]] = key;
|
||||||
});
|
});
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -240,16 +272,24 @@ export function getMomentLanguage(): string {
|
||||||
return lang;
|
return lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const themes = ['litera', 'minty', 'solar', 'united', 'cyborg','darkly', 'journal', 'sketchy'];
|
export const themes = [
|
||||||
|
'litera',
|
||||||
|
'minty',
|
||||||
|
'solar',
|
||||||
|
'united',
|
||||||
|
'cyborg',
|
||||||
|
'darkly',
|
||||||
|
'journal',
|
||||||
|
'sketchy',
|
||||||
|
];
|
||||||
|
|
||||||
export function setTheme(theme: string = 'darkly') {
|
export function setTheme(theme: string = 'darkly') {
|
||||||
for (var i=0; i < themes.length; i++) {
|
for (var i = 0; i < themes.length; i++) {
|
||||||
|
|
||||||
let styleSheet = document.getElementById(themes[i]);
|
let styleSheet = document.getElementById(themes[i]);
|
||||||
if (themes[i] == theme) {
|
if (themes[i] == theme) {
|
||||||
styleSheet.removeAttribute("disabled");
|
styleSheet.removeAttribute('disabled');
|
||||||
} else {
|
} else {
|
||||||
styleSheet.setAttribute("disabled", "disabled");
|
styleSheet.setAttribute('disabled', 'disabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export let version: string = "v0.3.0.2-0-g9f5a328";
|
export let version: string = 'v0.3.0.7-0-g809d87d';
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "tslint:recommended",
|
|
||||||
"rules": {
|
|
||||||
"forin": false,
|
|
||||||
"indent": [ true, "spaces" ],
|
|
||||||
"interface-name": false,
|
|
||||||
"ban-types": true,
|
|
||||||
"max-classes-per-file": true,
|
|
||||||
"max-line-length": false,
|
|
||||||
"member-access": true,
|
|
||||||
"member-ordering": false,
|
|
||||||
"no-bitwise": false,
|
|
||||||
"no-conditional-assignment": false,
|
|
||||||
"no-debugger": false,
|
|
||||||
"no-empty": true,
|
|
||||||
"no-namespace": false,
|
|
||||||
"no-unused-expression": true,
|
|
||||||
"object-literal-sort-keys": true,
|
|
||||||
"one-variable-per-declaration": [true, "ignore-for-loop"],
|
|
||||||
"only-arrow-functions": [false],
|
|
||||||
"ordered-imports": true,
|
|
||||||
"prefer-const": true,
|
|
||||||
"prefer-for-of": false,
|
|
||||||
"quotemark": [ true, "single", "jsx-double" ],
|
|
||||||
"trailing-comma": [true, {"multiline": "never", "singleline": "never"}],
|
|
||||||
"variable-name": false
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue