Merge branch 'master' into master

This commit is contained in:
Matteo Guglielmetti 2019-10-21 08:50:38 +02:00 committed by GitHub
commit bab917f1be
84 changed files with 8064 additions and 2266 deletions

40
README.md vendored
View file

@ -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.

View file

@ -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:

1
docs/api.md vendored
View file

@ -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
} }
} }

View file

@ -0,0 +1,2 @@
drop view user_mention_view;
drop table user_mention;

View file

@ -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;

View file

@ -0,0 +1,2 @@
alter table user_ drop column default_sort_type;
alter table user_ drop column default_listing_type;

View file

@ -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;

View file

@ -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 {

View file

@ -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,

View file

@ -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(

View file

@ -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,

View file

@ -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),

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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,

View file

@ -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();

View file

@ -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();

View file

@ -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,

345
server/src/db/src/schema.rs Normal file
View file

@ -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,
);

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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)
}
}

View file

@ -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();
} }

View file

@ -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,
); );

View file

@ -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()?;

57
ui/.eslintrc.json vendored Normal file
View file

@ -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
}
}

4
ui/.prettierrc.js vendored Normal file
View file

@ -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

44
ui/package.json vendored
View file

@ -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"
]
} }
} }

View file

@ -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;
@ -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,21 +279,22 @@ 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) {
@ -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 {

View file

@ -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,18 +485,20 @@ 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}
@ -261,23 +506,38 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
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 (
this.props.moderators &&
UserService.Instance.user && UserService.Instance.user &&
(this.props.node.comment.creator_id != UserService.Instance.user.id) && this.props.node.comment.creator_id != UserService.Instance.user.id &&
(UserService.Instance.user.id == this.props.moderators[0].user_id); UserService.Instance.user.id == this.props.moderators[0].user_id
);
} }
get amSiteCreator(): boolean { get amSiteCreator(): boolean {
return this.props.admins && return (
this.props.admins &&
UserService.Instance.user && UserService.Instance.user &&
(this.props.node.comment.creator_id != UserService.Instance.user.id) && this.props.node.comment.creator_id != UserService.Instance.user.id &&
(UserService.Instance.user.id == this.props.admins[0].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;

View file

@ -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,8 +30,9 @@ 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
node={node}
noIndent={this.props.noIndent} noIndent={this.props.noIndent}
viewOnly={this.props.viewOnly} viewOnly={this.props.viewOnly}
locked={this.props.locked} locked={this.props.locked}
@ -35,10 +41,8 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
postCreatorId={this.props.postCreatorId} postCreatorId={this.props.postCreatorId}
markable={this.props.markable} markable={this.props.markable}
/> />
)} ))}
</div> </div>
) );
} }
} }

View file

@ -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>
); );
} }
@ -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);

View file

@ -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) {
@ -193,5 +266,4 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
this.props.onEdit(res.community); this.props.onEdit(res.community);
} }
} }
} }

View file

@ -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,60 +130,70 @@ 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>
); );
} }
@ -164,7 +203,7 @@ export class Community extends Component<any, State> {
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) {
@ -172,21 +211,24 @@ export class Community extends Component<any, State> {
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> {
} }
} }
} }

View file

@ -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}`);
} }
} }

View file

@ -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,8 +49,8 @@ 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}`);
} }
} }

View file

@ -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> {
); );
} }
} }

View file

@ -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,10 +218,20 @@ 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>
); );
} }
@ -134,6 +248,13 @@ export class Inbox extends Component<any, InboxState> {
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,
});
} }
} }

View file

@ -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_);
}
}

View file

@ -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> {
} }
} }
} }
} }

View file

@ -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">
@ -241,23 +333,35 @@ export class Main extends Component<any, MainState> {
</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) {
@ -364,7 +484,7 @@ export class Main extends Component<any, MainState> {
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) {
@ -373,27 +493,27 @@ export class Main extends Component<any, MainState> {
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> {
} }
} }
} }

View file

@ -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,10 +390,20 @@ 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>
); );
} }
@ -219,7 +420,7 @@ export class Modlog extends Component<any, ModlogState> {
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,7 +438,7 @@ 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);
} }
} }

View file

@ -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>
)
} }
} }
} }

View file

@ -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;
@ -180,12 +264,12 @@ export class Navbar extends Component<any, NavbarState> {
} }
} }
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}`
);
}; };
} }
} }
} }

View file

@ -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;
@ -343,7 +471,4 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.setState(this.state); this.setState(this.state);
} }
} }
} }

View file

@ -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(
UserService.Instance.user,
adminsThenMods,
this.props.post.creator_id,
true
);
} else { } 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 (
this.props.moderators &&
UserService.Instance.user && UserService.Instance.user &&
(this.props.post.creator_id != UserService.Instance.user.id) && this.props.post.creator_id != UserService.Instance.user.id &&
(UserService.Instance.user.id == this.props.moderators[0].user_id); UserService.Instance.user.id == this.props.moderators[0].user_id
);
} }
get amSiteCreator(): boolean { get amSiteCreator(): boolean {
return this.props.admins && return (
this.props.admins &&
UserService.Instance.user && UserService.Instance.user &&
(this.props.post.creator_id != UserService.Instance.user.id) && this.props.post.creator_id != UserService.Instance.user.id &&
(UserService.Instance.user.id == this.props.admins[0].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);
@ -622,4 +945,3 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
i.setState(i.state); i.setState(i.state);
} }
} }

View file

@ -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>
) );
} }
} }

View file

@ -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,
@ -39,7 +63,7 @@ export class Post extends Component<any, PostState> {
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 &&
!this.state.scrolled &&
lastState.comments.length > 0
) {
var elmnt = document.getElementById(
`comment-${this.state.scrolled_comment_id}`
);
elmnt.scrollIntoView(); elmnt.scrollIntoView();
elmnt.classList.add("mark"); 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,9 +156,14 @@ 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}
@ -128,14 +173,19 @@ export class Post extends Component<any, PostState> {
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,51 +194,74 @@ 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>
</h6>
{this.state.comments.map(comment => (
<CommentNodes <CommentNodes
nodes={[{comment: comment}]} nodes={[{ comment: comment }]}
noIndent noIndent
locked={this.state.post.locked} locked={this.state.post.locked}
moderators={this.state.moderators} moderators={this.state.moderators}
admins={this.state.admins} admins={this.state.admins}
postCreatorId={this.state.post.creator_id} postCreatorId={this.state.post.creator_id}
/> />
)} ))}
</div> </div>
</div> </div>
) );
} }
sidebar() { sidebar() {
@ -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,26 +305,33 @@ 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() {
@ -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,12 +434,14 @@ 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)
.forEach(c => (c.banned_from_community = res.banned));
if (this.state.post.creator_id == res.user.id) { 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;
} }
@ -370,8 +452,9 @@ 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)
.forEach(c => (c.banned = res.banned));
if (this.state.post.creator_id == res.user.id) { if (this.state.post.creator_id == res.user.id) {
this.state.post.banned = res.banned; this.state.post.banned = res.banned;
} }
@ -396,9 +479,5 @@ export class Post extends Component<any, PostState> {
this.state.admins = res.admins; this.state.admins = res.admins;
this.setState(this.state); this.setState(this.state);
} }
} }
} }

View file

@ -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,11 +378,18 @@ 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) {
@ -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);
} }
} }
} }

View file

@ -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;

View file

@ -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}`}>
/c/{community.name}
</Link>
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
{this.canMod && {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) {

View file

@ -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>

69
ui/src/components/sort-select.tsx vendored Normal file
View file

@ -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);
}
}

View file

@ -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> );
)
} }
} }

View file

@ -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>
@ -22,58 +27,64 @@ export class Symbols extends Component<any, any> {
<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>
); );
} }
} }

View file

@ -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,28 +326,60 @@ 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() {
@ -292,95 +387,218 @@ export class User extends Component<any, UserState> {
<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>
<T i18nKey="moderates">#</T>
</h5>
<ul class="list-unstyled mb-0"> <ul class="list-unstyled mb-0">
{this.state.moderates.map(community => {this.state.moderates.map(community => (
<li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li> <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>
<T i18nKey="subscribed">#</T>
</h5>
<ul class="list-unstyled mb-0"> <ul class="list-unstyled mb-0">
{this.state.follows.map(community => {this.state.follows.map(community => (
<li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li> <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,7 +606,9 @@ 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) {
@ -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('/');
} }
} }
} }

6
ui/src/env.ts vendored
View file

@ -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`;

19
ui/src/i18next.ts vendored
View file

@ -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,
} }
}); });

27
ui/src/index.tsx vendored
View file

@ -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);

300
ui/src/interfaces.ts vendored
View file

@ -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 {

View file

@ -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());
} }
} }

View file

@ -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,13 +50,20 @@ 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());
} }
@ -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,12 +187,16 @@ 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) {
@ -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;
}); };

View file

@ -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.',
}, },
} };

View file

@ -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.',
}, },
} };

View file

@ -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.',
}, },
} };

View file

@ -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.',
}, },
} };

View file

@ -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.',
}, },
} };

View file

@ -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.',
}, },
} };

View file

@ -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:
'Системная ошибка. Попробуйте выйти из системы и вернуться обратно.',
}, },
} };

View file

@ -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.',
}, },
} };

View file

@ -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: '系统错误. 尝试注销再登录',
}, },
} };

172
ui/src/utils.ts vendored
View file

@ -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,18 +90,28 @@ 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);
@ -92,7 +119,7 @@ export function canMod(user: User, modIds: Array<number>, creator_id: number, on
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');
} }
} }
} }

2
ui/src/version.ts vendored
View file

@ -1 +1 @@
export let version: string = "v0.3.0.2-0-g9f5a328"; export let version: string = 'v0.3.0.7-0-g809d87d';

28
ui/tslint.json vendored
View file

@ -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
}
}

2087
ui/yarn.lock vendored

File diff suppressed because it is too large Load diff