mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-30 00:01:25 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
bab917f1be
84 changed files with 8064 additions and 2266 deletions
40
README.md
vendored
40
README.md
vendored
|
@ -58,7 +58,7 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins
|
|||
|
||||
## 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 [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
||||
- 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
|
||||
|
||||
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
|
||||
mkdir lemmy/
|
||||
|
@ -80,7 +80,7 @@ wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/.env
|
|||
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:
|
||||
|
||||
|
@ -100,8 +100,7 @@ docker-compose up -d
|
|||
|
||||
### Ansible
|
||||
|
||||
First, you need to [install Ansible on your local computer](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html),
|
||||
eg using `sudo apt install ansible`, or the equivalent for you platform.
|
||||
First, you need to [install Ansible on your local computer](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) (e.g. using `sudo apt install ansible`) or the equivalent for you platform.
|
||||
|
||||
Then run the following commands on your local computer:
|
||||
|
||||
|
@ -142,13 +141,15 @@ If you used a `LoadBalancer`, you should see it in your cloud provider's console
|
|||
|
||||
### Docker Development
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dessalines/lemmy
|
||||
cd lemmy/docker/dev
|
||||
./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
|
||||
|
||||
|
@ -195,21 +196,28 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
|
|||
|
||||
## 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`).
|
||||
|
||||
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
|
||||
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
|
||||
es | 100% |
|
||||
fr | 95% | view_source,sticky,unsticky,stickied,delete_account,delete_account_confirm,creator,number_online,theme
|
||||
nl | 93% | preview,upload_image,formatting_help,view_source,sticky,unsticky,stickied,delete_account,delete_account_confirm,banned,creator,number_online,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
|
||||
sv | 100% |
|
||||
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
|
||||
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 | 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 | 99% | replies,mentions
|
||||
fr | 99% | replies,mentions
|
||||
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,replies,mentions,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
||||
sv | 99% | replies,mentions
|
||||
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
|
||||
|
||||
Logo made by Andy Cuccaro (@andycuccaro) under the CC-BY-SA 4.0 license
|
||||
Logo made by Andy Cuccaro (@andycuccaro) under the CC-BY-SA 4.0 license.
|
||||
|
|
2
docker/prod/docker-compose.yml
vendored
2
docker/prod/docker-compose.yml
vendored
|
@ -10,7 +10,7 @@ services:
|
|||
volumes:
|
||||
- lemmy_db:/var/lib/postgresql/data
|
||||
lemmy:
|
||||
image: dessalines/lemmy:v0.3.0.2
|
||||
image: dessalines/lemmy:v0.3.0.7
|
||||
ports:
|
||||
- "127.0.0.1:8536:8536"
|
||||
environment:
|
||||
|
|
1
docs/api.md
vendored
1
docs/api.md
vendored
|
@ -210,6 +210,7 @@ Only the first user will be able to be the admin.
|
|||
{
|
||||
op: "DeleteAccount",
|
||||
data: {
|
||||
password: String,
|
||||
auth: String
|
||||
}
|
||||
}
|
||||
|
|
2
server/migrations/2019-10-19-052737_create_user_mention/down.sql
vendored
Normal file
2
server/migrations/2019-10-19-052737_create_user_mention/down.sql
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
drop view user_mention_view;
|
||||
drop table user_mention;
|
35
server/migrations/2019-10-19-052737_create_user_mention/up.sql
vendored
Normal file
35
server/migrations/2019-10-19-052737_create_user_mention/up.sql
vendored
Normal 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;
|
2
server/migrations/2019-10-21-011237_add_default_sorts/down.sql
vendored
Normal file
2
server/migrations/2019-10-21-011237_add_default_sorts/down.sql
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
alter table user_ drop column default_sort_type;
|
||||
alter table user_ drop column default_listing_type;
|
2
server/migrations/2019-10-21-011237_add_default_sorts/up.sql
vendored
Normal file
2
server/migrations/2019-10-21-011237_add_default_sorts/up.sql
vendored
Normal 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;
|
|
@ -85,6 +85,35 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
|||
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
|
||||
let like_form = CommentLikeForm {
|
||||
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"))?,
|
||||
};
|
||||
|
||||
// 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
|
||||
if let Some(removed) = data.removed.to_owned() {
|
||||
let form = ModRemoveCommentForm {
|
||||
|
|
|
@ -8,9 +8,11 @@ use crate::db::moderator_views::*;
|
|||
use crate::db::post::*;
|
||||
use crate::db::post_view::*;
|
||||
use crate::db::user::*;
|
||||
use crate::db::user_mention::*;
|
||||
use crate::db::user_mention_view::*;
|
||||
use crate::db::user_view::*;
|
||||
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 serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -43,6 +45,8 @@ pub enum UserOperation {
|
|||
GetFollowedCommunities,
|
||||
GetUserDetails,
|
||||
GetReplies,
|
||||
GetUserMentions,
|
||||
EditUserMention,
|
||||
GetModlog,
|
||||
BanFromCommunity,
|
||||
AddModToCommunity,
|
||||
|
|
|
@ -235,7 +235,7 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
|
|||
None => false,
|
||||
};
|
||||
|
||||
let type_ = PostListingType::from_str(&data.type_)?;
|
||||
let type_ = ListingType::from_str(&data.type_)?;
|
||||
let sort = SortType::from_str(&data.sort)?;
|
||||
|
||||
let posts = match PostView::list(
|
||||
|
|
|
@ -321,7 +321,7 @@ impl Perform<SearchResponse> for Oper<Search> {
|
|||
SearchType::Posts => {
|
||||
posts = PostView::list(
|
||||
&conn,
|
||||
PostListingType::All,
|
||||
ListingType::All,
|
||||
&sort,
|
||||
data.community_id,
|
||||
None,
|
||||
|
@ -365,7 +365,7 @@ impl Perform<SearchResponse> for Oper<Search> {
|
|||
SearchType::All => {
|
||||
posts = PostView::list(
|
||||
&conn,
|
||||
PostListingType::All,
|
||||
ListingType::All,
|
||||
&sort,
|
||||
data.community_id,
|
||||
None,
|
||||
|
@ -403,7 +403,7 @@ impl Perform<SearchResponse> for Oper<Search> {
|
|||
SearchType::Url => {
|
||||
posts = PostView::list(
|
||||
&conn,
|
||||
PostListingType::All,
|
||||
ListingType::All,
|
||||
&sort,
|
||||
data.community_id,
|
||||
None,
|
||||
|
|
|
@ -22,6 +22,8 @@ pub struct Register {
|
|||
pub struct SaveUserSettings {
|
||||
show_nsfw: bool,
|
||||
theme: String,
|
||||
default_sort_type: i16,
|
||||
default_listing_type: i16,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
|
@ -60,6 +62,12 @@ pub struct GetRepliesResponse {
|
|||
replies: Vec<ReplyView>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetUserMentionsResponse {
|
||||
op: String,
|
||||
mentions: Vec<UserMentionView>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MarkAllAsRead {
|
||||
auth: String,
|
||||
|
@ -103,6 +111,28 @@ pub struct GetReplies {
|
|||
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)]
|
||||
pub struct DeleteAccount {
|
||||
password: String,
|
||||
|
@ -170,6 +200,8 @@ impl Perform<LoginResponse> for Oper<Register> {
|
|||
banned: false,
|
||||
show_nsfw: data.show_nsfw,
|
||||
theme: "darkly".into(),
|
||||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
};
|
||||
|
||||
// Create the user
|
||||
|
@ -261,6 +293,8 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
|||
banned: read_user.banned,
|
||||
show_nsfw: data.show_nsfw,
|
||||
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) {
|
||||
|
@ -299,7 +333,6 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
|||
None => false,
|
||||
};
|
||||
|
||||
//TODO add save
|
||||
let sort = SortType::from_str(&data.sort)?;
|
||||
|
||||
let user_details_id = match data.user_id {
|
||||
|
@ -319,7 +352,7 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
|||
let posts = if data.saved_only {
|
||||
PostView::list(
|
||||
&conn,
|
||||
PostListingType::All,
|
||||
ListingType::All,
|
||||
&sort,
|
||||
data.community_id,
|
||||
None,
|
||||
|
@ -335,7 +368,7 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
|||
} else {
|
||||
PostView::list(
|
||||
&conn,
|
||||
PostListingType::All,
|
||||
ListingType::All,
|
||||
&sort,
|
||||
data.community_id,
|
||||
Some(user_details_id),
|
||||
|
@ -426,6 +459,8 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
|||
banned: read_user.banned,
|
||||
show_nsfw: read_user.show_nsfw,
|
||||
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) {
|
||||
|
@ -485,6 +520,8 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
|||
banned: data.ban,
|
||||
show_nsfw: read_user.show_nsfw,
|
||||
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) {
|
||||
|
@ -541,7 +578,6 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
|||
data.limit,
|
||||
)?;
|
||||
|
||||
// Return the jwt
|
||||
Ok(GetRepliesResponse {
|
||||
op: self.op.to_string(),
|
||||
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> {
|
||||
fn perform(&self) -> Result<GetRepliesResponse, Error> {
|
||||
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 {
|
||||
op: self.op.to_string(),
|
||||
replies: replies,
|
||||
replies: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -644,7 +761,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
|||
// Posts
|
||||
let posts = PostView::list(
|
||||
&conn,
|
||||
PostListingType::All,
|
||||
ListingType::All,
|
||||
&SortType::New,
|
||||
None,
|
||||
Some(user_id),
|
||||
|
|
|
@ -55,6 +55,7 @@ impl User_ {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::User_;
|
||||
use crate::db::{ListingType, SortType};
|
||||
use crate::naive_now;
|
||||
|
||||
#[test]
|
||||
|
@ -73,6 +74,8 @@ mod tests {
|
|||
updated: None,
|
||||
show_nsfw: false,
|
||||
theme: "darkly".into(),
|
||||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
};
|
||||
|
||||
let person = expected_user.person();
|
||||
|
|
|
@ -179,6 +179,8 @@ mod tests {
|
|||
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();
|
||||
|
|
|
@ -69,7 +69,6 @@ impl CommentView {
|
|||
|
||||
let (limit, offset) = limit_and_offset(page, limit);
|
||||
|
||||
// TODO no limits here?
|
||||
let mut query = comment_view.into_boxed();
|
||||
|
||||
// The view lets you pass a null user_id, if you're not logged in
|
||||
|
@ -265,6 +264,8 @@ mod tests {
|
|||
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();
|
||||
|
|
|
@ -265,6 +265,8 @@ mod tests {
|
|||
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();
|
||||
|
|
|
@ -14,6 +14,8 @@ pub mod moderator_views;
|
|||
pub mod post;
|
||||
pub mod post_view;
|
||||
pub mod user;
|
||||
pub mod user_mention;
|
||||
pub mod user_mention_view;
|
||||
pub mod user_view;
|
||||
|
||||
pub trait Crud<T> {
|
||||
|
@ -104,6 +106,13 @@ pub enum SortType {
|
|||
TopAll,
|
||||
}
|
||||
|
||||
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
||||
pub enum ListingType {
|
||||
All,
|
||||
Subscribed,
|
||||
Community,
|
||||
}
|
||||
|
||||
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
||||
pub enum SearchType {
|
||||
All,
|
||||
|
|
|
@ -447,6 +447,8 @@ mod tests {
|
|||
updated: None,
|
||||
show_nsfw: false,
|
||||
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();
|
||||
|
@ -462,6 +464,8 @@ mod tests {
|
|||
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();
|
||||
|
|
|
@ -192,6 +192,8 @@ mod tests {
|
|||
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();
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
use super::*;
|
||||
|
||||
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
||||
pub enum PostListingType {
|
||||
All,
|
||||
Subscribed,
|
||||
Community,
|
||||
}
|
||||
|
||||
// The faked schema since diesel doesn't do views
|
||||
table! {
|
||||
post_view (id) {
|
||||
|
@ -83,7 +76,7 @@ pub struct PostView {
|
|||
impl PostView {
|
||||
pub fn list(
|
||||
conn: &PgConnection,
|
||||
type_: PostListingType,
|
||||
type_: ListingType,
|
||||
sort: &SortType,
|
||||
for_community_id: Option<i32>,
|
||||
for_creator_id: Option<i32>,
|
||||
|
@ -129,7 +122,7 @@ impl PostView {
|
|||
};
|
||||
|
||||
match type_ {
|
||||
PostListingType::Subscribed => {
|
||||
ListingType::Subscribed => {
|
||||
query = query.filter(subscribed.eq(true));
|
||||
}
|
||||
_ => {}
|
||||
|
@ -226,6 +219,8 @@ mod tests {
|
|||
banned: false,
|
||||
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();
|
||||
|
@ -351,7 +346,7 @@ mod tests {
|
|||
|
||||
let read_post_listings_with_user = PostView::list(
|
||||
&conn,
|
||||
PostListingType::Community,
|
||||
ListingType::Community,
|
||||
&SortType::New,
|
||||
Some(inserted_community.id),
|
||||
None,
|
||||
|
@ -367,7 +362,7 @@ mod tests {
|
|||
.unwrap();
|
||||
let read_post_listings_no_user = PostView::list(
|
||||
&conn,
|
||||
PostListingType::Community,
|
||||
ListingType::Community,
|
||||
&SortType::New,
|
||||
Some(inserted_community.id),
|
||||
None,
|
||||
|
|
345
server/src/db/src/schema.rs
Normal file
345
server/src/db/src/schema.rs
Normal 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,
|
||||
);
|
|
@ -21,6 +21,8 @@ pub struct User_ {
|
|||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub show_nsfw: bool,
|
||||
pub theme: String,
|
||||
pub default_sort_type: i16,
|
||||
pub default_listing_type: i16,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Clone)]
|
||||
|
@ -36,6 +38,8 @@ pub struct UserForm {
|
|||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub show_nsfw: bool,
|
||||
pub theme: String,
|
||||
pub default_sort_type: i16,
|
||||
pub default_listing_type: i16,
|
||||
}
|
||||
|
||||
impl Crud<UserForm> for User_ {
|
||||
|
@ -77,6 +81,8 @@ pub struct Claims {
|
|||
pub iss: String,
|
||||
pub show_nsfw: bool,
|
||||
pub theme: String,
|
||||
pub default_sort_type: i16,
|
||||
pub default_listing_type: i16,
|
||||
}
|
||||
|
||||
impl Claims {
|
||||
|
@ -98,6 +104,8 @@ impl User_ {
|
|||
iss: self.fedi_name.to_owned(),
|
||||
show_nsfw: self.show_nsfw,
|
||||
theme: self.theme.to_owned(),
|
||||
default_sort_type: self.default_sort_type,
|
||||
default_listing_type: self.default_listing_type,
|
||||
};
|
||||
encode(
|
||||
&Header::default(),
|
||||
|
@ -146,6 +154,8 @@ mod tests {
|
|||
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();
|
||||
|
@ -164,6 +174,8 @@ mod tests {
|
|||
updated: None,
|
||||
show_nsfw: false,
|
||||
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();
|
||||
|
|
173
server/src/db/user_mention.rs
Normal file
173
server/src/db/user_mention.rs
Normal 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);
|
||||
}
|
||||
}
|
117
server/src/db/user_mention_view.rs
Normal file
117
server/src/db/user_mention_view.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -104,9 +104,23 @@ pub fn has_slurs(test: &str) -> bool {
|
|||
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)]
|
||||
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]
|
||||
fn test_api() {
|
||||
assert_eq!(Settings::get().api_endpoint(), "rrr/api/v1");
|
||||
|
@ -131,9 +145,17 @@ mod tests {
|
|||
assert!(has_slurs(&test));
|
||||
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! {
|
||||
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 USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
|
||||
}
|
||||
|
|
|
@ -255,6 +255,8 @@ table! {
|
|||
updated -> Nullable<Timestamp>,
|
||||
show_nsfw -> Bool,
|
||||
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 -> user_ (creator_id));
|
||||
joinable!(comment_like -> comment (comment_id));
|
||||
|
@ -303,6 +315,8 @@ 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,
|
||||
|
@ -329,4 +343,5 @@ allow_tables_to_appear_in_same_query!(
|
|||
site,
|
||||
user_,
|
||||
user_ban,
|
||||
user_mention,
|
||||
);
|
||||
|
|
|
@ -136,7 +136,7 @@ impl ChatServer {
|
|||
let conn = establish_connection();
|
||||
let posts = PostView::list(
|
||||
&conn,
|
||||
PostListingType::Community,
|
||||
ListingType::Community,
|
||||
&SortType::New,
|
||||
Some(*community_id),
|
||||
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()?;
|
||||
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 => {
|
||||
let mark_all_as_read: MarkAllAsRead = serde_json::from_str(data)?;
|
||||
let res = Oper::new(user_operation, mark_all_as_read).perform()?;
|
||||
|
|
57
ui/.eslintrc.json
vendored
Normal file
57
ui/.eslintrc.json
vendored
Normal 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
4
ui/.prettierrc.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
module.exports = Object.assign(require('eslint-plugin-jane/prettier-ts'), {
|
||||
arrowParens: 'avoid',
|
||||
semi: true,
|
||||
});
|
2
ui/assets/css/themes/cyborg.min.css
vendored
2
ui/assets/css/themes/cyborg.min.css
vendored
File diff suppressed because one or more lines are too long
2
ui/assets/css/themes/darkly.min.css
vendored
2
ui/assets/css/themes/darkly.min.css
vendored
File diff suppressed because one or more lines are too long
2
ui/assets/css/themes/journal.min.css
vendored
2
ui/assets/css/themes/journal.min.css
vendored
File diff suppressed because one or more lines are too long
2
ui/assets/css/themes/minty.min.css
vendored
2
ui/assets/css/themes/minty.min.css
vendored
File diff suppressed because one or more lines are too long
2
ui/assets/css/themes/sketchy.min.css
vendored
2
ui/assets/css/themes/sketchy.min.css
vendored
File diff suppressed because one or more lines are too long
2
ui/assets/css/themes/solar.min.css
vendored
2
ui/assets/css/themes/solar.min.css
vendored
File diff suppressed because one or more lines are too long
2
ui/assets/css/themes/united.min.css
vendored
2
ui/assets/css/themes/united.min.css
vendored
File diff suppressed because one or more lines are too long
44
ui/package.json
vendored
44
ui/package.json
vendored
|
@ -1,19 +1,16 @@
|
|||
{
|
||||
"name": "lemmy",
|
||||
"version": "1.0.0",
|
||||
"description": "A simple UI for lemmy",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node fuse dev",
|
||||
"build": "node fuse prod"
|
||||
},
|
||||
"keywords": [],
|
||||
"version": "1.0.0",
|
||||
"author": "Dessalines",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "node fuse prod",
|
||||
"lint": "eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
|
||||
"start": "node fuse dev"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"keywords": [],
|
||||
"dependencies": {
|
||||
"@types/autosize": "^3.0.6",
|
||||
"@types/js-cookie": "^2.2.1",
|
||||
|
@ -25,6 +22,7 @@
|
|||
"classcat": "^1.1.3",
|
||||
"dotenv": "^6.1.0",
|
||||
"emoji-short-name": "^0.1.0",
|
||||
"husky": "^3.0.9",
|
||||
"i18next": "^17.0.9",
|
||||
"inferno": "^7.0.1",
|
||||
"inferno-i18next": "nimbusec-oss/inferno-i18next",
|
||||
|
@ -35,6 +33,7 @@
|
|||
"markdown-it-container": "^2.0.0",
|
||||
"markdown-it-emoji": "^1.4.0",
|
||||
"moment": "^2.24.0",
|
||||
"prettier": "^1.18.2",
|
||||
"rxjs": "^6.4.0",
|
||||
"terser": "^3.17.0",
|
||||
"tributejs": "3.7.2",
|
||||
|
@ -43,9 +42,34 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"lint-staged": "^9.4.2",
|
||||
"sortpack": "^2.0.1",
|
||||
"ts-transform-classcat": "^0.0.2",
|
||||
"ts-transform-inferno": "^4.0.2",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
162
ui/src/components/comment-form.tsx
vendored
162
ui/src/components/comment-form.tsx
vendored
|
@ -1,7 +1,22 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { CommentNode as CommentNodeI, CommentForm as CommentFormI, SearchForm, SearchType, SortType, UserOperation, SearchResponse } from '../interfaces';
|
||||
import { Subscription } from "rxjs";
|
||||
import { capitalizeFirstLetter, mentionDropdownFetchLimit, msgOp, mdToHtml, randomStr, markdownHelpUrl } from '../utils';
|
||||
import {
|
||||
CommentNode as CommentNodeI,
|
||||
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 * as autosize from 'autosize';
|
||||
import { i18n } from '../i18next';
|
||||
|
@ -25,7 +40,6 @@ interface CommentFormState {
|
|||
}
|
||||
|
||||
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
|
||||
private id = `comment-form-${randomStr()}`;
|
||||
private userSub: Subscription;
|
||||
private communitySub: Subscription;
|
||||
|
@ -34,13 +48,21 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
commentForm: {
|
||||
auth: null,
|
||||
content: null,
|
||||
post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId,
|
||||
creator_id: UserService.Instance.user ? UserService.Instance.user.id : null,
|
||||
post_id: this.props.node
|
||||
? this.props.node.comment.post_id
|
||||
: this.props.postId,
|
||||
creator_id: UserService.Instance.user
|
||||
? UserService.Instance.user.id
|
||||
: null,
|
||||
},
|
||||
buttonTitle: !this.props.node ? 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,
|
||||
imageLoading: false,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
@ -57,7 +79,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
selectTemplate: (item: any) => {
|
||||
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,
|
||||
autocompleteMode: true,
|
||||
menuItemLimit: mentionDropdownFetchLimit,
|
||||
|
@ -88,8 +112,8 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
allowSpaces: false,
|
||||
autocompleteMode: true,
|
||||
menuItemLimit: mentionDropdownFetchLimit,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
@ -124,27 +148,82 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
|
||||
<div class="form-group row">
|
||||
<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} />
|
||||
{this.state.previewMode &&
|
||||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.commentForm.content)} />
|
||||
}
|
||||
<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}
|
||||
/>
|
||||
{this.state.previewMode && (
|
||||
<div
|
||||
className="md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(
|
||||
this.state.commentForm.content
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<button type="submit" class="btn btn-sm btn-secondary mr-2" disabled={this.props.disabled}>{this.state.buttonTitle}</button>
|
||||
{this.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>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-sm btn-secondary mr-2"
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
{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">
|
||||
<label 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)} />
|
||||
<label
|
||||
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>
|
||||
{this.state.imageLoading &&
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg>
|
||||
}
|
||||
{this.state.imageLoading && (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -203,18 +282,19 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
.then(res => res.json())
|
||||
.then(res => {
|
||||
let url = `${window.location.origin}/pictshare/${res.url}`;
|
||||
let markdown = (res.filetype == 'mp4') ? `[vid](${url}/raw)` : `![](${url})`;
|
||||
let markdown =
|
||||
res.filetype == 'mp4' ? `[vid](${url}/raw)` : `![](${url})`;
|
||||
let content = i.state.commentForm.content;
|
||||
content = (content) ? `${content} ${markdown}` : markdown;
|
||||
content = content ? `${content} ${markdown}` : markdown;
|
||||
i.state.commentForm.content = content;
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
alert(error);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
userSearch(text: string, cb: any) {
|
||||
|
@ -229,18 +309,19 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
|
||||
WebSocketService.Instance.search(form);
|
||||
|
||||
this.userSub = WebSocketService.Instance.subject
|
||||
.subscribe(
|
||||
(msg) => {
|
||||
this.userSub = WebSocketService.Instance.subject.subscribe(
|
||||
msg => {
|
||||
let op: UserOperation = msgOp(msg);
|
||||
if (op == UserOperation.Search) {
|
||||
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);
|
||||
this.userSub.unsubscribe();
|
||||
}
|
||||
},
|
||||
(err) => console.error(err),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
} else {
|
||||
|
@ -260,18 +341,19 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
|
||||
WebSocketService.Instance.search(form);
|
||||
|
||||
this.communitySub = WebSocketService.Instance.subject
|
||||
.subscribe(
|
||||
(msg) => {
|
||||
this.communitySub = WebSocketService.Instance.subject.subscribe(
|
||||
msg => {
|
||||
let op: UserOperation = msgOp(msg);
|
||||
if (op == UserOperation.Search) {
|
||||
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);
|
||||
this.communitySub.unsubscribe();
|
||||
}
|
||||
},
|
||||
(err) => console.error(err),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
} else {
|
||||
|
|
572
ui/src/components/comment-node.tsx
vendored
572
ui/src/components/comment-node.tsx
vendored
|
@ -1,6 +1,21 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, SaveCommentForm, BanFromCommunityForm, BanUserForm, CommunityUser, UserView, AddModToCommunityForm, AddAdminForm, 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 { mdToHtml, getUnixTime, canMod, isMod } from '../utils';
|
||||
import * as moment from 'moment';
|
||||
|
@ -37,7 +52,6 @@ interface CommentNodeProps {
|
|||
}
|
||||
|
||||
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||
|
||||
private emptyState: CommentNodeState = {
|
||||
showReply: false,
|
||||
showEdit: false,
|
||||
|
@ -51,7 +65,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
viewSource: false,
|
||||
showConfirmTransferSite: false,
|
||||
showConfirmTransferCommunity: false,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
@ -65,176 +79,405 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
render() {
|
||||
let node = this.props.node;
|
||||
return (
|
||||
<div className={`comment ${node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''}`}>
|
||||
{!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>
|
||||
<div
|
||||
className={`comment ${
|
||||
node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''
|
||||
}`}
|
||||
>
|
||||
{!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>
|
||||
<div class={`font-weight-bold text-muted`}>{node.comment.score}</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>
|
||||
<div class={`font-weight-bold text-muted`}>
|
||||
{node.comment.score}
|
||||
</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>
|
||||
</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">
|
||||
<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>
|
||||
{this.isMod &&
|
||||
<li className="list-inline-item badge badge-light"><T i18nKey="mod">#</T></li>
|
||||
}
|
||||
{this.isAdmin &&
|
||||
<li className="list-inline-item badge badge-light"><T i18nKey="admin">#</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>
|
||||
}
|
||||
{this.isMod && (
|
||||
<li className="list-inline-item badge badge-light">
|
||||
<T i18nKey="mod">#</T>
|
||||
</li>
|
||||
)}
|
||||
{this.isAdmin && (
|
||||
<li className="list-inline-item badge badge-light">
|
||||
<T i18nKey="admin">#</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">
|
||||
<span>(
|
||||
<span className="text-info">+{node.comment.upvotes}</span>
|
||||
<span>
|
||||
(<span className="text-info">+{node.comment.upvotes}</span>
|
||||
<span> | </span>
|
||||
<span className="text-danger">-{node.comment.downvotes}</span>
|
||||
<span>) </span>
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<span><MomentTime data={node.comment} /></span>
|
||||
<span>
|
||||
<MomentTime data={node.comment} />
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<div className="pointer text-monospace" onClick={linkEvent(this, this.handleCommentCollapse)}>{this.state.collapsed ? '[+]' : '[-]'}</div>
|
||||
<div
|
||||
className="pointer text-monospace"
|
||||
onClick={linkEvent(this, this.handleCommentCollapse)}
|
||||
>
|
||||
{this.state.collapsed ? '[+]' : '[-]'}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
|
||||
{!this.state.showEdit && !this.state.collapsed &&
|
||||
{this.state.showEdit && (
|
||||
<CommentForm
|
||||
node={node}
|
||||
edit
|
||||
onReplyCancel={this.handleReplyCancel}
|
||||
disabled={this.props.locked}
|
||||
/>
|
||||
)}
|
||||
{!this.state.showEdit && !this.state.collapsed && (
|
||||
<div>
|
||||
{this.state.viewSource ? <pre>{this.commentUnlessRemoved}</pre> :
|
||||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.commentUnlessRemoved)} />
|
||||
}
|
||||
{this.state.viewSource ? (
|
||||
<pre>{this.commentUnlessRemoved}</pre>
|
||||
) : (
|
||||
<div
|
||||
className="md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(this.commentUnlessRemoved)}
|
||||
/>
|
||||
)}
|
||||
<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">
|
||||
<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 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>
|
||||
{this.myComment &&
|
||||
{this.myComment && (
|
||||
<>
|
||||
<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 className="list-inline-item">
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
|
||||
{!node.comment.deleted ? i18n.t('delete') : i18n.t('restore')}
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||
>
|
||||
{!node.comment.deleted
|
||||
? i18n.t('delete')
|
||||
: i18n.t('restore')}
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
{/* Admins and mods can remove comments */}
|
||||
{(this.canMod || this.canAdmin) &&
|
||||
{(this.canMod || this.canAdmin) && (
|
||||
<li className="list-inline-item">
|
||||
{!node.comment.removed ?
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
|
||||
}
|
||||
{!node.comment.removed ? (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleModRemoveShow)}
|
||||
>
|
||||
<T i18nKey="remove">#</T>
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleModRemoveSubmit
|
||||
)}
|
||||
>
|
||||
<T i18nKey="restore">#</T>
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
}
|
||||
)}
|
||||
{/* Mods can ban from community, and appoint as mods to community */}
|
||||
{this.canMod &&
|
||||
{this.canMod && (
|
||||
<>
|
||||
{!this.isMod &&
|
||||
{!this.isMod && (
|
||||
<li className="list-inline-item">
|
||||
{!node.comment.banned_from_community ?
|
||||
<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>
|
||||
}
|
||||
{!node.comment.banned_from_community ? (
|
||||
<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>
|
||||
}
|
||||
{!node.comment.banned_from_community &&
|
||||
)}
|
||||
{!node.comment.banned_from_community && (
|
||||
<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>
|
||||
}
|
||||
)}
|
||||
</>
|
||||
}
|
||||
)}
|
||||
{/* 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">
|
||||
{!this.state.showConfirmTransferCommunity ?
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferCommunity)}><T i18nKey="transfer_community">#</T>
|
||||
</span> : <>
|
||||
<span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
|
||||
<span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferCommunity)}><T i18nKey="yes">#</T></span>
|
||||
<span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferCommunity)}><T i18nKey="no">#</T></span>
|
||||
</>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
{/* Admins can ban from all, and appoint other admins */}
|
||||
{this.canAdmin &&
|
||||
{!this.state.showConfirmTransferCommunity ? (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleShowConfirmTransferCommunity
|
||||
)}
|
||||
>
|
||||
<T i18nKey="transfer_community">#</T>
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{!this.isAdmin &&
|
||||
<li className="list-inline-item">
|
||||
{!node.comment.banned ?
|
||||
<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>
|
||||
}
|
||||
{!node.comment.banned &&
|
||||
<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>
|
||||
</li>
|
||||
}
|
||||
<span class="d-inline-block mr-1">
|
||||
<T i18nKey="are_you_sure">#</T>
|
||||
</span>
|
||||
<span
|
||||
class="pointer d-inline-block mr-1"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleTransferCommunity
|
||||
)}
|
||||
>
|
||||
<T i18nKey="yes">#</T>
|
||||
</span>
|
||||
<span
|
||||
class="pointer d-inline-block"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleCancelShowConfirmTransferCommunity
|
||||
)}
|
||||
>
|
||||
<T i18nKey="no">#</T>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
{/* Admins can ban from all, and appoint other admins */}
|
||||
{this.canAdmin && (
|
||||
<>
|
||||
{!this.isAdmin && (
|
||||
<li className="list-inline-item">
|
||||
{!node.comment.banned ? (
|
||||
<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>
|
||||
)}
|
||||
{!node.comment.banned && (
|
||||
<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>
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* Site Creator can transfer to another admin */}
|
||||
{this.amSiteCreator && this.isAdmin &&
|
||||
{this.amSiteCreator && this.isAdmin && (
|
||||
<li className="list-inline-item">
|
||||
{!this.state.showConfirmTransferSite ?
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferSite)}><T i18nKey="transfer_site">#</T>
|
||||
</span> : <>
|
||||
<span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
|
||||
<span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferSite)}><T i18nKey="yes">#</T></span>
|
||||
<span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferSite)}><T i18nKey="no">#</T></span>
|
||||
{!this.state.showConfirmTransferSite ? (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleShowConfirmTransferSite
|
||||
)}
|
||||
>
|
||||
<T i18nKey="transfer_site">#</T>
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span class="d-inline-block mr-1">
|
||||
<T i18nKey="are_you_sure">#</T>
|
||||
</span>
|
||||
<span
|
||||
class="pointer d-inline-block mr-1"
|
||||
onClick={linkEvent(this, this.handleTransferSite)}
|
||||
>
|
||||
<T i18nKey="yes">#</T>
|
||||
</span>
|
||||
<span
|
||||
class="pointer d-inline-block"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleCancelShowConfirmTransferSite
|
||||
)}
|
||||
>
|
||||
<T i18nKey="no">#</T>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</li>
|
||||
}
|
||||
)}
|
||||
</>
|
||||
}
|
||||
)}
|
||||
<li className="list-inline-item">
|
||||
<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 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>
|
||||
{this.props.markable &&
|
||||
{this.props.markable && (
|
||||
<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>
|
||||
}
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
{this.state.showRemoveDialog &&
|
||||
<form class="form-inline" 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>
|
||||
{this.state.showRemoveDialog && (
|
||||
<form
|
||||
class="form-inline"
|
||||
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>
|
||||
}
|
||||
{this.state.showBanDialog &&
|
||||
)}
|
||||
{this.state.showBanDialog && (
|
||||
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label"><T i18nKey="reason">#</T></label>
|
||||
<input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
|
||||
<label class="col-form-label">
|
||||
<T i18nKey="reason">#</T>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control mr-2"
|
||||
placeholder={i18n.t('reason')}
|
||||
value={this.state.banReason}
|
||||
onInput={linkEvent(this, this.handleModBanReasonChange)}
|
||||
/>
|
||||
</div>
|
||||
{/* TODO hold off on expires until later */}
|
||||
{/* <div class="form-group row"> */}
|
||||
|
@ -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)} /> */}
|
||||
{/* </div> */}
|
||||
<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>
|
||||
</form>
|
||||
}
|
||||
{this.state.showReply &&
|
||||
)}
|
||||
{this.state.showReply && (
|
||||
<CommentForm
|
||||
node={node}
|
||||
onReplyCancel={this.handleReplyCancel}
|
||||
disabled={this.props.locked}
|
||||
/>
|
||||
}
|
||||
{node.children && !this.state.collapsed &&
|
||||
)}
|
||||
{node.children && !this.state.collapsed && (
|
||||
<CommentNodes
|
||||
nodes={node.children}
|
||||
locked={this.props.locked}
|
||||
|
@ -261,23 +506,38 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
admins={this.props.admins}
|
||||
postCreatorId={this.props.postCreatorId}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
{/* A collapsed clearfix */}
|
||||
{this.state.collapsed && <div class="row col-12"></div>}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
|
@ -285,38 +545,57 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
}
|
||||
|
||||
get canMod(): boolean {
|
||||
|
||||
if (this.props.admins && this.props.moderators) {
|
||||
let adminsThenMods = this.props.admins.map(a => a.id)
|
||||
let adminsThenMods = this.props.admins
|
||||
.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(
|
||||
UserService.Instance.user,
|
||||
adminsThenMods,
|
||||
this.props.node.comment.creator_id
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return this.props.moderators &&
|
||||
return (
|
||||
this.props.moderators &&
|
||||
UserService.Instance.user &&
|
||||
(this.props.node.comment.creator_id != UserService.Instance.user.id) &&
|
||||
(UserService.Instance.user.id == this.props.moderators[0].user_id);
|
||||
this.props.node.comment.creator_id != UserService.Instance.user.id &&
|
||||
UserService.Instance.user.id == this.props.moderators[0].user_id
|
||||
);
|
||||
}
|
||||
|
||||
get amSiteCreator(): boolean {
|
||||
return this.props.admins &&
|
||||
return (
|
||||
this.props.admins &&
|
||||
UserService.Instance.user &&
|
||||
(this.props.node.comment.creator_id != UserService.Instance.user.id) &&
|
||||
(UserService.Instance.user.id == this.props.admins[0].id);
|
||||
this.props.node.comment.creator_id != UserService.Instance.user.id &&
|
||||
UserService.Instance.user.id == this.props.admins[0].id
|
||||
);
|
||||
}
|
||||
|
||||
get commentUnlessRemoved(): string {
|
||||
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) {
|
||||
|
@ -337,16 +616,19 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
post_id: i.props.node.comment.post_id,
|
||||
parent_id: i.props.node.comment.parent_id,
|
||||
deleted: !i.props.node.comment.deleted,
|
||||
auth: null
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editComment(deleteForm);
|
||||
}
|
||||
|
||||
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 = {
|
||||
comment_id: i.props.node.comment.id,
|
||||
save: saved
|
||||
save: saved,
|
||||
};
|
||||
|
||||
WebSocketService.Instance.saveComment(form);
|
||||
|
@ -358,13 +640,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
this.setState(this.state);
|
||||
}
|
||||
|
||||
|
||||
handleCommentLike(i: CommentNodeI) {
|
||||
|
||||
let form: CommentLikeForm = {
|
||||
comment_id: i.comment.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);
|
||||
}
|
||||
|
@ -373,7 +653,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
let form: CommentLikeForm = {
|
||||
comment_id: i.comment.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);
|
||||
}
|
||||
|
@ -398,7 +678,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
parent_id: i.props.node.comment.parent_id,
|
||||
removed: !i.props.node.comment.removed,
|
||||
reason: i.state.removeReason,
|
||||
auth: null
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editComment(form);
|
||||
|
||||
|
@ -407,6 +687,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
}
|
||||
|
||||
handleMarkRead(i: CommentNode) {
|
||||
// if it has a user_mention_id field, then its a mention
|
||||
if (i.props.node.comment.user_mention_id) {
|
||||
let form: EditUserMentionForm = {
|
||||
user_mention_id: i.props.node.comment.user_mention_id,
|
||||
read: !i.props.node.comment.read,
|
||||
};
|
||||
WebSocketService.Instance.editUserMention(form);
|
||||
} else {
|
||||
let form: CommentFormI = {
|
||||
content: i.props.node.comment.content,
|
||||
edit_id: i.props.node.comment.id,
|
||||
|
@ -414,11 +702,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
post_id: i.props.node.comment.post_id,
|
||||
parent_id: i.props.node.comment.parent_id,
|
||||
read: !i.props.node.comment.read,
|
||||
auth: null
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editComment(form);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
handleModBanFromCommunityShow(i: CommentNode) {
|
||||
i.state.showBanDialog = true;
|
||||
|
|
26
ui/src/components/comment-nodes.tsx
vendored
26
ui/src/components/comment-nodes.tsx
vendored
|
@ -1,9 +1,12 @@
|
|||
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';
|
||||
|
||||
interface CommentNodesState {
|
||||
}
|
||||
interface CommentNodesState {}
|
||||
|
||||
interface CommentNodesProps {
|
||||
nodes: Array<CommentNodeI>;
|
||||
|
@ -16,8 +19,10 @@ interface CommentNodesProps {
|
|||
markable?: boolean;
|
||||
}
|
||||
|
||||
export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> {
|
||||
|
||||
export class CommentNodes extends Component<
|
||||
CommentNodesProps,
|
||||
CommentNodesState
|
||||
> {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
@ -25,8 +30,9 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
|
|||
render() {
|
||||
return (
|
||||
<div className="comments">
|
||||
{this.props.nodes.map(node =>
|
||||
<CommentNode node={node}
|
||||
{this.props.nodes.map(node => (
|
||||
<CommentNode
|
||||
node={node}
|
||||
noIndent={this.props.noIndent}
|
||||
viewOnly={this.props.viewOnly}
|
||||
locked={this.props.locked}
|
||||
|
@ -35,10 +41,8 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
|
|||
postCreatorId={this.props.postCreatorId}
|
||||
markable={this.props.markable}
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
149
ui/src/components/communities.tsx
vendored
149
ui/src/components/communities.tsx
vendored
|
@ -1,8 +1,16 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Subscription } from 'rxjs';
|
||||
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 { msgOp } from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
@ -22,25 +30,31 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
communities: [],
|
||||
loading: true,
|
||||
page: this.getPageFromProps(this.props),
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.state = this.emptyState;
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
this.refetch();
|
||||
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@ -48,7 +62,9 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
}
|
||||
|
||||
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
|
||||
|
@ -63,46 +79,92 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
{this.state.loading ?
|
||||
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
||||
{this.state.loading ? (
|
||||
<h5 class="">
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
</h5>
|
||||
) : (
|
||||
<div>
|
||||
<h5><T i18nKey="list_of_communities">#</T></h5>
|
||||
<h5>
|
||||
<T i18nKey="list_of_communities">#</T>
|
||||
</h5>
|
||||
<div class="table-responsive">
|
||||
<table id="community_table" class="table table-sm table-hover">
|
||||
<thead class="pointer">
|
||||
<tr>
|
||||
<th><T i18nKey="name">#</T></th>
|
||||
<th class="d-none d-lg-table-cell"><T i18nKey="title">#</T></th>
|
||||
<th><T i18nKey="category">#</T></th>
|
||||
<th class="text-right"><T i18nKey="subscribers">#</T></th>
|
||||
<th class="text-right d-none d-lg-table-cell"><T i18nKey="posts">#</T></th>
|
||||
<th class="text-right d-none d-lg-table-cell"><T i18nKey="comments">#</T></th>
|
||||
<th>
|
||||
<T i18nKey="name">#</T>
|
||||
</th>
|
||||
<th class="d-none d-lg-table-cell">
|
||||
<T i18nKey="title">#</T>
|
||||
</th>
|
||||
<th>
|
||||
<T i18nKey="category">#</T>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<T i18nKey="subscribers">#</T>
|
||||
</th>
|
||||
<th class="text-right d-none d-lg-table-cell">
|
||||
<T i18nKey="posts">#</T>
|
||||
</th>
|
||||
<th class="text-right d-none d-lg-table-cell">
|
||||
<T i18nKey="comments">#</T>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.state.communities.map(community =>
|
||||
{this.state.communities.map(community => (
|
||||
<tr>
|
||||
<td><Link to={`/c/${community.name}`}>{community.name}</Link></td>
|
||||
<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>
|
||||
}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
@ -110,10 +172,20 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
paginator() {
|
||||
return (
|
||||
<div class="mt-2">
|
||||
{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" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
|
||||
{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"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
>
|
||||
<T i18nKey="next">#</T>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -139,7 +211,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
handleUnsubscribe(communityId: number) {
|
||||
let form: FollowCommunityForm = {
|
||||
community_id: communityId,
|
||||
follow: false
|
||||
follow: false,
|
||||
};
|
||||
WebSocketService.Instance.followCommunity(form);
|
||||
}
|
||||
|
@ -147,7 +219,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
handleSubscribe(communityId: number) {
|
||||
let form: FollowCommunityForm = {
|
||||
community_id: communityId,
|
||||
follow: true
|
||||
follow: true,
|
||||
};
|
||||
WebSocketService.Instance.followCommunity(form);
|
||||
}
|
||||
|
@ -157,10 +229,9 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
sort: SortType[SortType.TopAll],
|
||||
limit: 100,
|
||||
page: this.state.page,
|
||||
}
|
||||
};
|
||||
|
||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||
|
||||
}
|
||||
|
||||
parseMessage(msg: any) {
|
||||
|
@ -172,7 +243,9 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
} else if (op == UserOperation.ListCommunities) {
|
||||
let res: ListCommunitiesResponse = msg;
|
||||
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;
|
||||
window.scrollTo(0, 0);
|
||||
this.setState(this.state);
|
||||
|
|
130
ui/src/components/community-form.tsx
vendored
130
ui/src/components/community-form.tsx
vendored
|
@ -1,7 +1,13 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Subscription } from 'rxjs';
|
||||
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 { msgOp, capitalizeFirstLetter } from '../utils';
|
||||
import * as autosize from 'autosize';
|
||||
|
@ -23,7 +29,10 @@ interface CommunityFormState {
|
|||
loading: boolean;
|
||||
}
|
||||
|
||||
export class CommunityForm extends Component<CommunityFormProps, CommunityFormState> {
|
||||
export class CommunityForm extends Component<
|
||||
CommunityFormProps,
|
||||
CommunityFormState
|
||||
> {
|
||||
private subscription: Subscription;
|
||||
|
||||
private emptyState: CommunityFormState = {
|
||||
|
@ -34,8 +43,8 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
|||
nsfw: false,
|
||||
},
|
||||
categories: [],
|
||||
loading: false
|
||||
}
|
||||
loading: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
@ -50,16 +59,23 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
|||
description: this.props.community.description,
|
||||
edit_id: this.props.community.id,
|
||||
nsfw: this.props.community.nsfw,
|
||||
auth: null
|
||||
}
|
||||
auth: null,
|
||||
};
|
||||
}
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
() => console.log("complete")
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
WebSocketService.Instance.listCategories();
|
||||
|
@ -73,53 +89,110 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
|||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
|
||||
<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">
|
||||
<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 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">
|
||||
<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 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">
|
||||
<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 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">
|
||||
<select class="form-control" value={this.state.communityForm.category_id} onInput={linkEvent(this, this.handleCommunityCategoryChange)}>
|
||||
{this.state.categories.map(category =>
|
||||
<select
|
||||
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>
|
||||
)}
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" checked={this.state.communityForm.nsfw} onChange={linkEvent(this, this.handleCommunityNsfwChange)}/>
|
||||
<label class="form-check-label"><T i18nKey="nsfw">#</T></label>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
checked={this.state.communityForm.nsfw}
|
||||
onChange={linkEvent(this, this.handleCommunityNsfwChange)}
|
||||
/>
|
||||
<label class="form-check-label">
|
||||
<T i18nKey="nsfw">#</T>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-secondary mr-2">
|
||||
{this.state.loading ?
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
|
||||
this.props.community ? 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>}
|
||||
{this.state.loading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</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>
|
||||
</form>
|
||||
|
@ -193,5 +266,4 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
|||
this.props.onEdit(res.community);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
154
ui/src/components/community.tsx
vendored
154
ui/src/components/community.tsx
vendored
|
@ -1,11 +1,30 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Subscription } from 'rxjs';
|
||||
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 { WebSocketService } from '../services';
|
||||
import {
|
||||
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 { SortSelect } from './sort-select';
|
||||
import { Sidebar } from './sidebar';
|
||||
import { msgOp, routeSortTypeToEnum, fetchLimit, postRefetchSeconds } from '../utils';
|
||||
import {
|
||||
msgOp,
|
||||
routeSortTypeToEnum,
|
||||
fetchLimit,
|
||||
postRefetchSeconds,
|
||||
} from '../utils';
|
||||
import { T, i18n } from 'inferno-i18next';
|
||||
|
||||
interface State {
|
||||
|
@ -21,7 +40,6 @@ interface State {
|
|||
}
|
||||
|
||||
export class Community extends Component<any, State> {
|
||||
|
||||
private subscription: Subscription;
|
||||
private postFetcher: any;
|
||||
private emptyState: State = {
|
||||
|
@ -49,28 +67,38 @@ export class Community extends Component<any, State> {
|
|||
posts: [],
|
||||
sort: this.getSortTypeFromProps(this.props),
|
||||
page: this.getPageFromProps(this.props),
|
||||
}
|
||||
};
|
||||
|
||||
getSortTypeFromProps(props: any): SortType {
|
||||
return (props.match.params.sort) ?
|
||||
routeSortTypeToEnum(props.match.params.sort) :
|
||||
SortType.Hot;
|
||||
return props.match.params.sort
|
||||
? routeSortTypeToEnum(props.match.params.sort)
|
||||
: UserService.Instance.user
|
||||
? UserService.Instance.user.default_sort_type
|
||||
: SortType.Hot;
|
||||
}
|
||||
|
||||
getPageFromProps(props: any): number {
|
||||
return (props.match.params.page) ? Number(props.match.params.page) : 1;
|
||||
return props.match.params.page ? Number(props.match.params.page) : 1;
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
this.handleSortChange = this.handleSortChange.bind(this);
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
|
@ -79,8 +107,6 @@ export class Community extends Component<any, State> {
|
|||
} else if (this.state.communityName) {
|
||||
WebSocketService.Instance.getCommunityByName(this.state.communityName);
|
||||
}
|
||||
|
||||
this.keepFetchingPosts();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -90,10 +116,13 @@ export class Community extends Component<any, State> {
|
|||
|
||||
// Necessary for back button for some reason
|
||||
componentWillReceiveProps(nextProps: any) {
|
||||
if (nextProps.history.action == 'POP') {
|
||||
this.state = this.emptyState;
|
||||
if (
|
||||
nextProps.history.action == 'POP' ||
|
||||
nextProps.history.action == 'PUSH'
|
||||
) {
|
||||
this.state.sort = this.getSortTypeFromProps(nextProps);
|
||||
this.state.page = this.getPageFromProps(nextProps);
|
||||
this.setState(this.state);
|
||||
this.fetchPosts();
|
||||
}
|
||||
}
|
||||
|
@ -101,17 +130,27 @@ export class Community extends Component<any, State> {
|
|||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
{this.state.loading ?
|
||||
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
</h5>
|
||||
) : (
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8">
|
||||
<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.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} />
|
||||
|
@ -125,36 +164,36 @@ export class Community extends Component<any, State> {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
selects() {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto">
|
||||
<option disabled><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 class="mb-2">
|
||||
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
paginator() {
|
||||
return (
|
||||
<div class="my-2">
|
||||
{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" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
|
||||
{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"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
>
|
||||
<T i18nKey="next">#</T>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -175,18 +214,21 @@ export class Community extends Component<any, State> {
|
|||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
handleSortChange(i: Community, event: any) {
|
||||
i.state.sort = Number(event.target.value);
|
||||
i.state.page = 1;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.fetchPosts();
|
||||
handleSortChange(val: SortType) {
|
||||
this.state.sort = val;
|
||||
this.state.page = 1;
|
||||
this.state.loading = true;
|
||||
this.setState(this.state);
|
||||
this.updateUrl();
|
||||
this.fetchPosts();
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
updateUrl() {
|
||||
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() {
|
||||
|
@ -201,7 +243,7 @@ export class Community extends Component<any, State> {
|
|||
sort: SortType[this.state.sort],
|
||||
type_: ListingType[ListingType.Community],
|
||||
community_id: this.state.community.id,
|
||||
}
|
||||
};
|
||||
WebSocketService.Instance.getPosts(getPostsForm);
|
||||
}
|
||||
|
||||
|
@ -218,7 +260,7 @@ export class Community extends Component<any, State> {
|
|||
this.state.admins = res.admins;
|
||||
document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`;
|
||||
this.setState(this.state);
|
||||
this.fetchPosts();
|
||||
this.keepFetchingPosts();
|
||||
} else if (op == UserOperation.EditCommunity) {
|
||||
let res: CommunityResponse = msg;
|
||||
this.state.community = res.community;
|
||||
|
@ -226,7 +268,8 @@ export class Community extends Component<any, State> {
|
|||
} else if (op == UserOperation.FollowCommunity) {
|
||||
let res: CommunityResponse = msg;
|
||||
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);
|
||||
} else if (op == UserOperation.GetPosts) {
|
||||
let res: GetPostsResponse = msg;
|
||||
|
@ -244,4 +287,3 @@ export class Community extends Component<any, State> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
13
ui/src/components/create-community.tsx
vendored
13
ui/src/components/create-community.tsx
vendored
|
@ -6,14 +6,15 @@ import { i18n } from '../i18next';
|
|||
import { T } from 'inferno-i18next';
|
||||
|
||||
export class CreateCommunity extends Component<any, any> {
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = `${i18n.t('create_community')} - ${WebSocketService.Instance.site.name}`;
|
||||
document.title = `${i18n.t('create_community')} - ${
|
||||
WebSocketService.Instance.site.name
|
||||
}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -21,17 +22,17 @@ export class CreateCommunity extends Component<any, any> {
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||
<h5><T i18nKey="create_community">#</T></h5>
|
||||
<h5>
|
||||
<T i18nKey="create_community">#</T>
|
||||
</h5>
|
||||
<CommunityForm onCreate={this.handleCommunityCreate} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
handleCommunityCreate(community: Community) {
|
||||
this.props.history.push(`/c/${community.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
25
ui/src/components/create-post.tsx
vendored
25
ui/src/components/create-post.tsx
vendored
|
@ -6,14 +6,15 @@ import { i18n } from '../i18next';
|
|||
import { T } from 'inferno-i18next';
|
||||
|
||||
export class CreatePost extends Component<any, any> {
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.handlePostCreate = this.handlePostCreate.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = `${i18n.t('create_post')} - ${WebSocketService.Instance.site.name}`;
|
||||
document.title = `${i18n.t('create_post')} - ${
|
||||
WebSocketService.Instance.site.name
|
||||
}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -21,21 +22,23 @@ export class CreatePost extends Component<any, any> {
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
get params(): PostFormParams {
|
||||
let urlParams = new URLSearchParams(this.props.location.search);
|
||||
let params: PostFormParams = {
|
||||
name: urlParams.get("name"),
|
||||
community: urlParams.get("community") || this.prevCommunityName,
|
||||
body: urlParams.get("body"),
|
||||
url: urlParams.get("url"),
|
||||
name: urlParams.get('name'),
|
||||
community: urlParams.get('community') || this.prevCommunityName,
|
||||
body: urlParams.get('body'),
|
||||
url: urlParams.get('url'),
|
||||
};
|
||||
|
||||
return params;
|
||||
|
@ -46,8 +49,8 @@ export class CreatePost extends Component<any, any> {
|
|||
return this.props.match.params.name;
|
||||
} else if (this.props.location.state) {
|
||||
let lastLocation = this.props.location.state.prevPath;
|
||||
if (lastLocation.includes("/c/")) {
|
||||
return lastLocation.split("/c/")[1];
|
||||
if (lastLocation.includes('/c/')) {
|
||||
return lastLocation.split('/c/')[1];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
@ -57,5 +60,3 @@ export class CreatePost extends Component<any, any> {
|
|||
this.props.history.push(`/post/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
19
ui/src/components/footer.tsx
vendored
19
ui/src/components/footer.tsx
vendored
|
@ -5,8 +5,6 @@ import { version } from '../version';
|
|||
import { T } from 'inferno-i18next';
|
||||
|
||||
export class Footer extends Component<any, any> {
|
||||
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
@ -20,16 +18,24 @@ export class Footer extends Component<any, any> {
|
|||
<span class="navbar-text">{version}</span>
|
||||
</li>
|
||||
<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 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 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 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>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -37,4 +43,3 @@ export class Footer extends Component<any, any> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
278
ui/src/components/inbox.tsx
vendored
278
ui/src/components/inbox.tsx
vendored
|
@ -1,45 +1,74 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Subscription } from 'rxjs';
|
||||
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 { msgOp } from '../utils';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
import { SortSelect } from './sort-select';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
enum UnreadOrAll {
|
||||
Unread,
|
||||
All,
|
||||
}
|
||||
|
||||
enum UnreadType {
|
||||
Unread, All
|
||||
Both,
|
||||
Replies,
|
||||
Mentions,
|
||||
}
|
||||
|
||||
interface InboxState {
|
||||
unreadOrAll: UnreadOrAll;
|
||||
unreadType: UnreadType;
|
||||
replies: Array<Comment>;
|
||||
mentions: Array<Comment>;
|
||||
sort: SortType;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export class Inbox extends Component<any, InboxState> {
|
||||
|
||||
private subscription: Subscription;
|
||||
private emptyState: InboxState = {
|
||||
unreadType: UnreadType.Unread,
|
||||
unreadOrAll: UnreadOrAll.Unread,
|
||||
unreadType: UnreadType.Both,
|
||||
replies: [],
|
||||
mentions: [],
|
||||
sort: SortType.New,
|
||||
page: 1,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
this.handleSortChange = this.handleSortChange.bind(this);
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
|
@ -51,7 +80,9 @@ export class Inbox extends Component<any, InboxState> {
|
|||
}
|
||||
|
||||
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() {
|
||||
|
@ -61,52 +92,125 @@ export class Inbox extends Component<any, InboxState> {
|
|||
<div class="row">
|
||||
<div class="col-12">
|
||||
<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>
|
||||
{this.state.replies.length > 0 && this.state.unreadType == UnreadType.Unread &&
|
||||
{this.state.replies.length + this.state.mentions.length > 0 &&
|
||||
this.state.unreadOrAll == UnreadOrAll.Unread && (
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
<li className="list-inline-item">
|
||||
<span class="pointer" onClick={this.markAllAsRead}><T i18nKey="mark_all_as_read">#</T></span>
|
||||
<span class="pointer" onClick={this.markAllAsRead}>
|
||||
<T i18nKey="mark_all_as_read">#</T>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
)}
|
||||
{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()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
selects() {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<select value={this.state.unreadType} onChange={linkEvent(this, this.handleUnreadTypeChange)} class="custom-select custom-select-sm w-auto">
|
||||
<option disabled><T i18nKey="type">#</T></option>
|
||||
<option value={UnreadType.Unread}><T i18nKey="unread">#</T></option>
|
||||
<option value={UnreadType.All}><T i18nKey="all">#</T></option>
|
||||
<select
|
||||
value={this.state.unreadOrAll}
|
||||
onChange={linkEvent(this, this.handleUnreadOrAllChange)}
|
||||
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 value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
|
||||
<option disabled><T i18nKey="sort_type">#</T></option>
|
||||
<option value={SortType.New}><T i18nKey="new">#</T></option>
|
||||
<option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
|
||||
<option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
|
||||
<option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
|
||||
<option value={SortType.TopYear}><T i18nKey="year">#</T></option>
|
||||
<option value={SortType.TopAll}><T i18nKey="all">#</T></option>
|
||||
<select
|
||||
value={this.state.unreadType}
|
||||
onChange={linkEvent(this, this.handleUnreadTypeChange)}
|
||||
class="custom-select custom-select-sm w-auto mr-2"
|
||||
>
|
||||
<option disabled>
|
||||
<T i18nKey="type">#</T>
|
||||
</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>
|
||||
<SortSelect
|
||||
sort={this.state.sort}
|
||||
onChange={this.handleSortChange}
|
||||
hideHot
|
||||
/>
|
||||
</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() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.replies.map(reply =>
|
||||
{this.state.replies.map(reply => (
|
||||
<CommentNodes nodes={[{ comment: reply }]} noIndent markable />
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
mentions() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.mentions.map(mention => (
|
||||
<CommentNodes nodes={[{ comment: mention }]} noIndent markable />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -114,10 +218,20 @@ export class Inbox extends Component<any, InboxState> {
|
|||
paginator() {
|
||||
return (
|
||||
<div class="mt-2">
|
||||
{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" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
|
||||
{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"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
>
|
||||
<T i18nKey="next">#</T>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -134,6 +248,13 @@ export class Inbox extends Component<any, InboxState> {
|
|||
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) {
|
||||
i.state.unreadType = Number(event.target.value);
|
||||
i.state.page = 1;
|
||||
|
@ -142,20 +263,28 @@ export class Inbox extends Component<any, InboxState> {
|
|||
}
|
||||
|
||||
refetch() {
|
||||
let form: GetRepliesForm = {
|
||||
let repliesForm: GetRepliesForm = {
|
||||
sort: SortType[this.state.sort],
|
||||
unread_only: (this.state.unreadType == UnreadType.Unread),
|
||||
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
|
||||
page: this.state.page,
|
||||
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) {
|
||||
i.state.sort = Number(event.target.value);
|
||||
i.state.page = 1;
|
||||
i.setState(i.state);
|
||||
i.refetch();
|
||||
handleSortChange(val: SortType) {
|
||||
this.state.sort = val;
|
||||
this.state.page = 1;
|
||||
this.setState(this.state);
|
||||
this.refetch();
|
||||
}
|
||||
|
||||
markAllAsRead() {
|
||||
|
@ -168,10 +297,21 @@ export class Inbox extends Component<any, InboxState> {
|
|||
if (msg.error) {
|
||||
alert(i18n.t(msg.error));
|
||||
return;
|
||||
} else if (op == UserOperation.GetReplies || op == UserOperation.MarkAllAsRead) {
|
||||
} else if (op == UserOperation.GetReplies) {
|
||||
let res: GetRepliesResponse = msg;
|
||||
this.state.replies = res.replies;
|
||||
this.sendRepliesCount();
|
||||
this.sendUnreadCount();
|
||||
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);
|
||||
} else if (op == UserOperation.EditComment) {
|
||||
|
@ -187,14 +327,38 @@ export class Inbox extends Component<any, InboxState> {
|
|||
found.score = res.comment.score;
|
||||
|
||||
// If youre in the unread view, just remove it from the list
|
||||
if (this.state.unreadType == UnreadType.Unread && res.comment.read) {
|
||||
this.state.replies = this.state.replies.filter(r => r.id !== res.comment.id);
|
||||
if (this.state.unreadOrAll == UnreadOrAll.Unread && res.comment.read) {
|
||||
this.state.replies = this.state.replies.filter(
|
||||
r => r.id !== res.comment.id
|
||||
);
|
||||
} else {
|
||||
let found = this.state.replies.find(c => c.id == res.comment.id);
|
||||
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);
|
||||
} else if (op == UserOperation.CreateComment) {
|
||||
// let res: CommentResponse = msg;
|
||||
|
@ -208,18 +372,24 @@ export class Inbox extends Component<any, InboxState> {
|
|||
this.setState(this.state);
|
||||
} else if (op == UserOperation.CreateCommentLike) {
|
||||
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.upvotes = res.comment.upvotes;
|
||||
found.downvotes = res.comment.downvotes;
|
||||
if (res.comment.my_vote !== null)
|
||||
found.my_vote = res.comment.my_vote;
|
||||
if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
|
||||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
|
||||
sendRepliesCount() {
|
||||
UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: this.state.replies.filter(r => !r.read).length});
|
||||
sendUnreadCount() {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
68
ui/src/components/listing-type-select.tsx
vendored
Normal file
68
ui/src/components/listing-type-select.tsx
vendored
Normal 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_);
|
||||
}
|
||||
}
|
158
ui/src/components/login.tsx
vendored
158
ui/src/components/login.tsx
vendored
|
@ -1,7 +1,12 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Subscription } from 'rxjs';
|
||||
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 { msgOp } from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
@ -14,14 +19,13 @@ interface State {
|
|||
registerLoading: boolean;
|
||||
}
|
||||
|
||||
|
||||
export class Login extends Component<any, State> {
|
||||
private subscription: Subscription;
|
||||
|
||||
emptyState: State = {
|
||||
loginForm: {
|
||||
username_or_email: undefined,
|
||||
password: undefined
|
||||
password: undefined,
|
||||
},
|
||||
registerForm: {
|
||||
username: undefined,
|
||||
|
@ -32,7 +36,7 @@ export class Login extends Component<any, State> {
|
|||
},
|
||||
loginLoading: false,
|
||||
registerLoading: false,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
@ -40,11 +44,18 @@ export class Login extends Component<any, State> {
|
|||
this.state = this.emptyState;
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
() => console.log("complete")
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -53,22 +64,20 @@ export class Login extends Component<any, State> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = `${i18n.t('login')} - ${WebSocketService.Instance.site.name}`;
|
||||
document.title = `${i18n.t('login')} - ${
|
||||
WebSocketService.Instance.site.name
|
||||
}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6 mb-4">
|
||||
{this.loginForm()}
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
{this.registerForm()}
|
||||
<div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
|
||||
<div class="col-12 col-lg-6">{this.registerForm()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
loginForm() {
|
||||
|
@ -77,21 +86,45 @@ export class Login extends Component<any, State> {
|
|||
<form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
|
||||
<h5>Login</h5>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label"><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">
|
||||
<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 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">
|
||||
<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 class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-secondary">{this.state.loginLoading ?
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('login')}</button>
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
{this.state.loginLoading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
) : (
|
||||
i18n.t('login')
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -101,43 +134,95 @@ export class Login extends Component<any, State> {
|
|||
registerForm() {
|
||||
return (
|
||||
<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">
|
||||
<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">
|
||||
<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 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">
|
||||
<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 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">
|
||||
<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 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">
|
||||
<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 class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" checked={this.state.registerForm.show_nsfw} onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}/>
|
||||
<label class="form-check-label"><T i18nKey="show_nsfw">#</T></label>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
checked={this.state.registerForm.show_nsfw}
|
||||
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
|
||||
/>
|
||||
<label class="form-check-label">
|
||||
<T i18nKey="show_nsfw">#</T>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-secondary">{this.state.registerLoading ?
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('sign_up')}</button>
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
{this.state.registerLoading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
) : (
|
||||
i18n.t('sign_up')
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -217,5 +302,4 @@ export class Login extends Component<any, State> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
378
ui/src/components/main.tsx
vendored
378
ui/src/components/main.tsx
vendored
|
@ -1,12 +1,37 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Subscription } from 'rxjs';
|
||||
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 { PostListings } from './post-listings';
|
||||
import { SortSelect } from './sort-select';
|
||||
import { ListingTypeSelect } from './listing-type-select';
|
||||
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 { T } from 'inferno-i18next';
|
||||
|
||||
|
@ -23,7 +48,6 @@ interface MainState {
|
|||
}
|
||||
|
||||
export class Main extends Component<any, MainState> {
|
||||
|
||||
private subscription: Subscription;
|
||||
private postFetcher: any;
|
||||
private emptyState: MainState = {
|
||||
|
@ -52,24 +76,26 @@ export class Main extends Component<any, MainState> {
|
|||
type_: this.getListingTypeFromProps(this.props),
|
||||
sort: this.getSortTypeFromProps(this.props),
|
||||
page: this.getPageFromProps(this.props),
|
||||
}
|
||||
};
|
||||
|
||||
getListingTypeFromProps(props: any): ListingType {
|
||||
return (props.match.params.type) ?
|
||||
routeListingTypeToEnum(props.match.params.type) :
|
||||
UserService.Instance.user ?
|
||||
ListingType.Subscribed :
|
||||
ListingType.All;
|
||||
return props.match.params.type
|
||||
? routeListingTypeToEnum(props.match.params.type)
|
||||
: UserService.Instance.user
|
||||
? UserService.Instance.user.default_listing_type
|
||||
: ListingType.All;
|
||||
}
|
||||
|
||||
getSortTypeFromProps(props: any): SortType {
|
||||
return (props.match.params.sort) ?
|
||||
routeSortTypeToEnum(props.match.params.sort) :
|
||||
SortType.Hot;
|
||||
return props.match.params.sort
|
||||
? routeSortTypeToEnum(props.match.params.sort)
|
||||
: UserService.Instance.user
|
||||
? UserService.Instance.user.default_sort_type
|
||||
: SortType.Hot;
|
||||
}
|
||||
|
||||
getPageFromProps(props: any): number {
|
||||
return (props.match.params.page) ? Number(props.match.params.page) : 1;
|
||||
return props.match.params.page ? Number(props.match.params.page) : 1;
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -77,12 +103,21 @@ export class Main extends Component<any, MainState> {
|
|||
|
||||
this.state = this.emptyState;
|
||||
this.handleEditCancel = this.handleEditCancel.bind(this);
|
||||
this.handleSortChange = this.handleSortChange.bind(this);
|
||||
this.handleTypeChange = this.handleTypeChange.bind(this);
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
|
@ -94,8 +129,8 @@ export class Main extends Component<any, MainState> {
|
|||
|
||||
let listCommunitiesForm: ListCommunitiesForm = {
|
||||
sort: SortType[SortType.Hot],
|
||||
limit: 6
|
||||
}
|
||||
limit: 6,
|
||||
};
|
||||
|
||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||
|
||||
|
@ -109,7 +144,10 @@ export class Main extends Component<any, MainState> {
|
|||
|
||||
// Necessary for back button for some reason
|
||||
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.sort = this.getSortTypeFromProps(nextProps);
|
||||
this.state.page = this.getPageFromProps(nextProps);
|
||||
|
@ -122,39 +160,47 @@ export class Main extends Component<any, MainState> {
|
|||
return (
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8">
|
||||
{this.posts()}
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
{this.my_sidebar()}
|
||||
<div class="col-12 col-md-8">{this.posts()}</div>
|
||||
<div class="col-12 col-md-4">{this.my_sidebar()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
my_sidebar() {
|
||||
return (
|
||||
<div>
|
||||
{!this.state.loading &&
|
||||
{!this.state.loading && (
|
||||
<div>
|
||||
<div class="card border-secondary mb-3">
|
||||
<div class="card-body">
|
||||
{this.trendingCommunities()}
|
||||
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
|
||||
{UserService.Instance.user &&
|
||||
this.state.subscribedCommunities.length > 0 && (
|
||||
<div>
|
||||
<h5>
|
||||
<T i18nKey="subscribed_to_communities">#<Link class="text-white" to="/communities">#</Link></T>
|
||||
<T i18nKey="subscribed_to_communities">
|
||||
#
|
||||
<Link class="text-white" to="/communities">
|
||||
#
|
||||
</Link>
|
||||
</T>
|
||||
</h5>
|
||||
<ul class="list-inline">
|
||||
{this.state.subscribedCommunities.map(community =>
|
||||
<li class="list-inline-item"><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
|
||||
)}
|
||||
{this.state.subscribedCommunities.map(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">
|
||||
)}
|
||||
<Link
|
||||
class="btn btn-sm btn-secondary btn-block"
|
||||
to="/create_community"
|
||||
>
|
||||
<T i18nKey="create_a_community">#</T>
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -162,44 +208,54 @@ export class Main extends Component<any, MainState> {
|
|||
{this.sidebar()}
|
||||
{this.landing()}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
trendingCommunities() {
|
||||
return (
|
||||
<div>
|
||||
<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>
|
||||
<ul class="list-inline">
|
||||
{this.state.trendingCommunities.map(community =>
|
||||
<li class="list-inline-item"><Link to={`/c/${community.name}`}>{community.name}</Link></li>
|
||||
)}
|
||||
{this.state.trendingCommunities.map(community => (
|
||||
<li class="list-inline-item">
|
||||
<Link to={`/c/${community.name}`}>{community.name}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
sidebar() {
|
||||
return (
|
||||
<div>
|
||||
{!this.state.showEditSite ?
|
||||
this.siteInfo() :
|
||||
{!this.state.showEditSite ? (
|
||||
this.siteInfo()
|
||||
) : (
|
||||
<SiteForm
|
||||
site={this.state.site.site}
|
||||
onCancel={this.handleEditCancel}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
updateUrl() {
|
||||
let typeStr = ListingType[this.state.type_].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() {
|
||||
|
@ -208,30 +264,66 @@ export class Main extends Component<any, MainState> {
|
|||
<div class="card border-secondary mb-3">
|
||||
<div class="card-body">
|
||||
<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">
|
||||
<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>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
)}
|
||||
<ul class="my-2 list-inline">
|
||||
<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 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 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 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 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 className="list-inline-item">
|
||||
<Link className="badge badge-secondary" to="/modlog">
|
||||
|
@ -241,23 +333,35 @@ export class Main extends Component<any, MainState> {
|
|||
</ul>
|
||||
<ul class="mt-1 list-inline small mb-0">
|
||||
<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>
|
||||
)}
|
||||
{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 &&
|
||||
{this.state.site.site.description && (
|
||||
<div class="card border-secondary mb-3">
|
||||
<div class="card-body">
|
||||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.site.site.description)} />
|
||||
<div
|
||||
className="md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(
|
||||
this.state.site.site.description
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
landing() {
|
||||
|
@ -265,87 +369,103 @@ export class Main extends Component<any, MainState> {
|
|||
<div class="card border-secondary">
|
||||
<div class="card-body">
|
||||
<h5>
|
||||
<T i18nKey="powered_by" class="d-inline">#</T>
|
||||
<svg class="icon mx-2"><use xlinkHref="#icon-mouse">#</use></svg>
|
||||
<a href={repoUrl}>Lemmy<sup>beta</sup></a>
|
||||
<T i18nKey="powered_by" class="d-inline">
|
||||
#
|
||||
</T>
|
||||
<svg class="icon mx-2">
|
||||
<use xlinkHref="#icon-mouse">#</use>
|
||||
</svg>
|
||||
<a href={repoUrl}>
|
||||
Lemmy<sup>beta</sup>
|
||||
</a>
|
||||
</h5>
|
||||
<p class="mb-0">
|
||||
<T i18nKey="landing_0">#<a href="https://en.wikipedia.org/wiki/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">
|
||||
#
|
||||
<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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
posts() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.loading ?
|
||||
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
</h5>
|
||||
) : (
|
||||
<div>
|
||||
{this.selects()}
|
||||
<PostListings posts={this.state.posts} showCommunity />
|
||||
{this.paginator()}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
selects() {
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<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}
|
||||
<ListingTypeSelect
|
||||
type_={this.state.type_}
|
||||
onChange={this.handleTypeChange}
|
||||
/>
|
||||
{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>
|
||||
<span class="ml-2">
|
||||
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
|
||||
</span>
|
||||
</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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
paginator() {
|
||||
return (
|
||||
<div class="my-2">
|
||||
{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" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
|
||||
{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"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
>
|
||||
<T i18nKey="next">#</T>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -376,23 +496,23 @@ export class Main extends Component<any, MainState> {
|
|||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
handleSortChange(i: Main, event: any) {
|
||||
i.state.sort = Number(event.target.value);
|
||||
i.state.page = 1;
|
||||
i.state.loading = true;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.fetchPosts();
|
||||
handleSortChange(val: SortType) {
|
||||
this.state.sort = val;
|
||||
this.state.page = 1;
|
||||
this.state.loading = true;
|
||||
this.setState(this.state);
|
||||
this.updateUrl();
|
||||
this.fetchPosts();
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
handleTypeChange(i: Main, event: any) {
|
||||
i.state.type_ = Number(event.target.value);
|
||||
i.state.page = 1;
|
||||
i.state.loading = true;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.fetchPosts();
|
||||
handleTypeChange(val: ListingType) {
|
||||
this.state.type_ = val;
|
||||
this.state.page = 1;
|
||||
this.state.loading = true;
|
||||
this.setState(this.state);
|
||||
this.updateUrl();
|
||||
this.fetchPosts();
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
|
@ -406,8 +526,8 @@ export class Main extends Component<any, MainState> {
|
|||
page: this.state.page,
|
||||
limit: fetchLimit,
|
||||
sort: SortType[this.state.sort],
|
||||
type_: ListingType[this.state.type_]
|
||||
}
|
||||
type_: ListingType[this.state.type_],
|
||||
};
|
||||
WebSocketService.Instance.getPosts(getPostsForm);
|
||||
}
|
||||
|
||||
|
@ -430,7 +550,7 @@ export class Main extends Component<any, MainState> {
|
|||
|
||||
// This means it hasn't been set up yet
|
||||
if (!res.site) {
|
||||
this.context.router.history.push("/setup");
|
||||
this.context.router.history.push('/setup');
|
||||
}
|
||||
this.state.site.admins = res.admins;
|
||||
this.state.site.site = res.site;
|
||||
|
@ -438,7 +558,6 @@ export class Main extends Component<any, MainState> {
|
|||
this.state.site.online = res.online;
|
||||
this.setState(this.state);
|
||||
document.title = `${WebSocketService.Instance.site.name}`;
|
||||
|
||||
} else if (op == UserOperation.EditSite) {
|
||||
let res: SiteResponse = msg;
|
||||
this.state.site.site = res.site;
|
||||
|
@ -460,4 +579,3 @@ export class Main extends Component<any, MainState> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
363
ui/src/components/modlog.tsx
vendored
363
ui/src/components/modlog.tsx
vendored
|
@ -1,8 +1,21 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Subscription } from 'rxjs';
|
||||
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 { msgOp, addTypeInfo, fetchLimit } from '../utils';
|
||||
import { MomentTime } from './moment-time';
|
||||
|
@ -10,9 +23,18 @@ import * as moment from 'moment';
|
|||
import { i18n } from '../i18next';
|
||||
|
||||
interface ModlogState {
|
||||
combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModStickyPost | ModRemoveCommunity | ModAdd | ModBan}>,
|
||||
communityId?: number,
|
||||
communityName?: string,
|
||||
combined: Array<{
|
||||
type_: string;
|
||||
data:
|
||||
| ModRemovePost
|
||||
| ModLockPost
|
||||
| ModStickyPost
|
||||
| ModRemoveCommunity
|
||||
| ModAdd
|
||||
| ModBan;
|
||||
}>;
|
||||
communityId?: number;
|
||||
communityName?: string;
|
||||
page: number;
|
||||
loading: boolean;
|
||||
}
|
||||
|
@ -23,18 +45,27 @@ export class Modlog extends Component<any, ModlogState> {
|
|||
combined: [],
|
||||
page: 1,
|
||||
loading: true,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
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
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
|
@ -50,15 +81,27 @@ export class Modlog extends Component<any, ModlogState> {
|
|||
}
|
||||
|
||||
setCombined(res: GetModlogResponse) {
|
||||
let removed_posts = addTypeInfo(res.removed_posts, "removed_posts");
|
||||
let locked_posts = addTypeInfo(res.locked_posts, "locked_posts");
|
||||
let stickied_posts = addTypeInfo(res.stickied_posts, "stickied_posts");
|
||||
let removed_comments = addTypeInfo(res.removed_comments, "removed_comments");
|
||||
let removed_communities = addTypeInfo(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");
|
||||
let removed_posts = addTypeInfo(res.removed_posts, 'removed_posts');
|
||||
let locked_posts = addTypeInfo(res.locked_posts, 'locked_posts');
|
||||
let stickied_posts = addTypeInfo(res.stickied_posts, 'stickied_posts');
|
||||
let removed_comments = addTypeInfo(
|
||||
res.removed_comments,
|
||||
'removed_comments'
|
||||
);
|
||||
let removed_communities = addTypeInfo(
|
||||
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.push(...removed_posts);
|
||||
|
@ -72,11 +115,14 @@ export class Modlog extends Component<any, ModlogState> {
|
|||
this.state.combined.push(...banned);
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
@ -84,97 +130,242 @@ export class Modlog extends Component<any, ModlogState> {
|
|||
combined() {
|
||||
return (
|
||||
<tbody>
|
||||
{this.state.combined.map(i =>
|
||||
{this.state.combined.map(i => (
|
||||
<tr>
|
||||
<td><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' &&
|
||||
<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'}
|
||||
<span> 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>
|
||||
<span>
|
||||
{' '}
|
||||
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'}
|
||||
<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'}
|
||||
<span> Post <Link to={`/post/${(i.data as ModStickyPost).post_id}`}>{(i.data as ModStickyPost).post_name}</Link></span>
|
||||
{(i.data as ModStickyPost).stickied
|
||||
? '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'}
|
||||
<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.data as ModRemoveComment).removed
|
||||
? 'Removed'
|
||||
: 'Restored'}
|
||||
<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'}
|
||||
<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.data as ModRemoveCommunity).removed
|
||||
? 'Removed'
|
||||
: 'Restored'}
|
||||
<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><Link to={`/u/${(i.data as ModBanFromCommunity).other_user_name}`}>{(i.data as ModBanFromCommunity).other_user_name}</Link></span>
|
||||
<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><Link 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>
|
||||
<span>
|
||||
<Link
|
||||
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><Link to={`/u/${(i.data as ModAddCommunity).other_user_name}`}>{(i.data as ModAddCommunity).other_user_name}</Link></span>
|
||||
<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><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><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>
|
||||
<span>
|
||||
{(i.data as ModBan).banned ? 'Banned ' : 'Unbanned '}{' '}
|
||||
</span>
|
||||
<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><Link to={`/u/${(i.data as ModAdd).other_user_name}`}>{(i.data as ModAdd).other_user_name}</Link></span>
|
||||
<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>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
))}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
{this.state.loading ?
|
||||
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
||||
{this.state.loading ? (
|
||||
<h5 class="">
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
</h5>
|
||||
) : (
|
||||
<div>
|
||||
<h5>
|
||||
{this.state.communityName && <Link className="text-white" to={`/c/${this.state.communityName}`}>/c/{this.state.communityName} </Link>}
|
||||
{this.state.communityName && (
|
||||
<Link
|
||||
className="text-white"
|
||||
to={`/c/${this.state.communityName}`}
|
||||
>
|
||||
/c/{this.state.communityName}{' '}
|
||||
</Link>
|
||||
)}
|
||||
<span>Modlog</span>
|
||||
</h5>
|
||||
<div class="table-responsive">
|
||||
|
@ -191,7 +382,7 @@ export class Modlog extends Component<any, ModlogState> {
|
|||
{this.paginator()}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -199,10 +390,20 @@ export class Modlog extends Component<any, ModlogState> {
|
|||
paginator() {
|
||||
return (
|
||||
<div class="mt-2">
|
||||
{this.state.page > 1 &&
|
||||
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
|
||||
}
|
||||
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
|
||||
{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"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
13
ui/src/components/moment-time.tsx
vendored
13
ui/src/components/moment-time.tsx
vendored
|
@ -8,11 +8,10 @@ interface MomentTimeProps {
|
|||
published?: string;
|
||||
when_?: string;
|
||||
updated?: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class MomentTime extends Component<MomentTimeProps, any> {
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
@ -24,13 +23,13 @@ export class MomentTime extends Component<MomentTimeProps, any> {
|
|||
render() {
|
||||
if (this.props.data.updated) {
|
||||
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 {
|
||||
let str = this.props.data.published || this.props.data.when_;
|
||||
return (
|
||||
<span title={str}>{moment.utc(str).fromNow()}</span>
|
||||
)
|
||||
return <span title={str}>{moment.utc(str).fromNow()}</span>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
201
ui/src/components/navbar.tsx
vendored
201
ui/src/components/navbar.tsx
vendored
|
@ -1,9 +1,18 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
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 { version } from '../version';
|
||||
import { i18n } from '../i18next';
|
||||
|
@ -13,8 +22,9 @@ interface NavbarState {
|
|||
isLoggedIn: boolean;
|
||||
expanded: boolean;
|
||||
expandUserDropdown: boolean;
|
||||
replies: Array<Comment>,
|
||||
fetchCount: number,
|
||||
replies: Array<Comment>;
|
||||
mentions: Array<Comment>;
|
||||
fetchCount: number;
|
||||
unreadCount: number;
|
||||
siteName: string;
|
||||
}
|
||||
|
@ -23,21 +33,22 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
private wsSub: Subscription;
|
||||
private userSub: Subscription;
|
||||
emptyState: NavbarState = {
|
||||
isLoggedIn: (UserService.Instance.user !== undefined),
|
||||
isLoggedIn: UserService.Instance.user !== undefined,
|
||||
unreadCount: 0,
|
||||
fetchCount: 0,
|
||||
replies: [],
|
||||
mentions: [],
|
||||
expanded: false,
|
||||
expandUserDropdown: false,
|
||||
siteName: undefined
|
||||
}
|
||||
siteName: undefined,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.state = this.emptyState;
|
||||
this.handleOverviewClick = this.handleOverviewClick.bind(this);
|
||||
|
||||
this.keepFetchingReplies();
|
||||
this.keepFetchingUnreads();
|
||||
|
||||
// Subscribe to user changes
|
||||
this.userSub = UserService.Instance.sub.subscribe(user => {
|
||||
|
@ -48,10 +59,17 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
});
|
||||
|
||||
this.wsSub = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
|
@ -63,9 +81,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>{this.navbar()}</div>
|
||||
)
|
||||
return <div>{this.navbar()}</div>;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -80,48 +96,98 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
<Link title={version} class="navbar-brand" to="/">
|
||||
{this.state.siteName}
|
||||
</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>
|
||||
</button>
|
||||
<div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}>
|
||||
<div
|
||||
className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
|
||||
>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<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 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 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 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>
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto mr-2">
|
||||
{this.state.isLoggedIn ?
|
||||
{this.state.isLoggedIn ? (
|
||||
<>
|
||||
{
|
||||
<li className="nav-item">
|
||||
<Link class="nav-link" to="/inbox">
|
||||
<svg class="icon"><use xlinkHref="#icon-mail"></use></svg>
|
||||
{this.state.unreadCount> 0 && <span class="ml-1 badge badge-light">{this.state.unreadCount}</span>}
|
||||
<svg class="icon">
|
||||
<use xlinkHref="#icon-mail"></use>
|
||||
</svg>
|
||||
{this.state.unreadCount > 0 && (
|
||||
<span class="ml-1 badge badge-light">
|
||||
{this.state.unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
}
|
||||
<li className={`nav-item dropdown ${this.state.expandUserDropdown && 'show'}`}>
|
||||
<a class="pointer nav-link dropdown-toggle" onClick={linkEvent(this, this.expandUserDropdown)} role="button">
|
||||
<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
|
||||
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>
|
||||
}
|
||||
) : (
|
||||
<Link class="nav-link" to="/login">
|
||||
<T i18nKey="login_sign_up">#</T>
|
||||
</Link>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
@ -154,7 +220,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
parseMessage(msg: any) {
|
||||
let op: UserOperation = msgOp(msg);
|
||||
if (msg.error) {
|
||||
if (msg.error == "not_logged_in") {
|
||||
if (msg.error == 'not_logged_in') {
|
||||
UserService.Instance.logout();
|
||||
location.reload();
|
||||
}
|
||||
|
@ -162,13 +228,31 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
} else if (op == UserOperation.GetReplies) {
|
||||
let res: GetRepliesResponse = msg;
|
||||
let unreadReplies = res.replies.filter(r => !r.read);
|
||||
if (unreadReplies.length > 0 && this.state.fetchCount > 1 &&
|
||||
(JSON.stringify(this.state.replies) !== JSON.stringify(unreadReplies))) {
|
||||
if (
|
||||
unreadReplies.length > 0 &&
|
||||
this.state.fetchCount > 1 &&
|
||||
JSON.stringify(this.state.replies) !== JSON.stringify(unreadReplies)
|
||||
) {
|
||||
this.notify(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) {
|
||||
let res: GetSiteResponse = msg;
|
||||
|
||||
|
@ -180,12 +264,12 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
}
|
||||
}
|
||||
|
||||
keepFetchingReplies() {
|
||||
this.fetchReplies();
|
||||
setInterval(() => this.fetchReplies(), 15000);
|
||||
keepFetchingUnreads() {
|
||||
this.fetchUnreads();
|
||||
setInterval(() => this.fetchUnreads(), 15000);
|
||||
}
|
||||
|
||||
fetchReplies() {
|
||||
fetchUnreads() {
|
||||
if (this.state.isLoggedIn) {
|
||||
let repliesForm: GetRepliesForm = {
|
||||
sort: SortType[SortType.New],
|
||||
|
@ -193,8 +277,16 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
page: 1,
|
||||
limit: 9999,
|
||||
};
|
||||
|
||||
let userMentionsForm: GetUserMentionsForm = {
|
||||
sort: SortType[SortType.New],
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: 9999,
|
||||
};
|
||||
if (this.currentLocation !== '/inbox') {
|
||||
WebSocketService.Instance.getReplies(repliesForm);
|
||||
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
||||
this.state.fetchCount++;
|
||||
}
|
||||
}
|
||||
|
@ -204,8 +296,18 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
return this.context.router.history.location.pathname;
|
||||
}
|
||||
|
||||
sendRepliesCount(res: GetRepliesResponse) {
|
||||
UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: res.replies.filter(r => !r.read).length});
|
||||
sendUnreadCount() {
|
||||
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() {
|
||||
|
@ -224,18 +326,21 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
|
||||
notify(replies: Array<Comment>) {
|
||||
let recentReply = replies[0];
|
||||
if (Notification.permission !== 'granted')
|
||||
Notification.requestPermission();
|
||||
if (Notification.permission !== 'granted') Notification.requestPermission();
|
||||
else {
|
||||
var notification = new Notification(`${replies.length} ${i18n.t('unread_messages')}`, {
|
||||
var notification = new Notification(
|
||||
`${replies.length} ${i18n.t('unread_messages')}`,
|
||||
{
|
||||
icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
|
||||
body: `${recentReply.creator_name}: ${recentReply.content}`
|
||||
});
|
||||
body: `${recentReply.creator_name}: ${recentReply.content}`,
|
||||
}
|
||||
);
|
||||
|
||||
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}`
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
257
ui/src/components/post-form.tsx
vendored
257
ui/src/components/post-form.tsx
vendored
|
@ -1,10 +1,31 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { PostListings } from './post-listings';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Subscription } from 'rxjs';
|
||||
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 { 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 { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
@ -29,7 +50,6 @@ interface PostFormState {
|
|||
}
|
||||
|
||||
export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
|
||||
private subscription: Subscription;
|
||||
private emptyState: PostFormState = {
|
||||
postForm: {
|
||||
|
@ -37,7 +57,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
nsfw: false,
|
||||
auth: 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: [],
|
||||
loading: false,
|
||||
|
@ -46,7 +68,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
suggestedTitle: undefined,
|
||||
suggestedPosts: [],
|
||||
crossPosts: [],
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
@ -62,8 +84,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
creator_id: this.props.post.creator_id,
|
||||
url: this.props.post.url,
|
||||
nsfw: this.props.post.nsfw,
|
||||
auth: null
|
||||
}
|
||||
auth: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.props.params) {
|
||||
|
@ -77,17 +99,24 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
}
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
let listCommunitiesForm: ListCommunitiesForm = {
|
||||
sort: SortType[SortType.TopAll],
|
||||
limit: 9999,
|
||||
}
|
||||
};
|
||||
|
||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||
}
|
||||
|
@ -105,79 +134,177 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
<div>
|
||||
<form onSubmit={linkEvent(this, this.handlePostSubmit)}>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label"><T i18nKey="url">#</T></label>
|
||||
<label class="col-sm-2 col-form-label">
|
||||
<T i18nKey="url">#</T>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="url" class="form-control" value={this.state.postForm.url} onInput={linkEvent(this, this.handlePostUrlChange)} />
|
||||
{this.state.suggestedTitle &&
|
||||
<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>
|
||||
}
|
||||
<input
|
||||
type="url"
|
||||
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>
|
||||
<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>
|
||||
<input id="file-upload" type="file" accept="image/*,video/*" name="file" class="d-none" disabled={!UserService.Instance.user} onChange={linkEvent(this, this.handleImageUpload)} />
|
||||
<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>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
name="file"
|
||||
class="d-none"
|
||||
disabled={!UserService.Instance.user}
|
||||
onChange={linkEvent(this, this.handleImageUpload)}
|
||||
/>
|
||||
</form>
|
||||
{this.state.imageLoading &&
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg>
|
||||
}
|
||||
{this.state.crossPosts.length > 0 &&
|
||||
{this.state.imageLoading && (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</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} />
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<textarea value={this.state.postForm.name} onInput={linkEvent(this, this.handlePostNameChange)} class="form-control" required rows={2} minLength={3} maxLength={100} />
|
||||
{this.state.suggestedPosts.length > 0 &&
|
||||
<textarea
|
||||
value={this.state.postForm.name}
|
||||
onInput={linkEvent(this, this.handlePostNameChange)}
|
||||
class="form-control"
|
||||
required
|
||||
rows={2}
|
||||
minLength={3}
|
||||
maxLength={100}
|
||||
/>
|
||||
{this.state.suggestedPosts.length > 0 && (
|
||||
<>
|
||||
<div class="my-1 text-muted small font-weight-bold"><T i18nKey="related_posts">#</T></div>
|
||||
<div class="my-1 text-muted small font-weight-bold">
|
||||
<T i18nKey="related_posts">#</T>
|
||||
</div>
|
||||
<PostListings posts={this.state.suggestedPosts} />
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label"><T i18nKey="body">#</T></label>
|
||||
<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} />
|
||||
{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>
|
||||
{!this.props.post &&
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label"><T i18nKey="community">#</T></label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}>
|
||||
{this.state.communities.map(community =>
|
||||
<option value={community.id}>{community.name}</option>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">
|
||||
<T i18nKey="body">#</T>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea
|
||||
value={this.state.postForm.body}
|
||||
onInput={linkEvent(this, this.handlePostBodyChange)}
|
||||
className={`form-control ${this.state.previewMode && 'd-none'}`}
|
||||
rows={4}
|
||||
maxLength={10000}
|
||||
/>
|
||||
{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>
|
||||
{!this.props.post && (
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">
|
||||
<T i18nKey="community">#</T>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select
|
||||
class="form-control"
|
||||
value={this.state.postForm.community_id}
|
||||
onInput={linkEvent(this, this.handlePostCommunityChange)}
|
||||
>
|
||||
{this.state.communities.map(community => (
|
||||
<option value={community.id}>{community.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" checked={this.state.postForm.nsfw} onChange={linkEvent(this, this.handlePostNsfwChange)}/>
|
||||
<label class="form-check-label"><T i18nKey="nsfw">#</T></label>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
checked={this.state.postForm.nsfw}
|
||||
onChange={linkEvent(this, this.handlePostNsfwChange)}
|
||||
/>
|
||||
<label class="form-check-label">
|
||||
<T i18nKey="nsfw">#</T>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-secondary mr-2">
|
||||
{this.state.loading ?
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
|
||||
this.props.post ? 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>}
|
||||
{this.state.loading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</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>
|
||||
</form>
|
||||
|
@ -205,7 +332,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
handlePostUrlChange(i: PostForm, event: any) {
|
||||
i.state.postForm.url = event.target.value;
|
||||
if (validURL(i.state.postForm.url)) {
|
||||
|
||||
let form: SearchForm = {
|
||||
q: i.state.postForm.url,
|
||||
type_: SearchType[SearchType.Url],
|
||||
|
@ -298,11 +424,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
alert(error);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
parseMessage(msg: any) {
|
||||
|
@ -318,7 +444,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
if (this.props.post) {
|
||||
this.state.postForm.community_id = this.props.post.community_id;
|
||||
} 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;
|
||||
} else {
|
||||
this.state.postForm.community_id = res.communities[0].id;
|
||||
|
@ -343,7 +471,4 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
672
ui/src/components/post-listing.tsx
vendored
672
ui/src/components/post-listing.tsx
vendored
|
@ -1,10 +1,31 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
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 { 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 { T } from 'inferno-i18next';
|
||||
|
||||
|
@ -32,7 +53,6 @@ interface PostListingProps {
|
|||
}
|
||||
|
||||
export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||
|
||||
private emptyState: PostListingState = {
|
||||
showEdit: false,
|
||||
showRemoveDialog: false,
|
||||
|
@ -45,7 +65,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
showConfirmTransferCommunity: false,
|
||||
imageExpanded: false,
|
||||
viewSource: false,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
@ -60,242 +80,505 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
render() {
|
||||
return (
|
||||
<div class="row">
|
||||
{!this.state.showEdit
|
||||
? this.listing()
|
||||
:
|
||||
{!this.state.showEdit ? (
|
||||
this.listing()
|
||||
) : (
|
||||
<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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
listing() {
|
||||
let post = this.props.post;
|
||||
return (
|
||||
<div class="listing col-12">
|
||||
<div className={`vote-bar mr-2 float-left small text-center ${this.props.viewOnly && 'no-click'}`}>
|
||||
<button className={`btn p-0 ${post.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostLike)}>
|
||||
<svg class="icon upvote"><use xlinkHref="#icon-arrow-up"></use></svg>
|
||||
<div
|
||||
className={`vote-bar mr-2 float-left small text-center ${this.props
|
||||
.viewOnly && 'no-click'}`}
|
||||
>
|
||||
<button
|
||||
className={`btn p-0 ${
|
||||
post.my_vote == 1 ? 'text-info' : 'text-muted'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePostLike)}
|
||||
>
|
||||
<svg class="icon upvote">
|
||||
<use xlinkHref="#icon-arrow-up"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<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)}>
|
||||
<svg class="icon downvote"><use xlinkHref="#icon-arrow-down"></use></svg>
|
||||
<button
|
||||
className={`btn p-0 ${
|
||||
post.my_vote == -1 ? 'text-danger' : 'text-muted'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePostDisLike)}
|
||||
>
|
||||
<svg class="icon downvote">
|
||||
<use xlinkHref="#icon-arrow-down"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{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>
|
||||
}
|
||||
{post.url && isVideo(post.url) &&
|
||||
<video playsinline muted loop controls class="mx-2 mt-1 float-left" height="100" width="150">
|
||||
{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>
|
||||
)}
|
||||
{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" />
|
||||
</video>
|
||||
}
|
||||
)}
|
||||
<div className="ml-4">
|
||||
<div className="post-title">
|
||||
<h5 className="mb-0 d-inline">
|
||||
{post.url ?
|
||||
<a 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>
|
||||
}
|
||||
{post.url ? (
|
||||
<a
|
||||
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>
|
||||
{post.url &&
|
||||
{post.url && (
|
||||
<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>
|
||||
}
|
||||
{ post.url && isImage(post.url) &&
|
||||
)}
|
||||
{post.url && isImage(post.url) && (
|
||||
<>
|
||||
{ !this.state.imageExpanded
|
||||
? <span class="text-monospace pointer ml-2 text-muted small" title={i18n.t('expand_here')} onClick={linkEvent(this, this.handleImageExpandClick)}>[+]</span>
|
||||
:
|
||||
{!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>
|
||||
<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>
|
||||
<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>
|
||||
</span>
|
||||
}
|
||||
)}
|
||||
</>
|
||||
}
|
||||
{post.removed &&
|
||||
<small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
|
||||
}
|
||||
{post.deleted &&
|
||||
<small className="ml-2 text-muted font-italic"><T i18nKey="deleted">#</T></small>
|
||||
}
|
||||
{post.locked &&
|
||||
<small className="ml-2 text-muted font-italic"><T i18nKey="locked">#</T></small>
|
||||
}
|
||||
{post.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>
|
||||
}
|
||||
)}
|
||||
{post.removed && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
<T i18nKey="removed">#</T>
|
||||
</small>
|
||||
)}
|
||||
{post.deleted && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
<T i18nKey="deleted">#</T>
|
||||
</small>
|
||||
)}
|
||||
{post.locked && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
<T i18nKey="locked">#</T>
|
||||
</small>
|
||||
)}
|
||||
{post.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 className="details ml-4">
|
||||
<ul class="list-inline mb-0 text-muted small">
|
||||
<li className="list-inline-item">
|
||||
<span>{i18n.t('by')} </span>
|
||||
<Link className="text-info" to={`/u/${post.creator_name}`}>{post.creator_name}</Link>
|
||||
{this.isMod &&
|
||||
<span className="mx-1 badge badge-light"><T i18nKey="mod">#</T></span>
|
||||
}
|
||||
{this.isAdmin &&
|
||||
<span className="mx-1 badge badge-light"><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 &&
|
||||
<Link className="text-info" to={`/u/${post.creator_name}`}>
|
||||
{post.creator_name}
|
||||
</Link>
|
||||
{this.isMod && (
|
||||
<span className="mx-1 badge badge-light">
|
||||
<T i18nKey="mod">#</T>
|
||||
</span>
|
||||
)}
|
||||
{this.isAdmin && (
|
||||
<span className="mx-1 badge badge-light">
|
||||
<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> {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>
|
||||
}
|
||||
)}
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<span><MomentTime data={post} /></span>
|
||||
<span>
|
||||
<MomentTime data={post} />
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<span>(
|
||||
<span className="text-info">+{post.upvotes}</span>
|
||||
<span>
|
||||
(<span className="text-info">+{post.upvotes}</span>
|
||||
<span> | </span>
|
||||
<span className="text-danger">-{post.downvotes}</span>
|
||||
<span>) </span>
|
||||
</span>
|
||||
</li>
|
||||
<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>
|
||||
</ul>
|
||||
<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">
|
||||
<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 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>
|
||||
</>
|
||||
}
|
||||
{this.myPost &&
|
||||
)}
|
||||
{this.myPost && (
|
||||
<>
|
||||
<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 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')}
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
}
|
||||
{this.canModOnSelf &&
|
||||
)}
|
||||
{this.canModOnSelf && (
|
||||
<>
|
||||
<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 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>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
{/* Mods can ban from community, and appoint as mods to community */}
|
||||
{(this.canMod || this.canAdmin) &&
|
||||
{(this.canMod || this.canAdmin) && (
|
||||
<li className="list-inline-item">
|
||||
{!post.removed ?
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
|
||||
}
|
||||
{!post.removed ? (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleModRemoveShow)}
|
||||
>
|
||||
<T i18nKey="remove">#</T>
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleModRemoveSubmit)}
|
||||
>
|
||||
<T i18nKey="restore">#</T>
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
}
|
||||
{this.canMod &&
|
||||
)}
|
||||
{this.canMod && (
|
||||
<>
|
||||
{!this.isMod &&
|
||||
{!this.isMod && (
|
||||
<li className="list-inline-item">
|
||||
{!post.banned_from_community ?
|
||||
<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>
|
||||
}
|
||||
{!post.banned_from_community ? (
|
||||
<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>
|
||||
}
|
||||
{!post.banned_from_community &&
|
||||
)}
|
||||
{!post.banned_from_community && (
|
||||
<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>
|
||||
}
|
||||
)}
|
||||
</>
|
||||
}
|
||||
)}
|
||||
{/* 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">
|
||||
{!this.state.showConfirmTransferCommunity ?
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferCommunity)}><T i18nKey="transfer_community">#</T>
|
||||
</span> : <>
|
||||
<span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
|
||||
<span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferCommunity)}><T i18nKey="yes">#</T></span>
|
||||
<span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferCommunity)}><T i18nKey="no">#</T></span>
|
||||
</>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
{/* Admins can ban from all, and appoint other admins */}
|
||||
{this.canAdmin &&
|
||||
{!this.state.showConfirmTransferCommunity ? (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleShowConfirmTransferCommunity
|
||||
)}
|
||||
>
|
||||
<T i18nKey="transfer_community">#</T>
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{!this.isAdmin &&
|
||||
<li className="list-inline-item">
|
||||
{!post.banned ?
|
||||
<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>
|
||||
}
|
||||
{!post.banned &&
|
||||
<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>
|
||||
</li>
|
||||
}
|
||||
<span class="d-inline-block mr-1">
|
||||
<T i18nKey="are_you_sure">#</T>
|
||||
</span>
|
||||
<span
|
||||
class="pointer d-inline-block mr-1"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleTransferCommunity
|
||||
)}
|
||||
>
|
||||
<T i18nKey="yes">#</T>
|
||||
</span>
|
||||
<span
|
||||
class="pointer d-inline-block"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleCancelShowConfirmTransferCommunity
|
||||
)}
|
||||
>
|
||||
<T i18nKey="no">#</T>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
{/* Admins can ban from all, and appoint other admins */}
|
||||
{this.canAdmin && (
|
||||
<>
|
||||
{!this.isAdmin && (
|
||||
<li className="list-inline-item">
|
||||
{!post.banned ? (
|
||||
<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>
|
||||
)}
|
||||
{!post.banned && (
|
||||
<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>
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* Site Creator can transfer to another admin */}
|
||||
{this.amSiteCreator && this.isAdmin &&
|
||||
{this.amSiteCreator && this.isAdmin && (
|
||||
<li className="list-inline-item">
|
||||
{!this.state.showConfirmTransferSite ?
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferSite)}><T i18nKey="transfer_site">#</T>
|
||||
</span> : <>
|
||||
<span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
|
||||
<span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferSite)}><T i18nKey="yes">#</T></span>
|
||||
<span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferSite)}><T i18nKey="no">#</T></span>
|
||||
{!this.state.showConfirmTransferSite ? (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleShowConfirmTransferSite
|
||||
)}
|
||||
>
|
||||
<T i18nKey="transfer_site">#</T>
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span class="d-inline-block mr-1">
|
||||
<T i18nKey="are_you_sure">#</T>
|
||||
</span>
|
||||
<span
|
||||
class="pointer d-inline-block mr-1"
|
||||
onClick={linkEvent(this, this.handleTransferSite)}
|
||||
>
|
||||
<T i18nKey="yes">#</T>
|
||||
</span>
|
||||
<span
|
||||
class="pointer d-inline-block"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleCancelShowConfirmTransferSite
|
||||
)}
|
||||
>
|
||||
<T i18nKey="no">#</T>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</li>
|
||||
}
|
||||
)}
|
||||
</>
|
||||
}
|
||||
{this.props.showBody && post.body &&
|
||||
)}
|
||||
{this.props.showBody && post.body && (
|
||||
<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>
|
||||
}
|
||||
)}
|
||||
</ul>
|
||||
{this.state.showRemoveDialog &&
|
||||
<form class="form-inline" 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>
|
||||
{this.state.showRemoveDialog && (
|
||||
<form
|
||||
class="form-inline"
|
||||
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>
|
||||
}
|
||||
{this.state.showBanDialog &&
|
||||
)}
|
||||
{this.state.showBanDialog && (
|
||||
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label"><T i18nKey="reason">#</T></label>
|
||||
<input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
|
||||
<label class="col-form-label">
|
||||
<T i18nKey="reason">#</T>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control mr-2"
|
||||
placeholder={i18n.t('reason')}
|
||||
value={this.state.banReason}
|
||||
onInput={linkEvent(this, this.handleModBanReasonChange)}
|
||||
/>
|
||||
</div>
|
||||
{/* TODO hold off on expires until later */}
|
||||
{/* <div class="form-group row"> */}
|
||||
|
@ -303,40 +586,64 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
{/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
|
||||
{/* </div> */}
|
||||
<div class="form-group row">
|
||||
<button type="submit" class="btn btn-secondary">{i18n.t('ban')} {post.creator_name}</button>
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
{i18n.t('ban')} {post.creator_name}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
{this.props.showBody && post.body &&
|
||||
)}
|
||||
{this.props.showBody && post.body && (
|
||||
<>
|
||||
{this.state.viewSource ? <pre>{post.body}</pre> :
|
||||
<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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
if (this.props.admins && this.props.moderators) {
|
||||
let adminsThenMods = this.props.admins.map(a => a.id)
|
||||
let adminsThenMods = this.props.admins
|
||||
.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(
|
||||
UserService.Instance.user,
|
||||
adminsThenMods,
|
||||
this.props.post.creator_id
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -344,38 +651,54 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
|
||||
get canModOnSelf(): boolean {
|
||||
if (this.props.admins && this.props.moderators) {
|
||||
let adminsThenMods = this.props.admins.map(a => a.id)
|
||||
let adminsThenMods = this.props.admins
|
||||
.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 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return this.props.moderators &&
|
||||
return (
|
||||
this.props.moderators &&
|
||||
UserService.Instance.user &&
|
||||
(this.props.post.creator_id != UserService.Instance.user.id) &&
|
||||
(UserService.Instance.user.id == this.props.moderators[0].user_id);
|
||||
this.props.post.creator_id != UserService.Instance.user.id &&
|
||||
UserService.Instance.user.id == this.props.moderators[0].user_id
|
||||
);
|
||||
}
|
||||
|
||||
get amSiteCreator(): boolean {
|
||||
return this.props.admins &&
|
||||
return (
|
||||
this.props.admins &&
|
||||
UserService.Instance.user &&
|
||||
(this.props.post.creator_id != UserService.Instance.user.id) &&
|
||||
(UserService.Instance.user.id == this.props.admins[0].id);
|
||||
this.props.post.creator_id != UserService.Instance.user.id &&
|
||||
UserService.Instance.user.id == this.props.admins[0].id
|
||||
);
|
||||
}
|
||||
|
||||
handlePostLike(i: PostListing) {
|
||||
|
||||
let form: CreatePostLikeForm = {
|
||||
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);
|
||||
}
|
||||
|
@ -383,7 +706,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
handlePostDisLike(i: PostListing) {
|
||||
let form: CreatePostLikeForm = {
|
||||
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);
|
||||
}
|
||||
|
@ -414,16 +737,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
creator_id: i.props.post.creator_id,
|
||||
deleted: !i.props.post.deleted,
|
||||
nsfw: i.props.post.nsfw,
|
||||
auth: null
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editPost(deleteForm);
|
||||
}
|
||||
|
||||
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 = {
|
||||
post_id: i.props.post.id,
|
||||
save: saved
|
||||
save: saved,
|
||||
};
|
||||
|
||||
WebSocketService.Instance.savePost(form);
|
||||
|
@ -622,4 +945,3 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
i.setState(i.state);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
30
ui/src/components/post-listings.tsx
vendored
30
ui/src/components/post-listings.tsx
vendored
|
@ -10,7 +10,6 @@ interface PostListingsProps {
|
|||
}
|
||||
|
||||
export class PostListings extends Component<PostListingsProps, any> {
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
@ -18,19 +17,32 @@ export class PostListings extends Component<PostListingsProps, any> {
|
|||
render() {
|
||||
return (
|
||||
<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} />
|
||||
<PostListing
|
||||
post={post}
|
||||
showCommunity={this.props.showCommunity}
|
||||
/>
|
||||
<hr class="d-md-none my-2" />
|
||||
<div class="d-none d-md-block my-2"></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>
|
||||
<T i18nKey="no_posts">#</T>
|
||||
</div>
|
||||
)
|
||||
{this.props.showCommunity !== undefined && (
|
||||
<div>
|
||||
<T i18nKey="subscribe_to_communities">
|
||||
#<Link to="/communities">#</Link>
|
||||
</T>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
205
ui/src/components/post.tsx
vendored
205
ui/src/components/post.tsx
vendored
|
@ -1,7 +1,32 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, BanUserResponse, AddModToCommunityResponse, AddAdminResponse, UserView, 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 { msgOp, hotRank } from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
|
@ -27,7 +52,6 @@ interface PostState {
|
|||
}
|
||||
|
||||
export class Post extends Component<any, PostState> {
|
||||
|
||||
private subscription: Subscription;
|
||||
private emptyState: PostState = {
|
||||
post: null,
|
||||
|
@ -39,7 +63,7 @@ export class Post extends Component<any, PostState> {
|
|||
scrolled: false,
|
||||
loading: true,
|
||||
crossPosts: [],
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
@ -52,10 +76,17 @@ export class Post extends Component<any, PostState> {
|
|||
}
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
|
@ -71,10 +102,16 @@ export class Post extends Component<any, PostState> {
|
|||
}
|
||||
|
||||
componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
|
||||
if (this.state.scrolled_comment_id && !this.state.scrolled && lastState.comments.length > 0) {
|
||||
var elmnt = document.getElementById(`comment-${this.state.scrolled_comment_id}`);
|
||||
if (
|
||||
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.classList.add("mark");
|
||||
elmnt.classList.add('mark');
|
||||
this.state.scrolled = true;
|
||||
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.refresh();
|
||||
// this.context.router.history.push(_lastProps.location.pathname);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
markScrolledAsRead(commentId: number) {
|
||||
let found = this.state.comments.find(c => c.id == commentId);
|
||||
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;
|
||||
|
||||
if (UserService.Instance.user && UserService.Instance.user.id == parent_user_id) {
|
||||
let parent_user_id = parent
|
||||
? parent.creator_id
|
||||
: this.state.post.creator_id;
|
||||
|
||||
if (
|
||||
UserService.Instance.user &&
|
||||
UserService.Instance.user.id == parent_user_id
|
||||
) {
|
||||
let form: CommentFormI = {
|
||||
content: found.content,
|
||||
edit_id: found.id,
|
||||
|
@ -107,7 +147,7 @@ export class Post extends Component<any, PostState> {
|
|||
post_id: found.post_id,
|
||||
parent_id: found.parent_id,
|
||||
read: true,
|
||||
auth: null
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editComment(form);
|
||||
}
|
||||
|
@ -116,8 +156,13 @@ export class Post extends Component<any, PostState> {
|
|||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
{this.state.loading ?
|
||||
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
</h5>
|
||||
) : (
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 mb-3">
|
||||
<PostListing
|
||||
|
@ -128,14 +173,19 @@ export class Post extends Component<any, PostState> {
|
|||
moderators={this.state.moderators}
|
||||
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} />
|
||||
</>
|
||||
}
|
||||
)}
|
||||
<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.commentsTree()}
|
||||
</div>
|
||||
|
@ -144,39 +194,62 @@ export class Post extends Component<any, PostState> {
|
|||
{this.sidebar()}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
sortRadios() {
|
||||
return (
|
||||
<div class="btn-group btn-group-toggle mb-3">
|
||||
<label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Hot && 'active'}`}>{i18n.t('hot')}
|
||||
<input type="radio" value={CommentSortType.Hot}
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${this.state
|
||||
.commentSort === CommentSortType.Hot && 'active'}`}
|
||||
>
|
||||
{i18n.t('hot')}
|
||||
<input
|
||||
type="radio"
|
||||
value={CommentSortType.Hot}
|
||||
checked={this.state.commentSort === CommentSortType.Hot}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)} />
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
</label>
|
||||
<label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Top && 'active'}`}>{i18n.t('top')}
|
||||
<input type="radio" value={CommentSortType.Top}
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${this.state
|
||||
.commentSort === CommentSortType.Top && 'active'}`}
|
||||
>
|
||||
{i18n.t('top')}
|
||||
<input
|
||||
type="radio"
|
||||
value={CommentSortType.Top}
|
||||
checked={this.state.commentSort === CommentSortType.Top}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)} />
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
</label>
|
||||
<label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.New && 'active'}`}>{i18n.t('new')}
|
||||
<input type="radio" value={CommentSortType.New}
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${this.state
|
||||
.commentSort === CommentSortType.New && 'active'}`}
|
||||
>
|
||||
{i18n.t('new')}
|
||||
<input
|
||||
type="radio"
|
||||
value={CommentSortType.New}
|
||||
checked={this.state.commentSort === CommentSortType.New}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)} />
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
newComments() {
|
||||
return (
|
||||
<div class="d-none d-md-block new-comments mb-3 card border-secondary">
|
||||
<div class="card-body small">
|
||||
<h6><T i18nKey="recent_comments">#</T></h6>
|
||||
{this.state.comments.map(comment =>
|
||||
<h6>
|
||||
<T i18nKey="recent_comments">#</T>
|
||||
</h6>
|
||||
{this.state.comments.map(comment => (
|
||||
<CommentNodes
|
||||
nodes={[{ comment: comment }]}
|
||||
noIndent
|
||||
|
@ -185,10 +258,10 @@ export class Post extends Component<any, PostState> {
|
|||
admins={this.state.admins}
|
||||
postCreatorId={this.state.post.creator_id}
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
sidebar() {
|
||||
|
@ -213,7 +286,7 @@ export class Post extends Component<any, PostState> {
|
|||
for (let comment of this.state.comments) {
|
||||
let node: CommentNodeI = {
|
||||
comment: comment,
|
||||
children: []
|
||||
children: [],
|
||||
};
|
||||
map.set(comment.id, { ...node });
|
||||
}
|
||||
|
@ -221,8 +294,7 @@ export class Post extends Component<any, PostState> {
|
|||
for (let comment of this.state.comments) {
|
||||
if (comment.parent_id) {
|
||||
map.get(comment.parent_id).children.push(map.get(comment.id));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
tree.push(map.get(comment.id));
|
||||
}
|
||||
}
|
||||
|
@ -233,26 +305,33 @@ export class Post extends Component<any, PostState> {
|
|||
}
|
||||
|
||||
sortTree(tree: Array<CommentNodeI>) {
|
||||
|
||||
// First, put removed and deleted comments at the bottom, then do your other sorts
|
||||
if (this.state.commentSort == CommentSortType.Top) {
|
||||
tree.sort((a, b) => (+a.comment.removed - +b.comment.removed) ||
|
||||
(+a.comment.deleted - +b.comment.deleted ) ||
|
||||
(b.comment.score - a.comment.score));
|
||||
tree.sort(
|
||||
(a, b) =>
|
||||
+a.comment.removed - +b.comment.removed ||
|
||||
+a.comment.deleted - +b.comment.deleted ||
|
||||
b.comment.score - a.comment.score
|
||||
);
|
||||
} else if (this.state.commentSort == CommentSortType.New) {
|
||||
tree.sort((a, b) => (+a.comment.removed - +b.comment.removed) ||
|
||||
(+a.comment.deleted - +b.comment.deleted ) ||
|
||||
(b.comment.published.localeCompare(a.comment.published)));
|
||||
tree.sort(
|
||||
(a, b) =>
|
||||
+a.comment.removed - +b.comment.removed ||
|
||||
+a.comment.deleted - +b.comment.deleted ||
|
||||
b.comment.published.localeCompare(a.comment.published)
|
||||
);
|
||||
} else if (this.state.commentSort == CommentSortType.Hot) {
|
||||
tree.sort((a, b) => (+a.comment.removed - +b.comment.removed) ||
|
||||
(+a.comment.deleted - +b.comment.deleted ) ||
|
||||
(hotRank(b.comment) - hotRank(a.comment)));
|
||||
tree.sort(
|
||||
(a, b) =>
|
||||
+a.comment.removed - +b.comment.removed ||
|
||||
+a.comment.deleted - +b.comment.deleted ||
|
||||
hotRank(b.comment) - hotRank(a.comment)
|
||||
);
|
||||
}
|
||||
|
||||
for (let node of tree) {
|
||||
this.sortTree(node.children);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
commentsTree() {
|
||||
|
@ -323,12 +402,13 @@ export class Post extends Component<any, PostState> {
|
|||
this.setState(this.state);
|
||||
} else if (op == UserOperation.CreateCommentLike) {
|
||||
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.upvotes = res.comment.upvotes;
|
||||
found.downvotes = res.comment.downvotes;
|
||||
if (res.comment.my_vote !== null)
|
||||
found.my_vote = res.comment.my_vote;
|
||||
if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.CreatePostLike) {
|
||||
let res: CreatePostLikeResponse = msg;
|
||||
|
@ -354,12 +434,14 @@ export class Post extends Component<any, PostState> {
|
|||
} else if (op == UserOperation.FollowCommunity) {
|
||||
let res: CommunityResponse = msg;
|
||||
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);
|
||||
} else if (op == UserOperation.BanFromCommunity) {
|
||||
let res: BanFromCommunityResponse = msg;
|
||||
this.state.comments.filter(c => c.creator_id == res.user.id)
|
||||
.forEach(c => c.banned_from_community = res.banned);
|
||||
this.state.comments
|
||||
.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) {
|
||||
this.state.post.banned_from_community = res.banned;
|
||||
}
|
||||
|
@ -370,8 +452,9 @@ export class Post extends Component<any, PostState> {
|
|||
this.setState(this.state);
|
||||
} else if (op == UserOperation.BanUser) {
|
||||
let res: BanUserResponse = msg;
|
||||
this.state.comments.filter(c => c.creator_id == res.user.id)
|
||||
.forEach(c => c.banned = res.banned);
|
||||
this.state.comments
|
||||
.filter(c => c.creator_id == res.user.id)
|
||||
.forEach(c => (c.banned = res.banned));
|
||||
if (this.state.post.creator_id == res.user.id) {
|
||||
this.state.post.banned = res.banned;
|
||||
}
|
||||
|
@ -396,9 +479,5 @@ export class Post extends Component<any, PostState> {
|
|||
this.state.admins = res.admins;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
344
ui/src/components/search.tsx
vendored
344
ui/src/components/search.tsx
vendored
|
@ -1,26 +1,41 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Subscription } from 'rxjs';
|
||||
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 { msgOp, fetchLimit, routeSearchTypeToEnum, routeSortTypeToEnum } from '../utils';
|
||||
import {
|
||||
msgOp,
|
||||
fetchLimit,
|
||||
routeSearchTypeToEnum,
|
||||
routeSortTypeToEnum,
|
||||
} from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
import { SortSelect } from './sort-select';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
interface SearchState {
|
||||
q: string,
|
||||
type_: SearchType,
|
||||
sort: SortType,
|
||||
page: number,
|
||||
q: string;
|
||||
type_: SearchType;
|
||||
sort: SortType;
|
||||
page: number;
|
||||
searchResponse: SearchResponse;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class Search extends Component<any, SearchState> {
|
||||
|
||||
private subscription: Subscription;
|
||||
private emptyState: SearchState = {
|
||||
q: this.getSearchQueryFromProps(this.props),
|
||||
|
@ -36,45 +51,52 @@ export class Search extends Component<any, SearchState> {
|
|||
users: [],
|
||||
},
|
||||
loading: false,
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
return (props.match.params.type) ?
|
||||
routeSearchTypeToEnum(props.match.params.type) :
|
||||
SearchType.All;
|
||||
return props.match.params.type
|
||||
? routeSearchTypeToEnum(props.match.params.type)
|
||||
: SearchType.All;
|
||||
}
|
||||
|
||||
getSortTypeFromProps(props: any): SortType {
|
||||
return (props.match.params.sort) ?
|
||||
routeSortTypeToEnum(props.match.params.sort) :
|
||||
SortType.TopAll;
|
||||
return props.match.params.sort
|
||||
? routeSortTypeToEnum(props.match.params.sort)
|
||||
: SortType.TopAll;
|
||||
}
|
||||
|
||||
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) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
this.handleSortChange = this.handleSortChange.bind(this);
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
if (this.state.q) {
|
||||
this.search();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -83,7 +105,10 @@ export class Search extends Component<any, SearchState> {
|
|||
|
||||
// Necessary for back button for some reason
|
||||
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.q = this.getSearchQueryFromProps(nextProps);
|
||||
this.state.type_ = this.getSearchTypeFromProps(nextProps);
|
||||
|
@ -95,7 +120,9 @@ export class Search extends Component<any, SearchState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = `${i18n.t('search')} - ${WebSocketService.Instance.site.name}`;
|
||||
document.title = `${i18n.t('search')} - ${
|
||||
WebSocketService.Instance.site.name
|
||||
}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -103,77 +130,109 @@ export class Search extends Component<any, SearchState> {
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h5><T i18nKey="search">#</T></h5>
|
||||
<h5>
|
||||
<T i18nKey="search">#</T>
|
||||
</h5>
|
||||
{this.selects()}
|
||||
{this.searchForm()}
|
||||
{this.state.type_ == SearchType.All &&
|
||||
this.all()
|
||||
}
|
||||
{this.state.type_ == SearchType.Comments &&
|
||||
this.comments()
|
||||
}
|
||||
{this.state.type_ == SearchType.Posts &&
|
||||
this.posts()
|
||||
}
|
||||
{this.state.type_ == SearchType.Communities &&
|
||||
this.communities()
|
||||
}
|
||||
{this.state.type_ == SearchType.Users &&
|
||||
this.users()
|
||||
}
|
||||
{this.state.type_ == SearchType.All && this.all()}
|
||||
{this.state.type_ == SearchType.Comments && this.comments()}
|
||||
{this.state.type_ == SearchType.Posts && this.posts()}
|
||||
{this.state.type_ == SearchType.Communities && this.communities()}
|
||||
{this.state.type_ == SearchType.Users && this.users()}
|
||||
{this.noResults()}
|
||||
{this.paginator()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
searchForm() {
|
||||
return (
|
||||
<form 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} />
|
||||
<form
|
||||
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">
|
||||
{this.state.loading ?
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
|
||||
<span><T i18nKey="search">#</T></span>
|
||||
}
|
||||
{this.state.loading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
) : (
|
||||
<span>
|
||||
<T i18nKey="search">#</T>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
selects() {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="custom-select custom-select-sm w-auto">
|
||||
<option disabled><T i18nKey="type">#</T></option>
|
||||
<option value={SearchType.All}><T i18nKey="all">#</T></option>
|
||||
<option value={SearchType.Comments}><T i18nKey="comments">#</T></option>
|
||||
<option value={SearchType.Posts}><T i18nKey="posts">#</T></option>
|
||||
<option value={SearchType.Communities}><T i18nKey="communities">#</T></option>
|
||||
<option value={SearchType.Users}><T i18nKey="users">#</T></option>
|
||||
</select>
|
||||
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
|
||||
<option disabled><T i18nKey="sort_type">#</T></option>
|
||||
<option value={SortType.New}><T i18nKey="new">#</T></option>
|
||||
<option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
|
||||
<option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
|
||||
<option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
|
||||
<option value={SortType.TopYear}><T i18nKey="year">#</T></option>
|
||||
<option value={SortType.TopAll}><T i18nKey="all">#</T></option>
|
||||
<select
|
||||
value={this.state.type_}
|
||||
onChange={linkEvent(this, this.handleTypeChange)}
|
||||
class="custom-select custom-select-sm w-auto"
|
||||
>
|
||||
<option disabled>
|
||||
<T i18nKey="type">#</T>
|
||||
</option>
|
||||
<option value={SearchType.All}>
|
||||
<T i18nKey="all">#</T>
|
||||
</option>
|
||||
<option value={SearchType.Comments}>
|
||||
<T i18nKey="comments">#</T>
|
||||
</option>
|
||||
<option value={SearchType.Posts}>
|
||||
<T i18nKey="posts">#</T>
|
||||
</option>
|
||||
<option value={SearchType.Communities}>
|
||||
<T i18nKey="communities">#</T>
|
||||
</option>
|
||||
<option value={SearchType.Users}>
|
||||
<T i18nKey="users">#</T>
|
||||
</option>
|
||||
</select>
|
||||
<span class="ml-2">
|
||||
<SortSelect
|
||||
sort={this.state.sort}
|
||||
onChange={this.handleSortChange}
|
||||
hideHot
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
all() {
|
||||
let combined: Array<{type_: string, data: Comment | Post | Community | UserView}> = [];
|
||||
let comments = this.state.searchResponse.comments.map(e => {return {type_: "comments", data: e}});
|
||||
let posts = this.state.searchResponse.posts.map(e => {return {type_: "posts", data: e}});
|
||||
let communities = this.state.searchResponse.communities.map(e => {return {type_: "communities", data: e}});
|
||||
let users = this.state.searchResponse.users.map(e => {return {type_: "users", data: e}});
|
||||
let combined: Array<{
|
||||
type_: string;
|
||||
data: Comment | Post | Community | UserView;
|
||||
}> = [];
|
||||
let comments = this.state.searchResponse.comments.map(e => {
|
||||
return { type_: 'comments', data: e };
|
||||
});
|
||||
let posts = this.state.searchResponse.posts.map(e => {
|
||||
return { type_: 'posts', data: e };
|
||||
});
|
||||
let communities = this.state.searchResponse.communities.map(e => {
|
||||
return { type_: 'communities', data: e };
|
||||
});
|
||||
let users = this.state.searchResponse.users.map(e => {
|
||||
return { type_: 'users', data: e };
|
||||
});
|
||||
|
||||
combined.push(...comments);
|
||||
combined.push(...posts);
|
||||
|
@ -184,49 +243,68 @@ export class Search extends Component<any, SearchState> {
|
|||
if (this.state.sort == SortType.New) {
|
||||
combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
|
||||
} else {
|
||||
combined.sort((a, b) => ((b.data as Comment | Post).score
|
||||
| (b.data as Community).number_of_subscribers
|
||||
| (b.data as UserView).comment_score)
|
||||
- ((a.data as Comment | Post).score
|
||||
| (a.data as Community).number_of_subscribers
|
||||
| (a.data as UserView).comment_score));
|
||||
combined.sort(
|
||||
(a, b) =>
|
||||
((b.data as Comment | Post).score |
|
||||
(b.data as Community).number_of_subscribers |
|
||||
(b.data as UserView).comment_score) -
|
||||
((a.data as Comment | Post).score |
|
||||
(a.data as Community).number_of_subscribers |
|
||||
(a.data as UserView).comment_score)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{combined.map(i =>
|
||||
{combined.map(i => (
|
||||
<div>
|
||||
{i.type_ == "posts" &&
|
||||
{i.type_ == 'posts' && (
|
||||
<PostListing post={i.data as Post} showCommunity viewOnly />
|
||||
}
|
||||
{i.type_ == "comments" &&
|
||||
<CommentNodes nodes={[{comment: i.data as Comment}]} viewOnly noIndent />
|
||||
}
|
||||
{i.type_ == "communities" &&
|
||||
)}
|
||||
{i.type_ == 'comments' && (
|
||||
<CommentNodes
|
||||
nodes={[{ comment: i.data as Comment }]}
|
||||
viewOnly
|
||||
noIndent
|
||||
/>
|
||||
)}
|
||||
{i.type_ == 'communities' && (
|
||||
<div>
|
||||
<span><Link to={`/c/${(i.data as Community).name}`}>{`/c/${(i.data as Community).name}`}</Link></span>
|
||||
<span>{` - ${(i.data as Community).title} - ${(i.data as Community).number_of_subscribers} subscribers`}</span>
|
||||
<span>
|
||||
<Link to={`/c/${(i.data as Community).name}`}>{`/c/${
|
||||
(i.data as Community).name
|
||||
}`}</Link>
|
||||
</span>
|
||||
<span>{` - ${(i.data as Community).title} - ${
|
||||
(i.data as Community).number_of_subscribers
|
||||
} subscribers`}</span>
|
||||
</div>
|
||||
}
|
||||
{i.type_ == "users" &&
|
||||
)}
|
||||
{i.type_ == 'users' && (
|
||||
<div>
|
||||
<span><Link className="text-info" to={`/u/${(i.data as UserView).name}`}>{`/u/${(i.data as UserView).name}`}</Link></span>
|
||||
<span>{` - ${(i.data as UserView).comment_score} comment karma`}</span>
|
||||
<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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
comments() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.searchResponse.comments.map(comment =>
|
||||
{this.state.searchResponse.comments.map(comment => (
|
||||
<CommentNodes nodes={[{ comment: comment }]} noIndent viewOnly />
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -234,9 +312,9 @@ export class Search extends Component<any, SearchState> {
|
|||
posts() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.searchResponse.posts.map(post =>
|
||||
{this.state.searchResponse.posts.map(post => (
|
||||
<PostListing post={post} showCommunity viewOnly />
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -245,12 +323,14 @@ export class Search extends Component<any, SearchState> {
|
|||
communities() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.searchResponse.communities.map(community =>
|
||||
{this.state.searchResponse.communities.map(community => (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -258,12 +338,17 @@ export class Search extends Component<any, SearchState> {
|
|||
users() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.searchResponse.users.map(user =>
|
||||
{this.state.searchResponse.users.map(user => (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -271,10 +356,20 @@ export class Search extends Component<any, SearchState> {
|
|||
paginator() {
|
||||
return (
|
||||
<div class="mt-2">
|
||||
{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" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
|
||||
{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"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
>
|
||||
<T i18nKey="next">#</T>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -283,11 +378,18 @@ export class Search extends Component<any, SearchState> {
|
|||
let res = this.state.searchResponse;
|
||||
return (
|
||||
<div>
|
||||
{res && res.op && res.posts.length == 0 && res.comments.length == 0 &&
|
||||
<span><T i18nKey="no_results">#</T></span>
|
||||
}
|
||||
{res &&
|
||||
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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
nextPage(i: Search) {
|
||||
|
@ -305,7 +407,6 @@ export class Search extends Component<any, SearchState> {
|
|||
}
|
||||
|
||||
search() {
|
||||
// TODO community
|
||||
let form: SearchForm = {
|
||||
q: this.state.q,
|
||||
type_: SearchType[this.state.type_],
|
||||
|
@ -319,11 +420,11 @@ export class Search extends Component<any, SearchState> {
|
|||
}
|
||||
}
|
||||
|
||||
handleSortChange(i: Search, event: any) {
|
||||
i.state.sort = Number(event.target.value);
|
||||
i.state.page = 1;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
handleSortChange(val: SortType) {
|
||||
this.state.sort = val;
|
||||
this.state.page = 1;
|
||||
this.setState(this.state);
|
||||
this.updateUrl();
|
||||
}
|
||||
|
||||
handleTypeChange(i: Search, event: any) {
|
||||
|
@ -349,7 +450,9 @@ export class Search extends Component<any, SearchState> {
|
|||
updateUrl() {
|
||||
let typeStr = SearchType[this.state.type_].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) {
|
||||
|
@ -362,10 +465,11 @@ export class Search extends Component<any, SearchState> {
|
|||
let res: SearchResponse = msg;
|
||||
this.state.searchResponse = res;
|
||||
this.state.loading = false;
|
||||
document.title = `${i18n.t('search')} - ${this.state.q} - ${WebSocketService.Instance.site.name}`;
|
||||
document.title = `${i18n.t('search')} - ${this.state.q} - ${
|
||||
WebSocketService.Instance.site.name
|
||||
}`;
|
||||
window.scrollTo(0, 0);
|
||||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
101
ui/src/components/setup.tsx
vendored
101
ui/src/components/setup.tsx
vendored
|
@ -1,5 +1,5 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import { RegisterForm, LoginResponse, UserOperation } from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
|
@ -27,8 +27,7 @@ export class Setup extends Component<any, State> {
|
|||
},
|
||||
doneRegisteringUser: false,
|
||||
userLoading: false,
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
@ -36,11 +35,18 @@ export class Setup extends Component<any, State> {
|
|||
this.state = this.emptyState;
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
() => console.log("complete")
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -57,54 +63,103 @@ export class Setup extends Component<any, State> {
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 offset-lg-3 col-lg-6">
|
||||
<h3><T i18nKey="lemmy_instance_setup">#</T></h3>
|
||||
{!this.state.doneRegisteringUser ? this.registerUser() : <SiteForm />}
|
||||
<h3>
|
||||
<T i18nKey="lemmy_instance_setup">#</T>
|
||||
</h3>
|
||||
{!this.state.doneRegisteringUser ? (
|
||||
this.registerUser()
|
||||
) : (
|
||||
<SiteForm />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
registerUser() {
|
||||
return (
|
||||
<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">
|
||||
<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">
|
||||
<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 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">
|
||||
<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 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">
|
||||
<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 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">
|
||||
<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 class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-secondary">{this.state.userLoading ?
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('sign_up')}</button>
|
||||
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
{this.state.userLoading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
) : (
|
||||
i18n.t('sign_up')
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
handleRegisterSubmit(i: Setup, event: any) {
|
||||
event.preventDefault();
|
||||
i.state.userLoading = true;
|
||||
|
|
227
ui/src/components/sidebar.tsx
vendored
227
ui/src/components/sidebar.tsx
vendored
|
@ -1,6 +1,12 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
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 { mdToHtml, getUnixTime } from '../utils';
|
||||
import { CommunityForm } from './community-form';
|
||||
|
@ -21,13 +27,12 @@ interface SidebarState {
|
|||
}
|
||||
|
||||
export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||
|
||||
private emptyState: SidebarState = {
|
||||
showEdit: false,
|
||||
showRemoveDialog: false,
|
||||
removeReason: null,
|
||||
removeExpires: null
|
||||
}
|
||||
removeExpires: null,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
@ -39,15 +44,17 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
render() {
|
||||
return (
|
||||
<div>
|
||||
{!this.state.showEdit
|
||||
? this.sidebar()
|
||||
: <CommunityForm
|
||||
{!this.state.showEdit ? (
|
||||
this.sidebar()
|
||||
) : (
|
||||
<CommunityForm
|
||||
community={this.props.community}
|
||||
onEdit={this.handleEditCommunity}
|
||||
onCancel={this.handleEditCancel} />
|
||||
}
|
||||
onCancel={this.handleEditCancel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
sidebar() {
|
||||
|
@ -58,44 +65,78 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
<div class="card-body">
|
||||
<h5 className="mb-0">
|
||||
<span>{community.title}</span>
|
||||
{community.removed &&
|
||||
<small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
|
||||
}
|
||||
{community.deleted &&
|
||||
<small className="ml-2 text-muted font-italic"><T i18nKey="deleted">#</T></small>
|
||||
}
|
||||
{community.removed && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
<T i18nKey="removed">#</T>
|
||||
</small>
|
||||
)}
|
||||
{community.deleted && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
<T i18nKey="deleted">#</T>
|
||||
</small>
|
||||
)}
|
||||
</h5>
|
||||
<Link className="text-muted" to={`/c/${community.name}`}>/c/{community.name}</Link>
|
||||
<Link className="text-muted" to={`/c/${community.name}`}>
|
||||
/c/{community.name}
|
||||
</Link>
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
{this.canMod &&
|
||||
{this.canMod && (
|
||||
<>
|
||||
<li className="list-inline-item">
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
|
||||
</li>
|
||||
{this.amCreator &&
|
||||
<li className="list-inline-item">
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
|
||||
{!community.deleted ? i18n.t('delete') : i18n.t('restore')}
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleEditClick)}
|
||||
>
|
||||
<T i18nKey="edit">#</T>
|
||||
</span>
|
||||
</li>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{this.canAdmin &&
|
||||
{this.amCreator && (
|
||||
<li className="list-inline-item">
|
||||
{!this.props.community.removed ?
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
|
||||
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
|
||||
}
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||
>
|
||||
{!community.deleted
|
||||
? i18n.t('delete')
|
||||
: i18n.t('restore')}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{this.canAdmin && (
|
||||
<li className="list-inline-item">
|
||||
{!this.props.community.removed ? (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleModRemoveShow)}
|
||||
>
|
||||
<T i18nKey="remove">#</T>
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleModRemoveSubmit)}
|
||||
>
|
||||
<T i18nKey="restore">#</T>
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
{this.state.showRemoveDialog &&
|
||||
{this.state.showRemoveDialog && (
|
||||
<form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label"><T i18nKey="reason">#</T></label>
|
||||
<input type="text" class="form-control mr-2" placeholder={i18n.t('optional')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
|
||||
<label class="col-form-label">
|
||||
<T i18nKey="reason">#</T>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control mr-2"
|
||||
placeholder={i18n.t('optional')}
|
||||
value={this.state.removeReason}
|
||||
onInput={linkEvent(this, this.handleModRemoveReasonChange)}
|
||||
/>
|
||||
</div>
|
||||
{/* TODO hold off on expires for now */}
|
||||
{/* <div class="form-group row"> */}
|
||||
|
@ -103,40 +144,98 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
{/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
|
||||
{/* </div> */}
|
||||
<div class="form-group row">
|
||||
<button type="submit" class="btn btn-secondary"><T i18nKey="remove_community">#</T></button>
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
<T i18nKey="remove_community">#</T>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
)}
|
||||
<ul class="my-1 list-inline">
|
||||
<li className="list-inline-item"><Link className="badge badge-secondary" to="/communities">{community.category_name}</Link></li>
|
||||
<li className="list-inline-item badge badge-secondary"><T i18nKey="number_of_subscribers" interpolation={{count: community.number_of_subscribers}}>#</T></li>
|
||||
<li className="list-inline-item badge badge-secondary"><T i18nKey="number_of_posts" interpolation={{count: community.number_of_posts}}>#</T></li>
|
||||
<li className="list-inline-item badge badge-secondary"><T i18nKey="number_of_comments" interpolation={{count: community.number_of_comments}}>#</T></li>
|
||||
<li className="list-inline-item"><Link className="badge badge-secondary" to={`/modlog/community/${this.props.community.id}`}><T i18nKey="modlog">#</T></Link></li>
|
||||
<li className="list-inline-item">
|
||||
<Link className="badge badge-secondary" to="/communities">
|
||||
{community.category_name}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="list-inline-item badge badge-secondary">
|
||||
<T
|
||||
i18nKey="number_of_subscribers"
|
||||
interpolation={{ count: community.number_of_subscribers }}
|
||||
>
|
||||
#
|
||||
</T>
|
||||
</li>
|
||||
<li className="list-inline-item badge badge-secondary">
|
||||
<T
|
||||
i18nKey="number_of_posts"
|
||||
interpolation={{ count: community.number_of_posts }}
|
||||
>
|
||||
#
|
||||
</T>
|
||||
</li>
|
||||
<li className="list-inline-item badge badge-secondary">
|
||||
<T
|
||||
i18nKey="number_of_comments"
|
||||
interpolation={{ count: community.number_of_comments }}
|
||||
>
|
||||
#
|
||||
</T>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<Link
|
||||
className="badge badge-secondary"
|
||||
to={`/modlog/community/${this.props.community.id}`}
|
||||
>
|
||||
<T i18nKey="modlog">#</T>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="list-inline small">
|
||||
<li class="list-inline-item">{i18n.t('mods')}: </li>
|
||||
{this.props.moderators.map(mod =>
|
||||
<li class="list-inline-item"><Link class="text-info" to={`/u/${mod.user_name}`}>{mod.user_name}</Link></li>
|
||||
)}
|
||||
{this.props.moderators.map(mod => (
|
||||
<li class="list-inline-item">
|
||||
<Link class="text-info" to={`/u/${mod.user_name}`}>
|
||||
{mod.user_name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link class={`btn btn-sm btn-secondary btn-block mb-3 ${(community.deleted || community.removed) && 'no-click'}`}
|
||||
to={`/create_post?community=${community.name}`}><T i18nKey="create_a_post">#</T></Link>
|
||||
<Link
|
||||
class={`btn btn-sm btn-secondary btn-block mb-3 ${(community.deleted ||
|
||||
community.removed) &&
|
||||
'no-click'}`}
|
||||
to={`/create_post?community=${community.name}`}
|
||||
>
|
||||
<T i18nKey="create_a_post">#</T>
|
||||
</Link>
|
||||
<div>
|
||||
{community.subscribed
|
||||
? <button class="btn btn-sm btn-secondary btn-block" onClick={linkEvent(community.id, this.handleUnsubscribe)}><T i18nKey="unsubscribe">#</T></button>
|
||||
: <button class="btn btn-sm btn-secondary btn-block" onClick={linkEvent(community.id, this.handleSubscribe)}><T i18nKey="subscribe">#</T></button>
|
||||
}
|
||||
{community.subscribed ? (
|
||||
<button
|
||||
class="btn btn-sm btn-secondary btn-block"
|
||||
onClick={linkEvent(community.id, this.handleUnsubscribe)}
|
||||
>
|
||||
<T i18nKey="unsubscribe">#</T>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
class="btn btn-sm btn-secondary btn-block"
|
||||
onClick={linkEvent(community.id, this.handleSubscribe)}
|
||||
>
|
||||
<T i18nKey="subscribe">#</T>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{community.description &&
|
||||
{community.description && (
|
||||
<div class="card border-secondary">
|
||||
<div class="card-body">
|
||||
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(community.description)} />
|
||||
<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) {
|
||||
let form: FollowCommunityForm = {
|
||||
community_id: communityId,
|
||||
follow: false
|
||||
follow: false,
|
||||
};
|
||||
WebSocketService.Instance.followCommunity(form);
|
||||
}
|
||||
|
@ -181,7 +280,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
handleSubscribe(communityId: number) {
|
||||
let form: FollowCommunityForm = {
|
||||
community_id: communityId,
|
||||
follow: true
|
||||
follow: true,
|
||||
};
|
||||
WebSocketService.Instance.followCommunity(form);
|
||||
}
|
||||
|
@ -191,11 +290,19 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
}
|
||||
|
||||
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 {
|
||||
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) {
|
||||
|
|
63
ui/src/components/site-form.tsx
vendored
63
ui/src/components/site-form.tsx
vendored
|
@ -19,10 +19,10 @@ interface SiteFormState {
|
|||
export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||
private emptyState: SiteFormState = {
|
||||
siteForm: {
|
||||
name: null
|
||||
name: null,
|
||||
},
|
||||
loading: false
|
||||
}
|
||||
loading: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
@ -31,7 +31,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
this.state.siteForm = {
|
||||
name: this.props.site.name,
|
||||
description: this.props.site.description,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,26 +42,63 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
render() {
|
||||
return (
|
||||
<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">
|
||||
<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">
|
||||
<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 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">
|
||||
<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 class="form-group row">
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-secondary mr-2">
|
||||
{this.state.loading ?
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
|
||||
this.props.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>}
|
||||
{this.state.loading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</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>
|
||||
</form>
|
||||
|
|
69
ui/src/components/sort-select.tsx
vendored
Normal file
69
ui/src/components/sort-select.tsx
vendored
Normal 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);
|
||||
}
|
||||
}
|
80
ui/src/components/sponsors.tsx
vendored
80
ui/src/components/sponsors.tsx
vendored
|
@ -3,24 +3,21 @@ import { WebSocketService } from '../services';
|
|||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
let general =
|
||||
[
|
||||
"Nathan J. Goode",
|
||||
];
|
||||
let general = ['riccardo', 'NotTooHighToHack'];
|
||||
// let highlighted = [];
|
||||
// let silver = [];
|
||||
// let gold = [];
|
||||
// let latinum = [];
|
||||
|
||||
export class Sponsors extends Component<any, any> {
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = `${i18n.t('sponsors')} - ${WebSocketService.Instance.site.name}`;
|
||||
document.title = `${i18n.t('sponsors')} - ${
|
||||
WebSocketService.Instance.site.name
|
||||
}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -32,62 +29,85 @@ export class Sponsors extends Component<any, any> {
|
|||
<hr />
|
||||
{this.bitcoin()}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
topMessage() {
|
||||
return (
|
||||
<div>
|
||||
<h5><T i18nKey="sponsors_of_lemmy">#</T></h5>
|
||||
<h5>
|
||||
<T i18nKey="sponsors_of_lemmy">#</T>
|
||||
</h5>
|
||||
<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>
|
||||
<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>
|
||||
)
|
||||
);
|
||||
}
|
||||
sponsors() {
|
||||
return (
|
||||
<div class="container">
|
||||
<h5><T i18nKey="sponsors">#</T></h5>
|
||||
<p><T i18nKey="general_sponsors">#</T></p>
|
||||
<h5>
|
||||
<T i18nKey="sponsors">#</T>
|
||||
</h5>
|
||||
<p>
|
||||
<T i18nKey="general_sponsors">#</T>
|
||||
</p>
|
||||
<div class="row card-columns">
|
||||
{general.map(s =>
|
||||
{general.map(s => (
|
||||
<div class="card col-12 col-md-2">
|
||||
<div>{s}</div>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
bitcoin() {
|
||||
return (
|
||||
<div>
|
||||
<h5><T i18nKey="crypto">#</T></h5>
|
||||
<h5>
|
||||
<T i18nKey="crypto">#</T>
|
||||
</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover text-center">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><T i18nKey="bitcoin">#</T></td>
|
||||
<td><code>1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><T i18nKey="ethereum">#</T></td>
|
||||
<td><code>0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><T i18nKey="monero">#</T></td>
|
||||
<td>
|
||||
<code>41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV</code>
|
||||
<T i18nKey="bitcoin">#</T>
|
||||
</td>
|
||||
<td>
|
||||
<code>1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<T i18nKey="ethereum">#</T>
|
||||
</td>
|
||||
<td>
|
||||
<code>0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<T i18nKey="monero">#</T>
|
||||
</td>
|
||||
<td>
|
||||
<code>
|
||||
41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV
|
||||
</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
21
ui/src/components/symbols.tsx
vendored
21
ui/src/components/symbols.tsx
vendored
|
@ -1,14 +1,19 @@
|
|||
import { Component } from 'inferno';
|
||||
|
||||
export class Symbols extends Component<any, any> {
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
render() {
|
||||
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>
|
||||
<symbol id="icon-arrow-down" viewBox="0 0 26 28">
|
||||
<title>arrow-down</title>
|
||||
|
@ -22,12 +27,18 @@ export class Symbols extends Component<any, any> {
|
|||
<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>
|
||||
</symbol>
|
||||
<symbol id="icon-mouse" version="1.1" x="0px" y="0px"
|
||||
viewBox="0 0 1024 1024">
|
||||
<symbol
|
||||
id="icon-mouse"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 1024 1024"
|
||||
>
|
||||
<g
|
||||
id="layer1"
|
||||
transform="translate(0,-26.066658)"
|
||||
style="display:inline">
|
||||
style="display:inline"
|
||||
>
|
||||
<path
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 167.03908,270.78735 c -0.94784,-0.002 -1.8939,0.004 -2.83789,0.0215 -4.31538,0.0778 -8.58934,0.3593 -12.8125,0.8457 -33.78522,3.89116 -64.215716,21.86394 -82.871086,53.27344 -18.27982,30.77718 -22.77749,64.66635 -13.46094,96.06837 9.31655,31.40203 31.88488,59.93174 65.296886,82.5332 0.20163,0.13618 0.40678,0.26709 0.61523,0.39258 28.65434,17.27768 57.18167,28.93179 87.74218,34.95508 -0.74566,12.61339 -0.72532,25.5717 0.082,38.84375 2.43989,40.10943 16.60718,77.03742 38.0957,109.67187 l -77.00781,31.4375 c -8.30605,3.25932 -12.34178,12.68234 -8.96967,20.94324 3.37211,8.2609 12.84919,12.16798 21.06342,8.68371 l 84.69727,-34.57617 c 15.70675,18.72702 33.75346,35.68305 53.12109,50.57032 0.74013,0.56891 1.4904,1.12236 2.23437,1.68554 l -49.61132,65.69141 c -5.45446,7.0474 -4.10058,17.19288 3.01098,22.5634 7.11156,5.37052 17.24028,3.89649 22.52612,-3.27824 l 50.38672,-66.71876 c 27.68572,17.53469 57.07524,31.20388 86.07227,40.25196 14.88153,27.28008 43.96965,44.64648 77.58789,44.64648 33.93762,0 63.04252,-18.68693 77.80082,-45.4375 28.7072,-9.21295 57.7527,-22.93196 85.1484,-40.40234 l 51.0977,67.66016 c 5.2858,7.17473 15.4145,8.64876 22.5261,3.27824 7.1115,-5.37052 8.4654,-15.516 3.011,-22.5634 l -50.3614,-66.68555 c 0.334,-0.25394 0.6727,-0.50077 1.0059,-0.75586 19.1376,-14.64919 37.0259,-31.28581 52.7031,-49.63476 l 82.5625,33.70507 c 8.2143,3.48427 17.6913,-0.42281 21.0634,-8.68371 3.3722,-8.2609 -0.6636,-17.68392 -8.9696,-20.94324 l -74.5391,-30.42773 c 22.1722,-32.82971 37.0383,-70.03397 40.1426,-110.46094 1.0253,-13.35251 1.2292,-26.42535 0.6387,-39.17578 30.3557,-6.05408 58.7164,-17.66833 87.2011,-34.84375 0.2085,-0.12549 0.4136,-0.2564 0.6153,-0.39258 33.412,-22.60147 55.9803,-51.13117 65.2968,-82.5332 9.3166,-31.40202 4.8189,-65.29118 -13.4609,-96.06837 -18.6553,-31.40951 -49.0859,-49.38228 -82.8711,-53.27344 -4.2231,-0.4864 -8.4971,-0.76791 -12.8125,-0.8457 -30.2077,-0.54448 -62.4407,8.82427 -93.4316,26.71484 -22.7976,13.16063 -43.3521,33.31423 -59.4375,55.30469 -44.9968,-25.75094 -103.5444,-40.25065 -175.4785,-41.43945 -6.4522,-0.10663 -13.0125,-0.10696 -19.67974,0.002 -80.18875,1.30929 -144.38284,16.5086 -192.87109,43.9922 -0.11914,-0.19111 -0.24287,-0.37932 -0.37109,-0.56446 -16.29,-22.764 -37.41085,-43.73706 -60.89649,-57.29493 -30.02247,-17.33149 -61.21051,-26.66489 -90.59375,-26.73633 z"
|
||||
|
|
523
ui/src/components/user.tsx
vendored
523
ui/src/components/user.tsx
vendored
|
@ -1,18 +1,46 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from "rxjs";
|
||||
import { Subscription } from 'rxjs';
|
||||
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 { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter, themes, setTheme } from '../utils';
|
||||
import {
|
||||
msgOp,
|
||||
fetchLimit,
|
||||
routeSortTypeToEnum,
|
||||
capitalizeFirstLetter,
|
||||
themes,
|
||||
setTheme,
|
||||
} from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
import { SortSelect } from './sort-select';
|
||||
import { ListingTypeSelect } from './listing-type-select';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
import { MomentTime } from './moment-time';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
enum View {
|
||||
Overview, Comments, Posts, Saved
|
||||
Overview,
|
||||
Comments,
|
||||
Posts,
|
||||
Saved,
|
||||
}
|
||||
|
||||
interface UserState {
|
||||
|
@ -37,7 +65,6 @@ interface UserState {
|
|||
}
|
||||
|
||||
export class User extends Component<any, UserState> {
|
||||
|
||||
private subscription: Subscription;
|
||||
private emptyState: UserState = {
|
||||
user: {
|
||||
|
@ -65,6 +92,8 @@ export class User extends Component<any, UserState> {
|
|||
userSettingsForm: {
|
||||
show_nsfw: null,
|
||||
theme: null,
|
||||
default_sort_type: null,
|
||||
default_listing_type: null,
|
||||
auth: null,
|
||||
},
|
||||
userSettingsLoading: null,
|
||||
|
@ -72,22 +101,36 @@ export class User extends Component<any, UserState> {
|
|||
deleteAccountShowConfirm: false,
|
||||
deleteAccountForm: {
|
||||
password: null,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
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.username = this.props.match.params.username;
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
(msg) => this.parseMessage(msg),
|
||||
(err) => console.error(err),
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
|
@ -95,23 +138,26 @@ export class User extends Component<any, UserState> {
|
|||
}
|
||||
|
||||
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 {
|
||||
return (props.match.params.view) ?
|
||||
View[capitalizeFirstLetter(props.match.params.view)] :
|
||||
View.Overview;
|
||||
return props.match.params.view
|
||||
? View[capitalizeFirstLetter(props.match.params.view)]
|
||||
: View.Overview;
|
||||
}
|
||||
|
||||
getSortTypeFromProps(props: any): SortType {
|
||||
return (props.match.params.sort) ?
|
||||
routeSortTypeToEnum(props.match.params.sort) :
|
||||
SortType.New;
|
||||
return props.match.params.sort
|
||||
? routeSortTypeToEnum(props.match.params.sort)
|
||||
: SortType.New;
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@ -120,11 +166,14 @@ export class User extends Component<any, UserState> {
|
|||
|
||||
// Necessary for back button for some reason
|
||||
componentWillReceiveProps(nextProps: any) {
|
||||
if (nextProps.history.action == 'POP') {
|
||||
this.state = this.emptyState;
|
||||
if (
|
||||
nextProps.history.action == 'POP' ||
|
||||
nextProps.history.action == 'PUSH'
|
||||
) {
|
||||
this.state.view = this.getViewFromProps(nextProps);
|
||||
this.state.sort = this.getSortTypeFromProps(nextProps);
|
||||
this.state.page = this.getPageFromProps(nextProps);
|
||||
this.setState(this.state);
|
||||
this.refetch();
|
||||
}
|
||||
}
|
||||
|
@ -132,68 +181,78 @@ export class User extends Component<any, UserState> {
|
|||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
{this.state.loading ?
|
||||
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
</h5>
|
||||
) : (
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8">
|
||||
<h5>/u/{this.state.user.name}</h5>
|
||||
{this.selects()}
|
||||
{this.state.view == View.Overview &&
|
||||
this.overview()
|
||||
}
|
||||
{this.state.view == View.Comments &&
|
||||
this.comments()
|
||||
}
|
||||
{this.state.view == View.Posts &&
|
||||
this.posts()
|
||||
}
|
||||
{this.state.view == View.Saved &&
|
||||
this.overview()
|
||||
}
|
||||
{this.state.view == View.Overview && this.overview()}
|
||||
{this.state.view == View.Comments && this.comments()}
|
||||
{this.state.view == View.Posts && this.posts()}
|
||||
{this.state.view == View.Saved && this.overview()}
|
||||
{this.paginator()}
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
{this.userInfo()}
|
||||
{this.isCurrentUser &&
|
||||
this.userSettings()
|
||||
}
|
||||
{this.isCurrentUser && this.userSettings()}
|
||||
{this.moderates()}
|
||||
{this.follows()}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
selects() {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<select value={this.state.view} onChange={linkEvent(this, this.handleViewChange)} class="custom-select custom-select-sm w-auto">
|
||||
<option disabled><T i18nKey="view">#</T></option>
|
||||
<option value={View.Overview}><T i18nKey="overview">#</T></option>
|
||||
<option value={View.Comments}><T i18nKey="comments">#</T></option>
|
||||
<option value={View.Posts}><T i18nKey="posts">#</T></option>
|
||||
<option value={View.Saved}><T i18nKey="saved">#</T></option>
|
||||
</select>
|
||||
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
|
||||
<option disabled><T i18nKey="sort_type">#</T></option>
|
||||
<option value={SortType.New}><T i18nKey="new">#</T></option>
|
||||
<option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
|
||||
<option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
|
||||
<option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
|
||||
<option value={SortType.TopYear}><T i18nKey="year">#</T></option>
|
||||
<option value={SortType.TopAll}><T i18nKey="all">#</T></option>
|
||||
<select
|
||||
value={this.state.view}
|
||||
onChange={linkEvent(this, this.handleViewChange)}
|
||||
class="custom-select custom-select-sm w-auto"
|
||||
>
|
||||
<option disabled>
|
||||
<T i18nKey="view">#</T>
|
||||
</option>
|
||||
<option value={View.Overview}>
|
||||
<T i18nKey="overview">#</T>
|
||||
</option>
|
||||
<option value={View.Comments}>
|
||||
<T i18nKey="comments">#</T>
|
||||
</option>
|
||||
<option value={View.Posts}>
|
||||
<T i18nKey="posts">#</T>
|
||||
</option>
|
||||
<option value={View.Saved}>
|
||||
<T i18nKey="saved">#</T>
|
||||
</option>
|
||||
</select>
|
||||
<span class="ml-2">
|
||||
<SortSelect
|
||||
sort={this.state.sort}
|
||||
onChange={this.handleSortChange}
|
||||
hideHot
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
overview() {
|
||||
let combined: Array<{type_: string, data: Comment | Post}> = [];
|
||||
let comments = this.state.comments.map(e => {return {type_: "comments", data: e}});
|
||||
let posts = this.state.posts.map(e => {return {type_: "posts", data: e}});
|
||||
let combined: Array<{ type_: string; data: Comment | Post }> = [];
|
||||
let comments = this.state.comments.map(e => {
|
||||
return { type_: 'comments', data: e };
|
||||
});
|
||||
let posts = this.state.posts.map(e => {
|
||||
return { type_: 'posts', data: e };
|
||||
});
|
||||
|
||||
combined.push(...comments);
|
||||
combined.push(...posts);
|
||||
|
@ -207,35 +266,38 @@ export class User extends Component<any, UserState> {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{combined.map(i =>
|
||||
{combined.map(i => (
|
||||
<div>
|
||||
{i.type_ == "posts"
|
||||
? <PostListing
|
||||
{i.type_ == 'posts' ? (
|
||||
<PostListing
|
||||
post={i.data as Post}
|
||||
admins={this.state.admins}
|
||||
showCommunity
|
||||
viewOnly />
|
||||
:
|
||||
viewOnly
|
||||
/>
|
||||
) : (
|
||||
<CommentNodes
|
||||
nodes={[{ comment: i.data as Comment }]}
|
||||
admins={this.state.admins}
|
||||
noIndent />
|
||||
}
|
||||
noIndent
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
comments() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.comments.map(comment =>
|
||||
<CommentNodes nodes={[{comment: comment}]}
|
||||
{this.state.comments.map(comment => (
|
||||
<CommentNodes
|
||||
nodes={[{ comment: comment }]}
|
||||
admins={this.state.admins}
|
||||
noIndent />
|
||||
)}
|
||||
noIndent
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -243,13 +305,14 @@ export class User extends Component<any, UserState> {
|
|||
posts() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.posts.map(post =>
|
||||
{this.state.posts.map(post => (
|
||||
<PostListing
|
||||
post={post}
|
||||
admins={this.state.admins}
|
||||
showCommunity
|
||||
viewOnly />
|
||||
)}
|
||||
viewOnly
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -263,28 +326,60 @@ export class User extends Component<any, UserState> {
|
|||
<h5>
|
||||
<ul class="list-inline mb-0">
|
||||
<li className="list-inline-item">{user.name}</li>
|
||||
{user.banned &&
|
||||
<li className="list-inline-item badge badge-danger"><T i18nKey="banned">#</T></li>
|
||||
}
|
||||
{user.banned && (
|
||||
<li className="list-inline-item badge badge-danger">
|
||||
<T i18nKey="banned">#</T>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</h5>
|
||||
<div>{i18n.t('joined')} <MomentTime data={user} /></div>
|
||||
<div>
|
||||
{i18n.t('joined')} <MomentTime data={user} />
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm mt-2 mb-0">
|
||||
<tr>
|
||||
<td><T i18nKey="number_of_points" interpolation={{count: user.post_score}}>#</T></td>
|
||||
<td><T i18nKey="number_of_posts" interpolation={{count: user.number_of_posts}}>#</T></td>
|
||||
<td>
|
||||
<T
|
||||
i18nKey="number_of_points"
|
||||
interpolation={{ count: user.post_score }}
|
||||
>
|
||||
#
|
||||
</T>
|
||||
</td>
|
||||
<td>
|
||||
<T
|
||||
i18nKey="number_of_posts"
|
||||
interpolation={{ count: user.number_of_posts }}
|
||||
>
|
||||
#
|
||||
</T>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><T i18nKey="number_of_points" interpolation={{count: user.comment_score}}>#</T></td>
|
||||
<td><T i18nKey="number_of_comments" interpolation={{count: user.number_of_comments}}>#</T></td>
|
||||
<td>
|
||||
<T
|
||||
i18nKey="number_of_points"
|
||||
interpolation={{ count: user.comment_score }}
|
||||
>
|
||||
#
|
||||
</T>
|
||||
</td>
|
||||
<td>
|
||||
<T
|
||||
i18nKey="number_of_comments"
|
||||
interpolation={{ count: user.number_of_comments }}
|
||||
>
|
||||
#
|
||||
</T>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
userSettings() {
|
||||
|
@ -292,95 +387,218 @@ export class User extends Component<any, UserState> {
|
|||
<div>
|
||||
<div class="card border-secondary mb-3">
|
||||
<div class="card-body">
|
||||
<h5><T i18nKey="settings">#</T></h5>
|
||||
<h5>
|
||||
<T i18nKey="settings">#</T>
|
||||
</h5>
|
||||
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
|
||||
<div class="form-group">
|
||||
<div class="col-12">
|
||||
<label><T i18nKey="theme">#</T></label>
|
||||
<select 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>
|
||||
<label>
|
||||
<T i18nKey="theme">#</T>
|
||||
</label>
|
||||
<select
|
||||
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>
|
||||
</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="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>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-block btn-secondary mr-4"
|
||||
>
|
||||
{this.state.userSettingsLoading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
) : (
|
||||
capitalizeFirstLetter(i18n.t('save'))
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row mb-0">
|
||||
<hr />
|
||||
<div class="form-group mb-0">
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-secondary mr-4">{this.state.userSettingsLoading ?
|
||||
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : capitalizeFirstLetter(i18n.t('save'))}</button>
|
||||
<button class="btn btn-danger" onClick={linkEvent(this, this.handleDeleteAccountShowConfirmToggle)}><T i18nKey="delete_account">#</T></button>
|
||||
{this.state.deleteAccountShowConfirm &&
|
||||
<button
|
||||
class="btn btn-block btn-danger"
|
||||
onClick={linkEvent(
|
||||
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>
|
||||
<input 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 class="my-2 alert alert-danger" role="alert">
|
||||
<T i18nKey="delete_account_confirm">#</T>
|
||||
</div>
|
||||
<input
|
||||
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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
moderates() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.moderates.length > 0 &&
|
||||
{this.state.moderates.length > 0 && (
|
||||
<div class="card border-secondary mb-3">
|
||||
<div class="card-body">
|
||||
<h5><T i18nKey="moderates">#</T></h5>
|
||||
<h5>
|
||||
<T i18nKey="moderates">#</T>
|
||||
</h5>
|
||||
<ul class="list-unstyled mb-0">
|
||||
{this.state.moderates.map(community =>
|
||||
<li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
|
||||
)}
|
||||
{this.state.moderates.map(community => (
|
||||
<li>
|
||||
<Link to={`/c/${community.community_name}`}>
|
||||
{community.community_name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
follows() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.follows.length > 0 &&
|
||||
{this.state.follows.length > 0 && (
|
||||
<div class="card border-secondary mb-3">
|
||||
<div class="card-body">
|
||||
<h5><T i18nKey="subscribed">#</T></h5>
|
||||
<h5>
|
||||
<T i18nKey="subscribed">#</T>
|
||||
</h5>
|
||||
<ul class="list-unstyled mb-0">
|
||||
{this.state.follows.map(community =>
|
||||
<li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
|
||||
)}
|
||||
{this.state.follows.map(community => (
|
||||
<li>
|
||||
<Link to={`/c/${community.community_name}`}>
|
||||
{community.community_name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
paginator() {
|
||||
return (
|
||||
<div class="my-2">
|
||||
{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" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
|
||||
{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"
|
||||
onClick={linkEvent(this, this.nextPage)}
|
||||
>
|
||||
<T i18nKey="next">#</T>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -388,7 +606,9 @@ export class User extends Component<any, UserState> {
|
|||
updateUrl() {
|
||||
let viewStr = View[this.state.view].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) {
|
||||
|
@ -417,12 +637,12 @@ export class User extends Component<any, UserState> {
|
|||
WebSocketService.Instance.getUserDetails(form);
|
||||
}
|
||||
|
||||
handleSortChange(i: User, event: any) {
|
||||
i.state.sort = Number(event.target.value);
|
||||
i.state.page = 1;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.refetch();
|
||||
handleSortChange(val: SortType) {
|
||||
this.state.sort = val;
|
||||
this.state.page = 1;
|
||||
this.setState(this.state);
|
||||
this.updateUrl();
|
||||
this.refetch();
|
||||
}
|
||||
|
||||
handleViewChange(i: User, event: any) {
|
||||
|
@ -444,6 +664,16 @@ export class User extends Component<any, UserState> {
|
|||
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) {
|
||||
event.preventDefault();
|
||||
i.state.userSettingsLoading = true;
|
||||
|
@ -489,8 +719,15 @@ export class User extends Component<any, UserState> {
|
|||
this.state.admins = res.admins;
|
||||
this.state.loading = false;
|
||||
if (this.isCurrentUser) {
|
||||
this.state.userSettingsForm.show_nsfw = UserService.Instance.user.show_nsfw;
|
||||
this.state.userSettingsForm.theme = UserService.Instance.user.theme ? UserService.Instance.user.theme : 'darkly';
|
||||
this.state.userSettingsForm.show_nsfw =
|
||||
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}`;
|
||||
window.scrollTo(0, 0);
|
||||
|
@ -520,19 +757,22 @@ export class User extends Component<any, UserState> {
|
|||
this.setState(this.state);
|
||||
} else if (op == UserOperation.CreateCommentLike) {
|
||||
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.upvotes = res.comment.upvotes;
|
||||
found.downvotes = res.comment.downvotes;
|
||||
if (res.comment.my_vote !== null)
|
||||
found.my_vote = res.comment.my_vote;
|
||||
if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.BanUser) {
|
||||
let res: BanUserResponse = msg;
|
||||
this.state.comments.filter(c => c.creator_id == res.user.id)
|
||||
.forEach(c => c.banned = res.banned);
|
||||
this.state.posts.filter(c => c.creator_id == res.user.id)
|
||||
.forEach(c => c.banned = res.banned);
|
||||
this.state.comments
|
||||
.filter(c => c.creator_id == res.user.id)
|
||||
.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);
|
||||
} else if (op == UserOperation.AddAdmin) {
|
||||
let res: AddAdminResponse = msg;
|
||||
|
@ -552,4 +792,3 @@ export class User extends Component<any, UserState> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
6
ui/src/env.ts
vendored
6
ui/src/env.ts
vendored
|
@ -1,4 +1,6 @@
|
|||
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}`;
|
||||
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`;
|
||||
|
|
11
ui/src/i18next.ts
vendored
11
ui/src/i18next.ts
vendored
|
@ -12,7 +12,6 @@ import { nl } from './translations/nl';
|
|||
import { it } from './translations/it';
|
||||
|
||||
// https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66
|
||||
// TODO don't forget to add moment locales for new languages.
|
||||
const resources = {
|
||||
en,
|
||||
eo,
|
||||
|
@ -24,16 +23,15 @@ const resources = {
|
|||
ru,
|
||||
nl,
|
||||
it,
|
||||
}
|
||||
};
|
||||
|
||||
function format(value: any, format: any, lng: any) {
|
||||
if (format === 'uppercase') return value.toUpperCase();
|
||||
return value;
|
||||
}
|
||||
|
||||
i18n
|
||||
.init({
|
||||
debug: true,
|
||||
i18n.init({
|
||||
debug: false,
|
||||
// load: 'languageOnly',
|
||||
|
||||
// initImmediate: false,
|
||||
|
@ -41,8 +39,7 @@ i18n
|
|||
fallbackLng: 'en',
|
||||
resources,
|
||||
interpolation: {
|
||||
format: format
|
||||
|
||||
format: format,
|
||||
}
|
||||
});
|
||||
|
||||
|
|
27
ui/src/index.tsx
vendored
27
ui/src/index.tsx
vendored
|
@ -24,7 +24,6 @@ import { WebSocketService, UserService } from './services';
|
|||
const container = document.getElementById('app');
|
||||
|
||||
class Index extends Component<any, any> {
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
WebSocketService.Instance;
|
||||
|
@ -38,7 +37,10 @@ class Index extends Component<any, any> {
|
|||
<Navbar />
|
||||
<div class="mt-4 p-0">
|
||||
<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 path={`/login`} component={Login} />
|
||||
<Route path={`/create_post`} component={CreatePost} />
|
||||
|
@ -47,17 +49,29 @@ class Index extends Component<any, any> {
|
|||
<Route path={`/communities`} component={Communities} />
|
||||
<Route path={`/post/:id/comment/:comment_id`} component={Post} />
|
||||
<Route path={`/post/:id`} component={Post} />
|
||||
<Route path={`/c/:name/sort/:sort/page/:page`} component={Community} />
|
||||
<Route
|
||||
path={`/c/:name/sort/:sort/page/:page`}
|
||||
component={Community}
|
||||
/>
|
||||
<Route path={`/community/:id`} component={Community} />
|
||||
<Route path={`/c/:name`} component={Community} />
|
||||
<Route path={`/u/:username/view/:view/sort/:sort/page/:page`} component={User} />
|
||||
<Route
|
||||
path={`/u/:username/view/:view/sort/:sort/page/:page`}
|
||||
component={User}
|
||||
/>
|
||||
<Route path={`/user/:id`} component={User} />
|
||||
<Route path={`/u/:username`} component={User} />
|
||||
<Route path={`/inbox`} component={Inbox} />
|
||||
<Route path={`/modlog/community/:community_id`} component={Modlog} />
|
||||
<Route
|
||||
path={`/modlog/community/:community_id`}
|
||||
component={Modlog}
|
||||
/>
|
||||
<Route path={`/modlog`} component={Modlog} />
|
||||
<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={`/sponsors`} component={Sponsors} />
|
||||
</Switch>
|
||||
|
@ -68,7 +82,6 @@ class Index extends Component<any, any> {
|
|||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
render(<Index />, container);
|
||||
|
|
300
ui/src/interfaces.ts
vendored
300
ui/src/interfaces.ts
vendored
|
@ -1,21 +1,72 @@
|
|||
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 {
|
||||
Hot, Top, New
|
||||
Hot,
|
||||
Top,
|
||||
New,
|
||||
}
|
||||
|
||||
export enum ListingType {
|
||||
All, Subscribed, Community
|
||||
All,
|
||||
Subscribed,
|
||||
Community,
|
||||
}
|
||||
|
||||
export enum SortType {
|
||||
Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll
|
||||
Hot,
|
||||
New,
|
||||
TopDay,
|
||||
TopWeek,
|
||||
TopMonth,
|
||||
TopYear,
|
||||
TopAll,
|
||||
}
|
||||
|
||||
export enum SearchType {
|
||||
All, Comments, Posts, Communities, Users, Url
|
||||
All,
|
||||
Comments,
|
||||
Posts,
|
||||
Communities,
|
||||
Users,
|
||||
Url,
|
||||
}
|
||||
|
||||
export interface User {
|
||||
|
@ -24,6 +75,8 @@ export interface User {
|
|||
username: string;
|
||||
show_nsfw: boolean;
|
||||
theme: string;
|
||||
default_sort_type: SortType;
|
||||
default_listing_type: ListingType;
|
||||
}
|
||||
|
||||
export interface UserView {
|
||||
|
@ -104,7 +157,7 @@ export interface Post {
|
|||
export interface Comment {
|
||||
id: number;
|
||||
creator_id: number;
|
||||
post_id: number,
|
||||
post_id: number;
|
||||
parent_id?: number;
|
||||
content: string;
|
||||
removed: boolean;
|
||||
|
@ -112,7 +165,7 @@ export interface Comment {
|
|||
read: boolean;
|
||||
published: string;
|
||||
updated?: string;
|
||||
community_id: number,
|
||||
community_id: number;
|
||||
banned: boolean;
|
||||
banned_from_community: boolean;
|
||||
creator_name: string;
|
||||
|
@ -122,6 +175,8 @@ export interface Comment {
|
|||
user_id?: number;
|
||||
my_vote?: number;
|
||||
saved?: boolean;
|
||||
user_mention_id?: number; // For mention type
|
||||
recipient_id?: number;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
|
@ -143,7 +198,10 @@ export interface Site {
|
|||
number_of_communities: number;
|
||||
}
|
||||
|
||||
export enum BanType {Community, Site};
|
||||
export enum BanType {
|
||||
Community,
|
||||
Site,
|
||||
}
|
||||
|
||||
export interface FollowCommunityForm {
|
||||
community_id: number;
|
||||
|
@ -177,7 +235,7 @@ export interface UserDetailsResponse {
|
|||
}
|
||||
|
||||
export interface GetRepliesForm {
|
||||
sort: string; // TODO figure this one out
|
||||
sort: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
unread_only: boolean;
|
||||
|
@ -189,19 +247,43 @@ export interface GetRepliesResponse {
|
|||
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 {
|
||||
community_id: number;
|
||||
user_id: number;
|
||||
ban: boolean;
|
||||
reason?: string,
|
||||
expires?: number,
|
||||
reason?: string;
|
||||
expires?: number;
|
||||
auth?: string;
|
||||
}
|
||||
|
||||
export interface BanFromCommunityResponse {
|
||||
op: string;
|
||||
user: UserView,
|
||||
banned: boolean,
|
||||
user: UserView;
|
||||
banned: boolean;
|
||||
}
|
||||
|
||||
export interface AddModToCommunityForm {
|
||||
|
@ -236,15 +318,15 @@ export interface GetModlogForm {
|
|||
|
||||
export interface GetModlogResponse {
|
||||
op: string;
|
||||
removed_posts: Array<ModRemovePost>,
|
||||
locked_posts: Array<ModLockPost>,
|
||||
stickied_posts: Array<ModStickyPost>,
|
||||
removed_comments: Array<ModRemoveComment>,
|
||||
removed_communities: Array<ModRemoveCommunity>,
|
||||
banned_from_community: Array<ModBanFromCommunity>,
|
||||
banned: Array<ModBan>,
|
||||
added_to_community: Array<ModAddCommunity>,
|
||||
added: Array<ModAdd>,
|
||||
removed_posts: Array<ModRemovePost>;
|
||||
locked_posts: Array<ModLockPost>;
|
||||
stickied_posts: Array<ModStickyPost>;
|
||||
removed_comments: Array<ModRemoveComment>;
|
||||
removed_communities: Array<ModRemoveCommunity>;
|
||||
banned_from_community: Array<ModBanFromCommunity>;
|
||||
banned: Array<ModBan>;
|
||||
added_to_community: Array<ModAddCommunity>;
|
||||
added: Array<ModAdd>;
|
||||
}
|
||||
|
||||
export interface ModRemovePost {
|
||||
|
@ -253,7 +335,7 @@ export interface ModRemovePost {
|
|||
post_id: number;
|
||||
reason?: string;
|
||||
removed?: boolean;
|
||||
when_: string
|
||||
when_: string;
|
||||
mod_user_name: string;
|
||||
post_name: string;
|
||||
community_id: number;
|
||||
|
@ -261,104 +343,104 @@ export interface ModRemovePost {
|
|||
}
|
||||
|
||||
export interface ModLockPost {
|
||||
id: number,
|
||||
mod_user_id: number,
|
||||
post_id: number,
|
||||
locked?: boolean,
|
||||
when_: string,
|
||||
mod_user_name: string,
|
||||
post_name: string,
|
||||
community_id: number,
|
||||
community_name: string,
|
||||
id: number;
|
||||
mod_user_id: number;
|
||||
post_id: number;
|
||||
locked?: boolean;
|
||||
when_: string;
|
||||
mod_user_name: string;
|
||||
post_name: string;
|
||||
community_id: number;
|
||||
community_name: string;
|
||||
}
|
||||
|
||||
export interface ModStickyPost {
|
||||
id: number,
|
||||
mod_user_id: number,
|
||||
post_id: number,
|
||||
stickied?: boolean,
|
||||
when_: string,
|
||||
mod_user_name: string,
|
||||
post_name: string,
|
||||
community_id: number,
|
||||
community_name: string,
|
||||
id: number;
|
||||
mod_user_id: number;
|
||||
post_id: number;
|
||||
stickied?: boolean;
|
||||
when_: string;
|
||||
mod_user_name: string;
|
||||
post_name: string;
|
||||
community_id: number;
|
||||
community_name: string;
|
||||
}
|
||||
|
||||
export interface ModRemoveComment {
|
||||
id: number,
|
||||
mod_user_id: number,
|
||||
comment_id: number,
|
||||
reason?: string,
|
||||
removed?: boolean,
|
||||
when_: string,
|
||||
mod_user_name: string,
|
||||
comment_user_id: number,
|
||||
comment_user_name: string,
|
||||
comment_content: string,
|
||||
post_id: number,
|
||||
post_name: string,
|
||||
community_id: number,
|
||||
community_name: string,
|
||||
id: number;
|
||||
mod_user_id: number;
|
||||
comment_id: number;
|
||||
reason?: string;
|
||||
removed?: boolean;
|
||||
when_: string;
|
||||
mod_user_name: string;
|
||||
comment_user_id: number;
|
||||
comment_user_name: string;
|
||||
comment_content: string;
|
||||
post_id: number;
|
||||
post_name: string;
|
||||
community_id: number;
|
||||
community_name: string;
|
||||
}
|
||||
|
||||
export interface ModRemoveCommunity {
|
||||
id: number,
|
||||
mod_user_id: number,
|
||||
community_id: number,
|
||||
reason?: string,
|
||||
removed?: boolean,
|
||||
expires?: number,
|
||||
when_: string,
|
||||
mod_user_name: string,
|
||||
community_name: string,
|
||||
id: number;
|
||||
mod_user_id: number;
|
||||
community_id: number;
|
||||
reason?: string;
|
||||
removed?: boolean;
|
||||
expires?: number;
|
||||
when_: string;
|
||||
mod_user_name: string;
|
||||
community_name: string;
|
||||
}
|
||||
|
||||
export interface ModBanFromCommunity {
|
||||
id: number,
|
||||
mod_user_id: number,
|
||||
other_user_id: number,
|
||||
community_id: number,
|
||||
reason?: string,
|
||||
banned?: boolean,
|
||||
expires?: number,
|
||||
when_: string,
|
||||
mod_user_name: string,
|
||||
other_user_name: string,
|
||||
community_name: string,
|
||||
id: number;
|
||||
mod_user_id: number;
|
||||
other_user_id: number;
|
||||
community_id: number;
|
||||
reason?: string;
|
||||
banned?: boolean;
|
||||
expires?: number;
|
||||
when_: string;
|
||||
mod_user_name: string;
|
||||
other_user_name: string;
|
||||
community_name: string;
|
||||
}
|
||||
|
||||
export interface ModBan {
|
||||
id: number,
|
||||
mod_user_id: number,
|
||||
other_user_id: number,
|
||||
reason?: string,
|
||||
banned?: boolean,
|
||||
expires?: number,
|
||||
when_: string,
|
||||
mod_user_name: string,
|
||||
other_user_name: string,
|
||||
id: number;
|
||||
mod_user_id: number;
|
||||
other_user_id: number;
|
||||
reason?: string;
|
||||
banned?: boolean;
|
||||
expires?: number;
|
||||
when_: string;
|
||||
mod_user_name: string;
|
||||
other_user_name: string;
|
||||
}
|
||||
|
||||
export interface ModAddCommunity {
|
||||
id: number,
|
||||
mod_user_id: number,
|
||||
other_user_id: number,
|
||||
community_id: number,
|
||||
removed?: boolean,
|
||||
when_: string,
|
||||
mod_user_name: string,
|
||||
other_user_name: string,
|
||||
community_name: string,
|
||||
id: number;
|
||||
mod_user_id: number;
|
||||
other_user_id: number;
|
||||
community_id: number;
|
||||
removed?: boolean;
|
||||
when_: string;
|
||||
mod_user_name: string;
|
||||
other_user_name: string;
|
||||
community_name: string;
|
||||
}
|
||||
|
||||
export interface ModAdd {
|
||||
id: number,
|
||||
mod_user_id: number,
|
||||
other_user_id: number,
|
||||
removed?: boolean,
|
||||
when_: string,
|
||||
mod_user_name: string,
|
||||
other_user_name: string,
|
||||
id: number;
|
||||
mod_user_id: number;
|
||||
other_user_id: number;
|
||||
removed?: boolean;
|
||||
when_: string;
|
||||
mod_user_name: string;
|
||||
other_user_name: string;
|
||||
}
|
||||
|
||||
export interface LoginForm {
|
||||
|
@ -383,14 +465,16 @@ export interface LoginResponse {
|
|||
export interface UserSettingsForm {
|
||||
show_nsfw: boolean;
|
||||
theme: string;
|
||||
default_sort_type: SortType;
|
||||
default_listing_type: ListingType;
|
||||
auth: string;
|
||||
}
|
||||
|
||||
export interface CommunityForm {
|
||||
name: string;
|
||||
title: string;
|
||||
description?: string,
|
||||
category_id: number,
|
||||
description?: string;
|
||||
category_id: number;
|
||||
edit_id?: number;
|
||||
removed?: boolean;
|
||||
deleted?: boolean;
|
||||
|
@ -407,7 +491,6 @@ export interface GetCommunityResponse {
|
|||
admins: Array<UserView>;
|
||||
}
|
||||
|
||||
|
||||
export interface CommunityResponse {
|
||||
op: string;
|
||||
community: Community;
|
||||
|
@ -537,7 +620,7 @@ export interface CreatePostLikeResponse {
|
|||
|
||||
export interface SiteForm {
|
||||
name: string;
|
||||
description?: string,
|
||||
description?: string;
|
||||
removed?: boolean;
|
||||
reason?: string;
|
||||
expires?: number;
|
||||
|
@ -552,7 +635,6 @@ export interface GetSiteResponse {
|
|||
online: number;
|
||||
}
|
||||
|
||||
|
||||
export interface SiteResponse {
|
||||
op: string;
|
||||
site: Site;
|
||||
|
@ -561,15 +643,15 @@ export interface SiteResponse {
|
|||
export interface BanUserForm {
|
||||
user_id: number;
|
||||
ban: boolean;
|
||||
reason?: string,
|
||||
expires?: number,
|
||||
reason?: string;
|
||||
expires?: number;
|
||||
auth?: string;
|
||||
}
|
||||
|
||||
export interface BanUserResponse {
|
||||
op: string;
|
||||
user: UserView,
|
||||
banned: boolean,
|
||||
user: UserView;
|
||||
banned: boolean;
|
||||
}
|
||||
|
||||
export interface AddAdminForm {
|
||||
|
|
18
ui/src/services/UserService.ts
vendored
18
ui/src/services/UserService.ts
vendored
|
@ -5,13 +5,15 @@ import * as jwt_decode from 'jwt-decode';
|
|||
import { Subject } from 'rxjs';
|
||||
|
||||
export class UserService {
|
||||
|
||||
private static _instance: UserService;
|
||||
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() {
|
||||
let jwt = Cookies.get("jwt");
|
||||
let jwt = Cookies.get('jwt');
|
||||
if (jwt) {
|
||||
this.setUser(jwt);
|
||||
} else {
|
||||
|
@ -22,20 +24,20 @@ export class UserService {
|
|||
|
||||
public login(res: LoginResponse) {
|
||||
this.setUser(res.jwt);
|
||||
Cookies.set("jwt", res.jwt, { expires: 365 });
|
||||
console.log("jwt cookie set");
|
||||
Cookies.set('jwt', res.jwt, { expires: 365 });
|
||||
console.log('jwt cookie set');
|
||||
}
|
||||
|
||||
public logout() {
|
||||
this.user = undefined;
|
||||
Cookies.remove("jwt");
|
||||
Cookies.remove('jwt');
|
||||
setTheme();
|
||||
this.sub.next({ user: undefined, unreadCount: 0 });
|
||||
console.log("Logged out.");
|
||||
console.log('Logged out.');
|
||||
}
|
||||
|
||||
public get auth(): string {
|
||||
return Cookies.get("jwt");
|
||||
return Cookies.get('jwt');
|
||||
}
|
||||
|
||||
private setUser(jwt: string) {
|
||||
|
|
103
ui/src/services/WebSocketService.ts
vendored
103
ui/src/services/WebSocketService.ts
vendored
|
@ -1,5 +1,36 @@
|
|||
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 { Subject } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
|
@ -19,7 +50,14 @@ export class WebSocketService {
|
|||
|
||||
// Necessary to not keep reconnecting
|
||||
this.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(60000), take(999))))
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(1000)
|
||||
// take(999)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
console.log(`Connected to ${wsUri}`);
|
||||
|
@ -39,17 +77,23 @@ export class WebSocketService {
|
|||
|
||||
public createCommunity(communityForm: CommunityForm) {
|
||||
this.setAuth(communityForm);
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.CreateCommunity, communityForm));
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.CreateCommunity, communityForm)
|
||||
);
|
||||
}
|
||||
|
||||
public editCommunity(communityForm: CommunityForm) {
|
||||
this.setAuth(communityForm);
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.EditCommunity, communityForm));
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.EditCommunity, communityForm)
|
||||
);
|
||||
}
|
||||
|
||||
public followCommunity(followCommunityForm: FollowCommunityForm) {
|
||||
this.setAuth(followCommunityForm);
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.FollowCommunity, followCommunityForm));
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.FollowCommunity, followCommunityForm)
|
||||
);
|
||||
}
|
||||
|
||||
public listCommunities(form: ListCommunitiesForm) {
|
||||
|
@ -59,11 +103,15 @@ export class WebSocketService {
|
|||
|
||||
public getFollowedCommunities() {
|
||||
let data = { auth: UserService.Instance.auth };
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.GetFollowedCommunities, data));
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.GetFollowedCommunities, data)
|
||||
);
|
||||
}
|
||||
|
||||
public listCategories() {
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.ListCategories, undefined));
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.ListCategories, undefined)
|
||||
);
|
||||
}
|
||||
|
||||
public createPost(postForm: PostForm) {
|
||||
|
@ -88,17 +136,23 @@ export class WebSocketService {
|
|||
|
||||
public createComment(commentForm: CommentForm) {
|
||||
this.setAuth(commentForm);
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.CreateComment, commentForm));
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.CreateComment, commentForm)
|
||||
);
|
||||
}
|
||||
|
||||
public editComment(commentForm: CommentForm) {
|
||||
this.setAuth(commentForm);
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.EditComment, commentForm));
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.EditComment, commentForm)
|
||||
);
|
||||
}
|
||||
|
||||
public likeComment(form: CommentLikeForm) {
|
||||
this.setAuth(form);
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.CreateCommentLike, form));
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.CreateCommentLike, form)
|
||||
);
|
||||
}
|
||||
|
||||
public saveComment(form: SaveCommentForm) {
|
||||
|
@ -133,12 +187,16 @@ export class WebSocketService {
|
|||
|
||||
public addModToCommunity(form: AddModToCommunityForm) {
|
||||
this.setAuth(form);
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.AddModToCommunity, form));
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.AddModToCommunity, form)
|
||||
);
|
||||
}
|
||||
|
||||
public transferCommunity(form: TransferCommunityForm) {
|
||||
this.setAuth(form);
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.TransferCommunity, form));
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.TransferCommunity, form)
|
||||
);
|
||||
}
|
||||
|
||||
public transferSite(form: TransferSiteForm) {
|
||||
|
@ -166,6 +224,16 @@ export class WebSocketService {
|
|||
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) {
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, form));
|
||||
}
|
||||
|
@ -196,7 +264,9 @@ export class WebSocketService {
|
|||
|
||||
public saveUserSettings(userSettingsForm: UserSettingsForm) {
|
||||
this.setAuth(userSettingsForm);
|
||||
this.subject.next(this.wsSendWrapper(UserOperation.SaveUserSettings, userSettingsForm));
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.SaveUserSettings, userSettingsForm)
|
||||
);
|
||||
}
|
||||
|
||||
public deleteAccount(form: DeleteAccountForm) {
|
||||
|
@ -214,13 +284,12 @@ export class WebSocketService {
|
|||
obj.auth = UserService.Instance.auth;
|
||||
if (obj.auth == null && throwErr) {
|
||||
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 = null;
|
||||
});
|
||||
|
||||
};
|
||||
|
|
24
ui/src/translations/de.ts
vendored
24
ui/src/translations/de.ts
vendored
|
@ -88,7 +88,8 @@ export const de = {
|
|||
view: 'Ansicht',
|
||||
logout: 'Ausloggen',
|
||||
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',
|
||||
password: 'Passwort',
|
||||
verify_password: 'Passwort überprüfen',
|
||||
|
@ -111,14 +112,17 @@ export const de = {
|
|||
modified: 'verändert',
|
||||
sponsors: 'Sponsoren',
|
||||
sponsors_of_lemmy: 'Sponsoren von Lemmy',
|
||||
sponsor_message: 'Lemmy ist freie <1>Open-Source</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:',
|
||||
sponsor_message:
|
||||
'Lemmy ist freie <1>Open-Source</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:',
|
||||
support_on_patreon: 'Auf Patreon unterstützen',
|
||||
general_sponsors:'Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.',
|
||||
general_sponsors:
|
||||
'Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
code: 'Code',
|
||||
powered_by: 'Bereitgestellt durch',
|
||||
landing_0: 'Lemmy ist ein <1>Link Aggregator</1> / Reddit Alternative im <2>Fediverse</2>.<3></3>Es ist selbst-hostbar, hat live-updates von Kommentar-threads und ist winzig (<4>~80kB</4>). Federation in das ActivityPub Netzwerk ist geplant. <5></5>Dies ist eine <6>sehr frühe Beta Version</6>, und viele Features funktionieren zurzeit nicht richtig oder fehlen. <7></7>Schlage neue Features vor oder melde Bugs <8>hier.</8><9></9>Gebaut mit <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
landing_0:
|
||||
'Lemmy ist ein <1>Link Aggregator</1> / Reddit Alternative im <2>Fediverse</2>.<3></3>Es ist selbst-hostbar, hat live-updates von Kommentar-threads und ist winzig (<4>~80kB</4>). Federation in das ActivityPub Netzwerk ist geplant. <5></5>Dies ist eine <6>sehr frühe Beta Version</6>, und viele Features funktionieren zurzeit nicht richtig oder fehlen. <7></7>Schlage neue Features vor oder melde Bugs <8>hier.</8><9></9>Gebaut mit <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
not_logged_in: 'Nicht eingeloggt.',
|
||||
community_ban: 'Du wurdest von dieser Community gebannt.',
|
||||
site_ban: 'Du wurdest von dieser Seite gebannt',
|
||||
|
@ -132,7 +136,8 @@ export const de = {
|
|||
couldnt_find_community: 'Konnte Community nicht finden.',
|
||||
couldnt_update_community: 'Konnte Community nicht aktualisieren.',
|
||||
community_already_exists: 'Community existiert bereits.',
|
||||
community_moderator_already_exists: 'Community Moderator existiert bereits.',
|
||||
community_moderator_already_exists:
|
||||
'Community Moderator existiert bereits.',
|
||||
community_follower_already_exists: 'Community Follower existiert bereits.',
|
||||
community_user_already_banned: 'Community Nutzer schon gebannt.',
|
||||
couldnt_create_post: 'Konnte Beitrag nicht anlegen.',
|
||||
|
@ -145,13 +150,14 @@ export const de = {
|
|||
not_an_admin: 'Kein Administrator.',
|
||||
site_already_exists: 'Seite existiert bereits.',
|
||||
couldnt_update_site: 'Konnte Seite nicht aktualisieren.',
|
||||
couldnt_find_that_username_or_email: 'Konnte Username oder E-Mail nicht finden.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'Konnte Username oder E-Mail nicht finden.',
|
||||
password_incorrect: 'Passwort falsch.',
|
||||
passwords_dont_match: 'Passwörter stimmen nicht überein.',
|
||||
admin_already_created: 'Entschuldigung, es gibt schon einen Administrator.',
|
||||
user_already_exists: 'Nutzer existiert bereits.',
|
||||
couldnt_update_user: 'Konnte Nutzer nicht aktualisieren',
|
||||
system_err_login: 'Systemfehler. Versuche dich aus- und wieder einzuloggen.',
|
||||
system_err_login:
|
||||
'Systemfehler. Versuche dich aus- und wieder einzuloggen.',
|
||||
},
|
||||
}
|
||||
|
||||
};
|
||||
|
|
53
ui/src/translations/en.ts
vendored
53
ui/src/translations/en.ts
vendored
|
@ -56,7 +56,8 @@ export const en = {
|
|||
delete: 'delete',
|
||||
deleted: 'deleted',
|
||||
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',
|
||||
ban: 'ban',
|
||||
ban_from_site: 'ban from site',
|
||||
|
@ -100,6 +101,8 @@ export const en = {
|
|||
mark_all_as_read: 'mark all as read',
|
||||
type: 'Type',
|
||||
unread: 'Unread',
|
||||
replies: 'Replies',
|
||||
mentions: 'Mentions',
|
||||
reply_sent: 'Reply sent',
|
||||
search: 'Search',
|
||||
overview: 'Overview',
|
||||
|
@ -108,7 +111,8 @@ export const en = {
|
|||
login_sign_up: 'Login / Sign up',
|
||||
login: 'Login',
|
||||
sign_up: 'Sign Up',
|
||||
notifications_error: 'Desktop notifications not available in your browser. Try Firefox or Chrome.',
|
||||
notifications_error:
|
||||
'Desktop notifications not available in your browser. Try Firefox or Chrome.',
|
||||
unread_messages: 'Unread Messages',
|
||||
password: 'Password',
|
||||
verify_password: 'Verify Password',
|
||||
|
@ -134,9 +138,11 @@ export const en = {
|
|||
theme: 'Theme',
|
||||
sponsors: 'Sponsors',
|
||||
sponsors_of_lemmy: 'Sponsors of Lemmy',
|
||||
sponsor_message: 'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
|
||||
sponsor_message:
|
||||
'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
|
||||
support_on_patreon: 'Support on Patreon',
|
||||
general_sponsors: 'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
||||
general_sponsors:
|
||||
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
||||
crypto: 'Crypto',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
|
@ -151,40 +157,41 @@ export const en = {
|
|||
yes: 'yes',
|
||||
no: 'no',
|
||||
powered_by: 'Powered by',
|
||||
landing_0: 'Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It\'s self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
landing_0:
|
||||
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
|
||||
not_logged_in: 'Not logged in.',
|
||||
community_ban: 'You have been banned from this community.',
|
||||
site_ban: 'You have been banned from the site',
|
||||
couldnt_create_comment: 'Couldn\'t create comment.',
|
||||
couldnt_like_comment: 'Couldn\'t like comment.',
|
||||
couldnt_update_comment: 'Couldn\'t update comment.',
|
||||
couldnt_save_comment: 'Couldn\'t save comment.',
|
||||
couldnt_create_comment: "Couldn't create comment.",
|
||||
couldnt_like_comment: "Couldn't like comment.",
|
||||
couldnt_update_comment: "Couldn't update comment.",
|
||||
couldnt_save_comment: "Couldn't save comment.",
|
||||
no_comment_edit_allowed: 'Not allowed to edit comment.',
|
||||
no_post_edit_allowed: 'Not allowed to edit post.',
|
||||
no_community_edit_allowed: 'Not allowed to edit community.',
|
||||
couldnt_find_community: 'Couldn\'t find community.',
|
||||
couldnt_update_community: 'Couldn\'t update Community.',
|
||||
couldnt_find_community: "Couldn't find community.",
|
||||
couldnt_update_community: "Couldn't update Community.",
|
||||
community_already_exists: 'Community already exists.',
|
||||
community_moderator_already_exists: 'Community moderator already exists.',
|
||||
community_follower_already_exists: 'Community follower already exists.',
|
||||
community_user_already_banned: 'Community user already banned.',
|
||||
couldnt_create_post: 'Couldn\'t create post.',
|
||||
couldnt_like_post: 'Couldn\'t like post.',
|
||||
couldnt_find_post: 'Couldn\'t find post.',
|
||||
couldnt_get_posts: 'Couldn\'t get posts',
|
||||
couldnt_update_post: 'Couldn\'t update post',
|
||||
couldnt_save_post: 'Couldn\'t save post.',
|
||||
couldnt_create_post: "Couldn't create post.",
|
||||
couldnt_like_post: "Couldn't like post.",
|
||||
couldnt_find_post: "Couldn't find post.",
|
||||
couldnt_get_posts: "Couldn't get posts",
|
||||
couldnt_update_post: "Couldn't update post",
|
||||
couldnt_save_post: "Couldn't save post.",
|
||||
no_slurs: 'No slurs.',
|
||||
not_an_admin: 'Not an admin.',
|
||||
site_already_exists: 'Site already exists.',
|
||||
couldnt_update_site: 'Couldn\'t update site.',
|
||||
couldnt_find_that_username_or_email: 'Couldn\'t find that username or email.',
|
||||
couldnt_update_site: "Couldn't update site.",
|
||||
couldnt_find_that_username_or_email:
|
||||
"Couldn't find that username or email.",
|
||||
password_incorrect: 'Password incorrect.',
|
||||
passwords_dont_match: 'Passwords do not match.',
|
||||
admin_already_created: 'Sorry, there\'s already an admin.',
|
||||
admin_already_created: "Sorry, there's already an admin.",
|
||||
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.',
|
||||
},
|
||||
}
|
||||
|
||||
};
|
||||
|
|
18
ui/src/translations/eo.ts
vendored
18
ui/src/translations/eo.ts
vendored
|
@ -95,7 +95,8 @@ export const eo = {
|
|||
login_sign_up: 'Ensaluti / Registriĝi',
|
||||
login: 'Ensaluti',
|
||||
sign_up: 'Registriĝi',
|
||||
notifications_error: 'Labortablaj avizoj estas nehavebla en via retumilo. Provu Firefox-on aŭ Chrome-on.',
|
||||
notifications_error:
|
||||
'Labortablaj avizoj estas nehavebla en via retumilo. Provu Firefox-on aŭ Chrome-on.',
|
||||
unread_messages: 'Nelegitaj Mesaĝoj',
|
||||
password: 'Pasvorto',
|
||||
verify_password: 'Konfirmu Vian Pasvorton',
|
||||
|
@ -120,9 +121,11 @@ export const eo = {
|
|||
show_nsfw: 'Vidigi NSFW-an enhavon',
|
||||
sponsors: 'Subtenantoj',
|
||||
sponsors_of_lemmy: 'Subtenantoj de Lemmy',
|
||||
sponsor_message: 'Lemmy estas senpaga, <1>liberkoda</1> programaro. Tio signifas ne reklami, pagigi, aŭ riska kapitalo, ĉiam. Viaj donacoj rekte subtenas plentempan evoluon de la projekto. Dankon al tiuj homoj:',
|
||||
sponsor_message:
|
||||
'Lemmy estas senpaga, <1>liberkoda</1> programaro. Tio signifas ne reklami, pagigi, aŭ riska kapitalo, ĉiam. Viaj donacoj rekte subtenas plentempan evoluon de la projekto. Dankon al tiuj homoj:',
|
||||
support_on_patreon: 'Subteni per Patreon',
|
||||
general_sponsors:'Ĝeneralaj Subtenantoj estas tiuj ke donacis inter $10 kaj $39 al Lemmy.',
|
||||
general_sponsors:
|
||||
'Ĝeneralaj Subtenantoj estas tiuj ke donacis inter $10 kaj $39 al Lemmy.',
|
||||
crypto: 'Crypto',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
|
@ -134,7 +137,8 @@ export const eo = {
|
|||
transfer_community: 'transdoni la komunumon',
|
||||
transfer_site: 'transdoni la retejon',
|
||||
powered_by: 'Konstruis per',
|
||||
landing_0: 'Lemmy estas <1>ligila agregatilo</1> / Reddit anstataŭo ke intenciĝas funkci en la <2>federacio-universo</2>.<3></3>ĝi estas mem-gastigebla, havas nuna-ĝisdatigajn komentarojn, kaj estas malgrandega (<4>~80kB</4>). Federacio en la ActivityPub-an reton estas planizita. <5></5>Estas <6>fruega beta versio</6>, kaj multaj trajtoj estas nune difektaj aŭ mankaj. <7></7>Sugestias novajn trajtojn aŭ raportas cimojn <8>ĉi tie.</8><9></9>Faris per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
landing_0:
|
||||
'Lemmy estas <1>ligila agregatilo</1> / Reddit anstataŭo ke intenciĝas funkci en la <2>federacio-universo</2>.<3></3>ĝi estas mem-gastigebla, havas nuna-ĝisdatigajn komentarojn, kaj estas malgrandega (<4>~80kB</4>). Federacio en la ActivityPub-an reton estas planizita. <5></5>Estas <6>fruega beta versio</6>, kaj multaj trajtoj estas nune difektaj aŭ mankaj. <7></7>Sugestias novajn trajtojn aŭ raportas cimojn <8>ĉi tie.</8><9></9>Faris per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
not_logged_in: 'Ne estas ensalutinta.',
|
||||
community_ban: 'Vi estas forbarita de la komunumo.',
|
||||
site_ban: 'Vi estas forbarita de la retejo',
|
||||
|
@ -161,7 +165,8 @@ export const eo = {
|
|||
not_an_admin: 'Ne estas administranto.',
|
||||
site_already_exists: 'Retejo jam ekzistas.',
|
||||
couldnt_update_site: 'Ne povis ĝisdatigi la retejon.',
|
||||
couldnt_find_that_username_or_email: 'Ne povis trovi tiun uzantnomon aŭ retadreson.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'Ne povis trovi tiun uzantnomon aŭ retadreson.',
|
||||
password_incorrect: 'Pasvorto malĝustas.',
|
||||
passwords_dont_match: 'Pasvortoj ne samas.',
|
||||
admin_already_created: 'Pardonu, jam estas administranto.',
|
||||
|
@ -169,5 +174,4 @@ export const eo = {
|
|||
couldnt_update_user: 'Ne povis ĝisdatigi la uzanton.',
|
||||
system_err_login: 'Sistema eraro. Provu elsaluti kaj ensaluti.',
|
||||
},
|
||||
}
|
||||
|
||||
};
|
||||
|
|
33
ui/src/translations/es.ts
vendored
33
ui/src/translations/es.ts
vendored
|
@ -56,7 +56,8 @@ export const es = {
|
|||
delete: 'eliminar',
|
||||
deleted: 'eliminado',
|
||||
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',
|
||||
ban: 'expulsar',
|
||||
ban_from_site: 'expulsar del sitio',
|
||||
|
@ -108,7 +109,8 @@ export const es = {
|
|||
login_sign_up: 'Iniciar sesión / Crear cuenta',
|
||||
login: 'Iniciar sesión',
|
||||
sign_up: 'Crear cuenta',
|
||||
notifications_error: 'Notificaciones de escritorio no disponibles en tu navegador. Prueba Firefox o Chrome.',
|
||||
notifications_error:
|
||||
'Notificaciones de escritorio no disponibles en tu navegador. Prueba Firefox o Chrome.',
|
||||
unread_messages: 'Mensajes no leídos',
|
||||
password: 'Contraseña',
|
||||
verify_password: 'Verificar contraseña',
|
||||
|
@ -134,9 +136,11 @@ export const es = {
|
|||
theme: 'Tema',
|
||||
sponsors: 'Patrocinadores',
|
||||
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',
|
||||
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',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
|
@ -151,7 +155,8 @@ export const es = {
|
|||
yes: 'sí',
|
||||
no: 'no',
|
||||
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.',
|
||||
community_ban: 'Has sido expulsado de esta comunidad.',
|
||||
site_ban: 'Has sido expulsado del sitio',
|
||||
|
@ -165,9 +170,12 @@ export const es = {
|
|||
couldnt_find_community: 'No se pudo encontrar la comunidad.',
|
||||
couldnt_update_community: 'No se pudo actualizar la comunidad.',
|
||||
community_already_exists: 'Esta comunidad ya existe.',
|
||||
community_moderator_already_exists: 'Este moderador de la comunidad ya existe.',
|
||||
community_follower_already_exists: 'Este seguidor de la comunidad ya existe.',
|
||||
community_user_already_banned: 'Este usuario de la comunidad ya fue expulsado.',
|
||||
community_moderator_already_exists:
|
||||
'Este moderador de la comunidad ya existe.',
|
||||
community_follower_already_exists:
|
||||
'Este seguidor de la comunidad ya existe.',
|
||||
community_user_already_banned:
|
||||
'Este usuario de la comunidad ya fue expulsado.',
|
||||
couldnt_create_post: 'No se pudo crear la publicación.',
|
||||
couldnt_like_post: 'No se pudo gustar la publicación.',
|
||||
couldnt_find_post: 'No se pudo encontrar la publicación.',
|
||||
|
@ -178,13 +186,14 @@ export const es = {
|
|||
not_an_admin: 'No es un administrador.',
|
||||
site_already_exists: 'El sitio ya existe.',
|
||||
couldnt_update_site: 'No se pudo actualizar el sitio.',
|
||||
couldnt_find_that_username_or_email: 'No se pudo encontrar ese nombre de usuario o correo electrónico.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'No se pudo encontrar ese nombre de usuario o correo electrónico.',
|
||||
password_incorrect: 'Contraseña incorrecta.',
|
||||
passwords_dont_match: 'Las contraseñas no coinciden.',
|
||||
admin_already_created: 'Lo sentimos, ya hay un adminisitrador.',
|
||||
user_already_exists: 'El usuario ya existe.',
|
||||
couldnt_update_user: 'No se pudo actualizar el usuario.',
|
||||
system_err_login: 'Error del sistema. Intente cerrar sesión e ingresar de nuevo.',
|
||||
system_err_login:
|
||||
'Error del sistema. Intente cerrar sesión e ingresar de nuevo.',
|
||||
},
|
||||
}
|
||||
|
||||
};
|
||||
|
|
68
ui/src/translations/fr.ts
vendored
68
ui/src/translations/fr.ts
vendored
|
@ -29,8 +29,11 @@ export const fr = {
|
|||
preview: 'prévisualiser',
|
||||
upload_image: 'téléverser une image',
|
||||
formatting_help: 'aide de formattage',
|
||||
view_source: 'voir les sources',
|
||||
unlock: 'débloquer',
|
||||
lock: 'bloquer',
|
||||
sticky: 'épingler',
|
||||
unsticky: 'désépingler',
|
||||
link: 'lien',
|
||||
mod: 'modérateur',
|
||||
mods: 'modérateurs',
|
||||
|
@ -46,11 +49,15 @@ export const fr = {
|
|||
remove: 'retirer',
|
||||
removed: 'retiré',
|
||||
locked: 'bloqué',
|
||||
stickied: 'épinglé',
|
||||
reason: 'Raison',
|
||||
mark_as_read: 'marquer comme lu',
|
||||
mark_as_unread: 'marquer comme non-lu',
|
||||
delete: 'supprimer',
|
||||
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',
|
||||
ban: 'bannir',
|
||||
ban_from_site: 'bannir du site',
|
||||
|
@ -60,11 +67,13 @@ export const fr = {
|
|||
save: 'sauvegarder',
|
||||
unsave: 'retirer',
|
||||
create: 'créer',
|
||||
username: 'Nom d\'utilisateur',
|
||||
email_or_username: 'Email ou Nom d\'utilisateur',
|
||||
creator: 'createur',
|
||||
username: "Nom d'utilisateur",
|
||||
email_or_username: "Email ou Nom d'utilisateur",
|
||||
number_of_users: '{{count}} Utilisateurs',
|
||||
number_of_subscribers: '{{count}} Abonnés',
|
||||
number_of_points: '{{count}} Points',
|
||||
number_online: '{{count}} Utilisateurs en ligne',
|
||||
name: 'Nom',
|
||||
title: 'Titre',
|
||||
category: 'Catégorie',
|
||||
|
@ -72,7 +81,7 @@ export const fr = {
|
|||
both: 'Les deux',
|
||||
saved: 'Sauvegardé',
|
||||
unsubscribe: 'Se désincrire',
|
||||
subscribe: 'S\'inscrire',
|
||||
subscribe: "S'inscrire",
|
||||
subscribed: 'Inscris',
|
||||
prev: 'Précédent',
|
||||
next: 'Suivant',
|
||||
|
@ -97,10 +106,11 @@ export const fr = {
|
|||
overview: 'Général',
|
||||
view: 'Voir',
|
||||
logout: 'Se déconnecter',
|
||||
login_sign_up: 'Se connecter / S\'inscrire',
|
||||
login_sign_up: "Se connecter / S'inscrire",
|
||||
login: 'Se connecter',
|
||||
sign_up: 'S\'inscrire',
|
||||
notifications_error: 'Les notifications de bureau ne sont pas discponibles sur votre navigateur. Essayez Firefox ou Chrome.',
|
||||
sign_up: "S'inscrire",
|
||||
notifications_error:
|
||||
'Les notifications de bureau ne sont pas discponibles sur votre navigateur. Essayez Firefox ou Chrome.',
|
||||
unread_messages: 'Messages non-lu',
|
||||
password: 'Mot de passe',
|
||||
verify_password: 'Vérifiez le mot de passe',
|
||||
|
@ -112,22 +122,25 @@ export const fr = {
|
|||
copy_suggested_title: 'Ajouter le titre suggéré: {{title}}',
|
||||
community: 'Communauté',
|
||||
expand_here: 'Développer ici',
|
||||
subscribe_to_communities: 'S\'abonner à quelques <1>communautés</1>.',
|
||||
subscribe_to_communities: "S'abonner à quelques <1>communautés</1>.",
|
||||
chat: 'Chat',
|
||||
recent_comments: 'Commentaires récents',
|
||||
no_results: 'Pas de résultats.',
|
||||
setup: 'Installation',
|
||||
lemmy_instance_setup: 'Installation d\'une instance Lemmy',
|
||||
lemmy_instance_setup: "Installation d'une instance Lemmy",
|
||||
setup_admin: 'Créer un administrateur',
|
||||
your_site: 'votre site',
|
||||
modified: 'modifié',
|
||||
nsfw: 'Pas sûr pour le travail',
|
||||
show_nsfw: 'Afficher le contenu NSFW',
|
||||
theme: 'Thème',
|
||||
sponsors: 'Sponsors',
|
||||
sponsors_of_lemmy: 'Sponsors de Lemmy',
|
||||
sponsor_message: 'Lemmy est gratuit et <1>open-source</1>, c\'est à dire sans publicité et sans monétisation. Pour toujours. Vos dons soutiennent directement le développement du projet. Merci à nos soutiens.',
|
||||
sponsor_message:
|
||||
"Lemmy est gratuit et <1>open-source</1>, c'est à dire sans publicité et sans monétisation. Pour toujours. Vos dons soutiennent directement le développement du projet. Merci à nos soutiens.",
|
||||
support_on_patreon: 'Soutenir sur Patreon',
|
||||
general_sponsors:'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
||||
general_sponsors:
|
||||
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
||||
crypto: 'Crypto',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
|
@ -142,39 +155,44 @@ export const fr = {
|
|||
yes: 'oui',
|
||||
no: 'non',
|
||||
powered_by: 'Propulsé par',
|
||||
landing_0: 'Lemmy est un <1>aggrégateur de lien</1>, similaire à reddit et conçu pour fonctionner sur le <2>fédiverse</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via Activitypub est prévue sur la feuille de route. <5></5>Lemmy est une <6>version beta très précoce</6>, et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez rapporter des bugs et suggérez de nouvelles fonctionnalités <8>ici.</8><9></9>Crée avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
not_logged_in: 'Vous n\'êtes pas connecté.',
|
||||
landing_0:
|
||||
'Lemmy est un <1>aggrégateur de lien</1>, similaire à reddit et conçu pour fonctionner sur le <2>fédiverse</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via Activitypub est prévue sur la feuille de route. <5></5>Lemmy est une <6>version beta très précoce</6>, et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez rapporter des bugs et suggérez de nouvelles fonctionnalités <8>ici.</8><9></9>Crée avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
not_logged_in: "Vous n'êtes pas connecté.",
|
||||
community_ban: 'Vous avez été banni de cette communauté.',
|
||||
site_ban: 'Vous avez été banni du site',
|
||||
couldnt_create_comment: 'Impossible de poster le commentaire.',
|
||||
couldnt_like_comment: 'Impossible d\'aimer le commentaire.',
|
||||
couldnt_like_comment: "Impossible d'aimer le commentaire.",
|
||||
couldnt_update_comment: 'Impossible de mettre à jour le commentaire.',
|
||||
couldnt_save_comment: 'Impossible de sauvegarder le commentaire.',
|
||||
no_comment_edit_allowed: 'Vous n\'êtes pas autorisé à éditer ce commentaire.',
|
||||
no_post_edit_allowed: 'ous n\'êtes pas autorisé à éditer sujet.',
|
||||
no_community_edit_allowed: 'ous n\'êtes pas autorisé à éditer cette communauté.',
|
||||
no_comment_edit_allowed:
|
||||
"Vous n'êtes pas autorisé à éditer ce commentaire.",
|
||||
no_post_edit_allowed: "ous n'êtes pas autorisé à éditer sujet.",
|
||||
no_community_edit_allowed:
|
||||
"ous n'êtes pas autorisé à éditer cette communauté.",
|
||||
couldnt_find_community: 'Impossible de trouver cette communauté.',
|
||||
couldnt_update_community: 'Impossible d\'éditer cette communauté.',
|
||||
couldnt_update_community: "Impossible d'éditer cette communauté.",
|
||||
community_already_exists: 'Cette communauté existe déjà.',
|
||||
community_moderator_already_exists: 'Ce membre est déjà modérateur.',
|
||||
community_follower_already_exists: 'Ce membre est déjà abonné.',
|
||||
community_user_already_banned: 'Ce membre est déjà banni.',
|
||||
couldnt_create_post: 'Impossible dae créer le sujet.',
|
||||
couldnt_like_post: 'Impossible d\'aimer le sujet.',
|
||||
couldnt_like_post: "Impossible d'aimer 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_save_post: 'Impossible de sauvegarder le sujet.',
|
||||
no_slurs: 'Pas d\'insultes.',
|
||||
no_slurs: "Pas d'insultes.",
|
||||
not_an_admin: 'Pas administrateur.',
|
||||
site_already_exists: 'Le site existe déjà.',
|
||||
couldnt_update_site: 'Impossible de mettre à jour le site.',
|
||||
couldnt_find_that_username_or_email: 'Impossible de trouver cet utilisateur ou cet email.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'Impossible de trouver cet utilisateur ou cet email.',
|
||||
password_incorrect: 'Mot de passe incorrect.',
|
||||
passwords_dont_match: 'Les mots de passes ne correspondent pas..',
|
||||
admin_already_created: 'Désolé, il y a déjà un admin.',
|
||||
user_already_exists: 'L\'utilisateur existe déjà.',
|
||||
couldnt_update_user: 'Impossible de mettre à jour l\'utilisateur.',
|
||||
system_err_login: 'Erreur système. Essayez de vous déconneter puis de vous reconnecter.',
|
||||
user_already_exists: "L'utilisateur existe déjà.",
|
||||
couldnt_update_user: "Impossible de mettre à jour l'utilisateur.",
|
||||
system_err_login:
|
||||
'Erreur système. Essayez de vous déconneter puis de vous reconnecter.',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
20
ui/src/translations/nl.ts
vendored
20
ui/src/translations/nl.ts
vendored
|
@ -96,7 +96,8 @@ export const nl = {
|
|||
login_sign_up: 'Log in / Aanmelden',
|
||||
login: 'Log in',
|
||||
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',
|
||||
password: 'Wachtwoord',
|
||||
verify_password: 'Herhaal wachtwoord',
|
||||
|
@ -121,9 +122,11 @@ export const nl = {
|
|||
show_nsfw: 'Laat NSFW-inhoud zien',
|
||||
sponsors: 'Sponsoren',
|
||||
sponsors_of_lemmy: 'Sponsoren van Lemmy',
|
||||
sponsor_message: 'Lemmy is vrije, <1>open-source</1> software, dus zonder reclame, winstoogmerk en durfkapitaal, punt. Jouw donaties gaan direct naar de full-time-ontwikkeling van het project. Met veel dank aan de volgende mensen:',
|
||||
sponsor_message:
|
||||
'Lemmy is vrije, <1>open-source</1> software, dus zonder reclame, winstoogmerk en durfkapitaal, punt. Jouw donaties gaan direct naar de full-time-ontwikkeling van het project. Met veel dank aan de volgende mensen:',
|
||||
support_on_patreon: 'Ondersteun op Patreon',
|
||||
general_sponsors:'Algemene sponsors zijn sponsors die tussen de $10 en $39 hebben gegeven aan Lemmy.',
|
||||
general_sponsors:
|
||||
'Algemene sponsors zijn sponsors die tussen de $10 en $39 hebben gegeven aan Lemmy.',
|
||||
crypto: 'Cryptovaluta',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
|
@ -138,7 +141,8 @@ export const nl = {
|
|||
yes: 'ja',
|
||||
no: 'nee',
|
||||
powered_by: 'Mogelijk gemaakt door',
|
||||
landing_0: 'Lemmy is een <1>linkverzameler</1> / reddit-alternatief, bedoeld om in de <2>fediverse</2> te werken.<3></3>Lemmy kan door om het even wie gehost worden, heeft live-bijgewerkte reacties en is superklein (<4>ca. 80 kB</4>). Federatie in hte ActivityPub-netwerk is gepland. <5></5>Dit is een <6>erg vroege bèta-versie</6>, en een hoop functies zijn stuk of afwezig. <7></7>Stel nieuwe functies voor of meldt fouten <8>hier</8>.<9></9>Gemaakt met <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> en <13>Typescript</13>.',
|
||||
landing_0:
|
||||
'Lemmy is een <1>linkverzameler</1> / reddit-alternatief, bedoeld om in de <2>fediverse</2> te werken.<3></3>Lemmy kan door om het even wie gehost worden, heeft live-bijgewerkte reacties en is superklein (<4>ca. 80 kB</4>). Federatie in hte ActivityPub-netwerk is gepland. <5></5>Dit is een <6>erg vroege bèta-versie</6>, en een hoop functies zijn stuk of afwezig. <7></7>Stel nieuwe functies voor of meldt fouten <8>hier</8>.<9></9>Gemaakt met <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> en <13>Typescript</13>.',
|
||||
not_logged_in: 'Niet ingelogd.',
|
||||
community_ban: 'Je bent verbannen uit deze community.',
|
||||
site_ban: 'Je bent verbannen van deze site.',
|
||||
|
@ -165,12 +169,14 @@ export const nl = {
|
|||
not_an_admin: 'Niet een beheerder.',
|
||||
site_already_exists: 'Site bestaat al.',
|
||||
couldnt_update_site: 'Kon site niet bijwerken.',
|
||||
couldnt_find_that_username_or_email: 'Kon gebruikersnaam of e-mailadres niet vinden.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'Kon gebruikersnaam of e-mailadres niet vinden.',
|
||||
password_incorrect: 'Wachtwoord incorrect.',
|
||||
passwords_dont_match: 'Wachtwoorden zijn niet gelijk.',
|
||||
admin_already_created: 'Sorry, er is al een beheerder.',
|
||||
user_already_exists: 'Gebruiker bestaat al.',
|
||||
couldnt_update_user: 'Kon gebruiker niet bijwerken.',
|
||||
system_err_login: 'Systeemfout. Probeer uit te loggen en weer in te loggen.',
|
||||
system_err_login:
|
||||
'Systeemfout. Probeer uit te loggen en weer in te loggen.',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
21
ui/src/translations/ru.ts
vendored
21
ui/src/translations/ru.ts
vendored
|
@ -93,7 +93,8 @@ export const ru = {
|
|||
login_sign_up: 'Войти / Регистрация',
|
||||
login: 'Авторизация',
|
||||
sign_up: 'Регистрация',
|
||||
notifications_error: 'Уведомления на рабочем столе недоступны в вашем браузере. Попробуйте Firefox или Chrome.',
|
||||
notifications_error:
|
||||
'Уведомления на рабочем столе недоступны в вашем браузере. Попробуйте Firefox или Chrome.',
|
||||
unread_messages: 'Непрочитанные сообщения',
|
||||
password: 'Пароль',
|
||||
verify_password: 'Повторите пароль',
|
||||
|
@ -117,16 +118,19 @@ export const ru = {
|
|||
show_nsfw: 'Показывать NSFW-контент',
|
||||
sponsors: 'Спонсоры',
|
||||
sponsors_of_lemmy: 'Спонсоры Lemmy',
|
||||
sponsor_message: 'Lemmy это бесплатное, <1>открытое</1> программное обеспечение, что означает отсутствие рекламы, монетизации или венчурного капитала, никогда. Ваши пожертвования напрямую поддерживают развитие проекта. Спасибо нижеуказанным людям:',
|
||||
sponsor_message:
|
||||
'Lemmy это бесплатное, <1>открытое</1> программное обеспечение, что означает отсутствие рекламы, монетизации или венчурного капитала, никогда. Ваши пожертвования напрямую поддерживают развитие проекта. Спасибо нижеуказанным людям:',
|
||||
support_on_patreon: 'Поддержать на Patreon',
|
||||
general_sponsors:'Генеральные спонсоры - это те, кто пообещал Lemmy от $10 до $39.',
|
||||
general_sponsors:
|
||||
'Генеральные спонсоры - это те, кто пообещал Lemmy от $10 до $39.',
|
||||
crypto: 'Крипто',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
code: 'Код',
|
||||
joined: 'Присоединился',
|
||||
powered_by: 'Работает на',
|
||||
landing_0: 'Lemmy - это <1>агрегатор ссылок</1> / альтернатива reddit, предназначенный для работы в <2>федиверсе</2>.<3></3>Это самодостаточная система, с обновляемыми комментариями, и эта система крошечная (<4>~80 Кб</4>). Федерация в сети ActivityPub находится в разработке. <5></5>Это <6>очень ранняя бета-версия</6>, и многие функции в настоящее время сломаны или отсутствуют. <7></7>Предлагать новые функции или сообщать об ошибках можно <8>здесь.</8><9></9>Сделано на <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
landing_0:
|
||||
'Lemmy - это <1>агрегатор ссылок</1> / альтернатива reddit, предназначенный для работы в <2>федиверсе</2>.<3></3>Это самодостаточная система, с обновляемыми комментариями, и эта система крошечная (<4>~80 Кб</4>). Федерация в сети ActivityPub находится в разработке. <5></5>Это <6>очень ранняя бета-версия</6>, и многие функции в настоящее время сломаны или отсутствуют. <7></7>Предлагать новые функции или сообщать об ошибках можно <8>здесь.</8><9></9>Сделано на <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
not_logged_in: 'Не авторизованы.',
|
||||
community_ban: 'Вы были заблокированы на данном сообществе.',
|
||||
site_ban: 'Вы были заблокированы на данном сайте',
|
||||
|
@ -153,13 +157,14 @@ export const ru = {
|
|||
not_an_admin: 'Не администратор.',
|
||||
site_already_exists: 'Сайт уже существует.',
|
||||
couldnt_update_site: 'Не получилось обновить сайт.',
|
||||
couldnt_find_that_username_or_email: 'Не получилось найти данное имя пользователя или электронную почту.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'Не получилось найти данное имя пользователя или электронную почту.',
|
||||
password_incorrect: 'Неверный пароль.',
|
||||
passwords_dont_match: 'Пароли не совпадают.',
|
||||
admin_already_created: 'Извините, уже есть администратор.',
|
||||
user_already_exists: 'Пользователь уже существует.',
|
||||
couldnt_update_user: 'Не получилось обновить пользователя.',
|
||||
system_err_login: 'Системная ошибка. Попробуйте выйти из системы и вернуться обратно.',
|
||||
system_err_login:
|
||||
'Системная ошибка. Попробуйте выйти из системы и вернуться обратно.',
|
||||
},
|
||||
}
|
||||
|
||||
};
|
||||
|
|
20
ui/src/translations/sv.ts
vendored
20
ui/src/translations/sv.ts
vendored
|
@ -56,7 +56,8 @@ export const sv = {
|
|||
delete: 'radera',
|
||||
deleted: 'raderad',
|
||||
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',
|
||||
ban: 'blockera',
|
||||
ban_from_site: 'blockera från webbplats',
|
||||
|
@ -108,7 +109,8 @@ export const sv = {
|
|||
login_sign_up: 'Logga in eller skapa konto',
|
||||
login: 'Logga in',
|
||||
sign_up: 'Skapa konto',
|
||||
notifications_error: 'Din webbläsare har inte stöd för skrivbordsaviseringar. Testa Firefox eller Chrome.',
|
||||
notifications_error:
|
||||
'Din webbläsare har inte stöd för skrivbordsaviseringar. Testa Firefox eller Chrome.',
|
||||
unread_messages: 'Olästa meddelanden',
|
||||
password: 'Lösenord',
|
||||
verify_password: 'Bekräfta lösenord',
|
||||
|
@ -134,9 +136,11 @@ export const sv = {
|
|||
theme: 'Utseende',
|
||||
sponsors: 'Sponsorer',
|
||||
sponsors_of_lemmy: 'Lemmys sponsorer',
|
||||
sponsor_message: 'Lemmy är fri mjukvara med <1>öppen källkod</1>, vilket innebär att ingen reklam, vinstindrivning eller venturekapital förekommer, någonsin. Dina donationer går direkt till att stöda utvecklingen av projektet. Stort tack till följande personer:',
|
||||
sponsor_message:
|
||||
'Lemmy är fri mjukvara med <1>öppen källkod</1>, vilket innebär att ingen reklam, vinstindrivning eller venturekapital förekommer, någonsin. Dina donationer går direkt till att stöda utvecklingen av projektet. Stort tack till följande personer:',
|
||||
support_on_patreon: 'Stöd på Patreon',
|
||||
general_sponsors: 'Allmänna sponsorer är dem som givit mellan 10 och 39\u00a0dollar till Lemmy.',
|
||||
general_sponsors:
|
||||
'Allmänna sponsorer är dem som givit mellan 10 och 39\u00a0dollar till Lemmy.',
|
||||
crypto: 'Kryptovaluta',
|
||||
bitcoin: 'Bitcoin',
|
||||
ethereum: 'Ethereum',
|
||||
|
@ -151,7 +155,8 @@ export const sv = {
|
|||
yes: 'ja',
|
||||
no: 'nej',
|
||||
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.',
|
||||
community_ban: 'Du har blockerats från den här gemenskapen.',
|
||||
site_ban: 'Du har blockerats från webbplatsen.',
|
||||
|
@ -178,7 +183,8 @@ export const sv = {
|
|||
not_an_admin: 'Inte en administratör.',
|
||||
site_already_exists: 'Webbplatsen finns redan.',
|
||||
couldnt_update_site: 'Kunde inte uppdatera webbplats.',
|
||||
couldnt_find_that_username_or_email: 'Kunde inte hitta det användarnamnet eller e-postadressen.',
|
||||
couldnt_find_that_username_or_email:
|
||||
'Kunde inte hitta det användarnamnet eller e-postadressen.',
|
||||
password_incorrect: 'Ogiltigt lösenord.',
|
||||
passwords_dont_match: 'Lösenorden stämmer inte överens.',
|
||||
admin_already_created: 'Beklagar, men det finns redan en administratör.',
|
||||
|
@ -186,4 +192,4 @@ export const sv = {
|
|||
couldnt_update_user: 'Kunde inte uppdatera användare.',
|
||||
system_err_login: 'Systemfel. Försök att logga ut och sedan in igen.',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
12
ui/src/translations/zh.ts
vendored
12
ui/src/translations/zh.ts
vendored
|
@ -113,16 +113,19 @@ export const zh = {
|
|||
modified: '修改',
|
||||
sponsors: 'Sponsors',
|
||||
sponsors_of_lemmy: 'Sponsors of Lemmy',
|
||||
sponsor_message: 'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
|
||||
sponsor_message:
|
||||
'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
|
||||
support_on_patreon: 'Support on Patreon',
|
||||
general_sponsors:'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
||||
general_sponsors:
|
||||
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
|
||||
crypto: '加密',
|
||||
bitcoin: '比特币',
|
||||
ethereum: '以太币',
|
||||
code: '代码',
|
||||
joined: '已加入',
|
||||
powered_by: '保留所有权利',
|
||||
landing_0: 'Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It\'s self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
|
||||
landing_0:
|
||||
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
|
||||
not_logged_in: '未登录.',
|
||||
community_ban: '你被此节点禁止.',
|
||||
site_ban: '你被此站点禁止',
|
||||
|
@ -157,5 +160,4 @@ export const zh = {
|
|||
couldnt_update_user: '不可以更新用户.',
|
||||
system_err_login: '系统错误. 尝试注销再登录',
|
||||
},
|
||||
}
|
||||
|
||||
};
|
||||
|
|
90
ui/src/utils.ts
vendored
90
ui/src/utils.ts
vendored
|
@ -8,7 +8,14 @@ import 'moment/locale/ru';
|
|||
import 'moment/locale/nl';
|
||||
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 markdownitEmoji from 'markdown-it-emoji/light';
|
||||
import * as markdown_it_container from 'markdown-it-container';
|
||||
|
@ -22,7 +29,12 @@ export const postRefetchSeconds: number = 60*1000;
|
|||
export const fetchLimit: number = 20;
|
||||
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 {
|
||||
let opStr: string = msg.op;
|
||||
|
@ -32,8 +44,9 @@ export function msgOp(msg: any): UserOperation {
|
|||
export const md = new markdown_it({
|
||||
html: false,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
}).use(markdown_it_container, 'spoiler', {
|
||||
typographer: true,
|
||||
})
|
||||
.use(markdown_it_container, 'spoiler', {
|
||||
validate: function(params: any) {
|
||||
return params.trim().match(/^spoiler\s+(.*)$/);
|
||||
},
|
||||
|
@ -43,15 +56,17 @@ export const md = new markdown_it({
|
|||
|
||||
if (tokens[idx].nesting === 1) {
|
||||
// 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
|
||||
return '</details>\n';
|
||||
}
|
||||
}
|
||||
}).use(markdownitEmoji, {
|
||||
defs: objectFlip(emojiShortName)
|
||||
},
|
||||
})
|
||||
.use(markdownitEmoji, {
|
||||
defs: objectFlip(emojiShortName),
|
||||
});
|
||||
|
||||
md.renderer.rules.emoji = function(token, idx) {
|
||||
|
@ -65,7 +80,9 @@ export function hotRank(comment: Comment): number {
|
|||
let now: Date = new Date();
|
||||
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}`);
|
||||
|
||||
|
@ -80,11 +97,21 @@ export function getUnixTime(text: string): number {
|
|||
return text ? new Date(text).getTime() / 1000 : undefined;
|
||||
}
|
||||
|
||||
export function addTypeInfo<T>(arr: Array<T>, name: string): Array<{type_: string, data: T}> {
|
||||
return arr.map(e => {return {type_: name, data: e}});
|
||||
export function addTypeInfo<T>(
|
||||
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.
|
||||
if (user) {
|
||||
let yourIndex = modIds.findIndex(id => id == user.id);
|
||||
|
@ -104,8 +131,9 @@ export function isMod(modIds: Array<number>, creator_id: number): boolean {
|
|||
return modIds.includes(creator_id);
|
||||
}
|
||||
|
||||
|
||||
var imageRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:png|jpg|jpeg|gif|png|svg))`);
|
||||
var imageRegex = new RegExp(
|
||||
`(http)?s?:?(\/\/[^"']*\.(?:png|jpg|jpeg|gif|png|svg))`
|
||||
);
|
||||
var videoRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:mp4))`);
|
||||
|
||||
export function isImage(url: string) {
|
||||
|
@ -128,7 +156,6 @@ export function capitalizeFirstLetter(str: string): string {
|
|||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
|
||||
export function routeSortTypeToEnum(sort: string): SortType {
|
||||
if (sort == 'new') {
|
||||
return SortType.New;
|
||||
|
@ -140,6 +167,8 @@ export function routeSortTypeToEnum(sort: string): SortType {
|
|||
return SortType.TopWeek;
|
||||
} else if (sort == 'topmonth') {
|
||||
return SortType.TopMonth;
|
||||
} else if (sort == 'topyear') {
|
||||
return SortType.TopYear;
|
||||
} else if (sort == 'topall') {
|
||||
return SortType.TopAll;
|
||||
}
|
||||
|
@ -159,7 +188,11 @@ export async function getPageTitle(url: string) {
|
|||
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
|
||||
// The returned function will be able to reference this due to closure.
|
||||
// Each call to the returned function will share this common timer.
|
||||
|
@ -183,7 +216,6 @@ export function debounce(func: any, wait: number = 500, immediate: boolean = fal
|
|||
|
||||
// Set the new timeout
|
||||
timeout = setTimeout(function() {
|
||||
|
||||
// Inside the timeout function, clear the timeout variable
|
||||
// which will let the next execution run when in 'immediate' mode
|
||||
timeout = null;
|
||||
|
@ -199,16 +231,16 @@ export function debounce(func: any, wait: number = 500, immediate: boolean = fal
|
|||
|
||||
// Immediate mode and no wait timer? Execute the function..
|
||||
if (callNow) func.apply(context, args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getLanguage(): string {
|
||||
return (navigator.language || navigator.userLanguage);
|
||||
return navigator.language || navigator.userLanguage;
|
||||
}
|
||||
|
||||
export function objectFlip(obj: any) {
|
||||
const ret = {};
|
||||
Object.keys(obj).forEach((key) => {
|
||||
Object.keys(obj).forEach(key => {
|
||||
ret[obj[key]] = key;
|
||||
});
|
||||
return ret;
|
||||
|
@ -240,16 +272,24 @@ export function getMomentLanguage(): string {
|
|||
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') {
|
||||
for (var i = 0; i < themes.length; i++) {
|
||||
|
||||
let styleSheet = document.getElementById(themes[i]);
|
||||
if (themes[i] == theme) {
|
||||
styleSheet.removeAttribute("disabled");
|
||||
styleSheet.removeAttribute('disabled');
|
||||
} else {
|
||||
styleSheet.setAttribute("disabled", "disabled");
|
||||
styleSheet.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
2
ui/src/version.ts
vendored
2
ui/src/version.ts
vendored
|
@ -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
28
ui/tslint.json
vendored
|
@ -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
2087
ui/yarn.lock
vendored
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue