Merge branch 'master' into master

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

40
README.md vendored
View File

@ -58,7 +58,7 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins
## Why's it called Lemmy?
- 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.

View File

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

@ -210,6 +210,7 @@ Only the first user will be able to be the admin.
{
op: "DeleteAccount",
data: {
password: String,
auth: String
}
}

View File

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

View File

@ -0,0 +1,35 @@
create table user_mention (
id serial primary key,
recipient_id int references user_ on update cascade on delete cascade not null,
comment_id int references comment on update cascade on delete cascade not null,
read boolean default false not null,
published timestamp not null default now(),
unique(recipient_id, comment_id)
);
create view user_mention_view as
select
c.id,
um.id as user_mention_id,
c.creator_id,
c.post_id,
c.parent_id,
c.content,
c.removed,
um.read,
c.published,
c.updated,
c.deleted,
c.community_id,
c.banned,
c.banned_from_community,
c.creator_name,
c.score,
c.upvotes,
c.downvotes,
c.user_id,
c.my_vote,
c.saved,
um.recipient_id
from user_mention um, comment_view c
where um.comment_id = c.id;

View File

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

View File

@ -0,0 +1,2 @@
alter table user_ add column default_sort_type smallint default 0 not null;
alter table user_ add column default_listing_type smallint default 1 not null;

View File

@ -85,6 +85,35 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment"))?,
};
// 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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,345 @@
table! {
category (id) {
id -> Int4,
name -> Varchar,
}
}
table! {
comment (id) {
id -> Int4,
creator_id -> Int4,
post_id -> Int4,
parent_id -> Nullable<Int4>,
content -> Text,
removed -> Bool,
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
}
}
table! {
comment_like (id) {
id -> Int4,
user_id -> Int4,
comment_id -> Int4,
post_id -> Int4,
score -> Int2,
published -> Timestamp,
}
}
table! {
comment_saved (id) {
id -> Int4,
comment_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
community (id) {
id -> Int4,
name -> Varchar,
title -> Varchar,
description -> Nullable<Text>,
category_id -> Int4,
creator_id -> Int4,
removed -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
nsfw -> Bool,
}
}
table! {
community_follower (id) {
id -> Int4,
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
community_moderator (id) {
id -> Int4,
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
community_user_ban (id) {
id -> Int4,
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
mod_add (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_add_community (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
community_id -> Int4,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_ban (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
reason -> Nullable<Text>,
banned -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
}
}
table! {
mod_ban_from_community (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
community_id -> Int4,
reason -> Nullable<Text>,
banned -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
}
}
table! {
mod_lock_post (id) {
id -> Int4,
mod_user_id -> Int4,
post_id -> Int4,
locked -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_remove_comment (id) {
id -> Int4,
mod_user_id -> Int4,
comment_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_remove_community (id) {
id -> Int4,
mod_user_id -> Int4,
community_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
}
}
table! {
mod_remove_post (id) {
id -> Int4,
mod_user_id -> Int4,
post_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_sticky_post (id) {
id -> Int4,
mod_user_id -> Int4,
post_id -> Int4,
stickied -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
post (id) {
id -> Int4,
name -> Varchar,
url -> Nullable<Text>,
body -> Nullable<Text>,
creator_id -> Int4,
community_id -> Int4,
removed -> Bool,
locked -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
nsfw -> Bool,
stickied -> Bool,
}
}
table! {
post_like (id) {
id -> Int4,
post_id -> Int4,
user_id -> Int4,
score -> Int2,
published -> Timestamp,
}
}
table! {
post_read (id) {
id -> Int4,
post_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
post_saved (id) {
id -> Int4,
post_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
site (id) {
id -> Int4,
name -> Varchar,
description -> Nullable<Text>,
creator_id -> Int4,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
}
table! {
user_ (id) {
id -> Int4,
name -> Varchar,
fedi_name -> Varchar,
preferred_username -> Nullable<Varchar>,
password_encrypted -> Text,
email -> Nullable<Text>,
icon -> Nullable<Bytea>,
admin -> Bool,
banned -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
show_nsfw -> Bool,
theme -> Varchar,
}
}
table! {
user_ban (id) {
id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
user_mention (id) {
id -> Int4,
recipient_id -> Int4,
comment_id -> Int4,
read -> Bool,
published -> Timestamp,
}
}
joinable!(comment -> post (post_id));
joinable!(comment -> user_ (creator_id));
joinable!(comment_like -> comment (comment_id));
joinable!(comment_like -> post (post_id));
joinable!(comment_like -> user_ (user_id));
joinable!(comment_saved -> comment (comment_id));
joinable!(comment_saved -> user_ (user_id));
joinable!(community -> category (category_id));
joinable!(community -> user_ (creator_id));
joinable!(community_follower -> community (community_id));
joinable!(community_follower -> user_ (user_id));
joinable!(community_moderator -> community (community_id));
joinable!(community_moderator -> user_ (user_id));
joinable!(community_user_ban -> community (community_id));
joinable!(community_user_ban -> user_ (user_id));
joinable!(mod_add_community -> community (community_id));
joinable!(mod_ban_from_community -> community (community_id));
joinable!(mod_lock_post -> post (post_id));
joinable!(mod_lock_post -> user_ (mod_user_id));
joinable!(mod_remove_comment -> comment (comment_id));
joinable!(mod_remove_comment -> user_ (mod_user_id));
joinable!(mod_remove_community -> community (community_id));
joinable!(mod_remove_community -> user_ (mod_user_id));
joinable!(mod_remove_post -> post (post_id));
joinable!(mod_remove_post -> user_ (mod_user_id));
joinable!(mod_sticky_post -> post (post_id));
joinable!(mod_sticky_post -> user_ (mod_user_id));
joinable!(post -> community (community_id));
joinable!(post -> user_ (creator_id));
joinable!(post_like -> post (post_id));
joinable!(post_like -> user_ (user_id));
joinable!(post_read -> post (post_id));
joinable!(post_read -> user_ (user_id));
joinable!(post_saved -> post (post_id));
joinable!(post_saved -> user_ (user_id));
joinable!(site -> user_ (creator_id));
joinable!(user_ban -> user_ (user_id));
joinable!(user_mention -> comment (comment_id));
joinable!(user_mention -> user_ (recipient_id));
allow_tables_to_appear_in_same_query!(
category,
comment,
comment_like,
comment_saved,
community,
community_follower,
community_moderator,
community_user_ban,
mod_add,
mod_add_community,
mod_ban,
mod_ban_from_community,
mod_lock_post,
mod_remove_comment,
mod_remove_community,
mod_remove_post,
mod_sticky_post,
post,
post_like,
post_read,
post_saved,
site,
user_,
user_ban,
user_mention,
);

View File

@ -21,6 +21,8 @@ pub struct User_ {
pub updated: Option<chrono::NaiveDateTime>,
pub 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();

View File

@ -0,0 +1,173 @@
use super::comment::Comment;
use super::*;
use crate::schema::user_mention;
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[belongs_to(Comment)]
#[table_name = "user_mention"]
pub struct UserMention {
pub id: i32,
pub recipient_id: i32,
pub comment_id: i32,
pub read: bool,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name = "user_mention"]
pub struct UserMentionForm {
pub recipient_id: i32,
pub comment_id: i32,
pub read: Option<bool>,
}
impl Crud<UserMentionForm> for UserMention {
fn read(conn: &PgConnection, user_mention_id: i32) -> Result<Self, Error> {
use crate::schema::user_mention::dsl::*;
user_mention.find(user_mention_id).first::<Self>(conn)
}
fn delete(conn: &PgConnection, user_mention_id: i32) -> Result<usize, Error> {
use crate::schema::user_mention::dsl::*;
diesel::delete(user_mention.find(user_mention_id)).execute(conn)
}
fn create(conn: &PgConnection, user_mention_form: &UserMentionForm) -> Result<Self, Error> {
use crate::schema::user_mention::dsl::*;
insert_into(user_mention)
.values(user_mention_form)
.get_result::<Self>(conn)
}
fn update(
conn: &PgConnection,
user_mention_id: i32,
user_mention_form: &UserMentionForm,
) -> Result<Self, Error> {
use crate::schema::user_mention::dsl::*;
diesel::update(user_mention.find(user_mention_id))
.set(user_mention_form)
.get_result::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use super::super::comment::*;
use super::super::community::*;
use super::super::post::*;
use super::super::user::*;
use super::*;
#[test]
fn test_crud() {
let conn = establish_connection();
let new_user = UserForm {
name: "terrylake".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
admin: false,
banned: false,
updated: None,
show_nsfw: false,
theme: "darkly".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
let recipient_form = UserForm {
name: "terrylakes recipient".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
admin: false,
banned: false,
updated: None,
show_nsfw: false,
theme: "darkly".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
};
let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
let new_community = CommunityForm {
name: "test community lake".to_string(),
title: "nada".to_owned(),
description: None,
category_id: 1,
creator_id: inserted_user.id,
removed: None,
deleted: None,
updated: None,
nsfw: false,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let new_post = PostForm {
name: "A test post".into(),
creator_id: inserted_user.id,
url: None,
body: None,
community_id: inserted_community.id,
removed: None,
deleted: None,
locked: None,
stickied: None,
updated: None,
nsfw: false,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_user.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
read: None,
parent_id: None,
updated: None,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
let user_mention_form = UserMentionForm {
recipient_id: inserted_recipient.id,
comment_id: inserted_comment.id,
read: None,
};
let inserted_mention = UserMention::create(&conn, &user_mention_form).unwrap();
let expected_mention = UserMention {
id: inserted_mention.id,
recipient_id: inserted_mention.recipient_id,
comment_id: inserted_mention.comment_id,
read: false,
published: inserted_mention.published,
};
let read_mention = UserMention::read(&conn, inserted_mention.id).unwrap();
let updated_mention =
UserMention::update(&conn, inserted_mention.id, &user_mention_form).unwrap();
let num_deleted = UserMention::delete(&conn, inserted_mention.id).unwrap();
Comment::delete(&conn, inserted_comment.id).unwrap();
Post::delete(&conn, inserted_post.id).unwrap();
Community::delete(&conn, inserted_community.id).unwrap();
User_::delete(&conn, inserted_user.id).unwrap();
User_::delete(&conn, inserted_recipient.id).unwrap();
assert_eq!(expected_mention, read_mention);
assert_eq!(expected_mention, inserted_mention);
assert_eq!(expected_mention, updated_mention);
assert_eq!(1, num_deleted);
}
}

View File

@ -0,0 +1,117 @@
use super::*;
// The faked schema since diesel doesn't do views
table! {
user_mention_view (id) {
id -> Int4,
user_mention_id -> Int4,
creator_id -> Int4,
post_id -> Int4,
parent_id -> Nullable<Int4>,
content -> Text,
removed -> Bool,
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
community_id -> Int4,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
saved -> Nullable<Bool>,
recipient_id -> Int4,
}
}
#[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)]
#[table_name = "user_mention_view"]
pub struct UserMentionView {
pub id: i32,
pub user_mention_id: i32,
pub creator_id: i32,
pub post_id: i32,
pub parent_id: Option<i32>,
pub content: String,
pub removed: bool,
pub read: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub community_id: i32,
pub banned: bool,
pub banned_from_community: bool,
pub creator_name: String,
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
pub user_id: Option<i32>,
pub my_vote: Option<i32>,
pub saved: Option<bool>,
pub recipient_id: i32,
}
impl UserMentionView {
pub fn get_mentions(
conn: &PgConnection,
for_user_id: i32,
sort: &SortType,
unread_only: bool,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
use super::user_mention_view::user_mention_view::dsl::*;
let (limit, offset) = limit_and_offset(page, limit);
let mut query = user_mention_view.into_boxed();
query = query
.filter(user_id.eq(for_user_id))
.filter(recipient_id.eq(for_user_id));
if unread_only {
query = query.filter(read.eq(false));
}
query = match sort {
// SortType::Hot => query.order_by(hot_rank.desc()),
SortType::New => query.order_by(published.desc()),
SortType::TopAll => query.order_by(score.desc()),
SortType::TopYear => query
.filter(published.gt(now - 1.years()))
.order_by(score.desc()),
SortType::TopMonth => query
.filter(published.gt(now - 1.months()))
.order_by(score.desc()),
SortType::TopWeek => query
.filter(published.gt(now - 1.weeks()))
.order_by(score.desc()),
SortType::TopDay => query
.filter(published.gt(now - 1.days()))
.order_by(score.desc()),
_ => query.order_by(published.desc()),
};
query.limit(limit).offset(offset).load::<Self>(conn)
}
pub fn read(
conn: &PgConnection,
from_user_mention_id: i32,
from_recipient_id: i32,
) -> Result<Self, Error> {
use super::user_mention_view::user_mention_view::dsl::*;
user_mention_view
.filter(user_mention_id.eq(from_user_mention_id))
.filter(user_id.eq(from_recipient_id))
.first::<Self>(conn)
}
}

View File

@ -104,9 +104,23 @@ pub fn has_slurs(test: &str) -> bool {
SLUR_REGEX.is_match(test)
}
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();
}

View File

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

View File

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

@ -0,0 +1,57 @@
{
"root": true,
"env": {
"browser": true
},
"plugins": [
"jane",
"inferno"
],
"extends": [
"plugin:jane/recommended",
"plugin:jane/typescript",
"plugin:inferno/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json",
"warnOnUnsupportedTypeScriptVersion": false
},
"rules": {
"@typescript-eslint/camelcase": 0,
"@typescript-eslint/member-delimiter-style": 0,
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-this-alias": 0,
"@typescript-eslint/no-unused-vars": 0,
"@typescript-eslint/no-use-before-define": 0,
"@typescript-eslint/no-useless-constructor": 0,
"arrow-body-style": 0,
"curly": 0,
"eol-last": 0,
"eqeqeq": 0,
"func-style": 0,
"import/no-duplicates": 0,
"inferno/jsx-key": 0,
"inferno/jsx-no-target-blank": 0,
"inferno/jsx-props-class-name": 0,
"inferno/no-direct-mutation-state": 0,
"inferno/no-unknown-property": 0,
"max-statements": 0,
"new-cap": 0,
"no-console": 0,
"no-duplicate-imports": 0,
"no-extra-parens": 0,
"no-return-assign": 0,
"no-throw-literal": 0,
"no-trailing-spaces": 0,
"no-unused-expressions": 0,
"no-useless-constructor": 0,
"no-useless-escape": 0,
"no-var": 0,
"prefer-const": 0,
"prefer-rest-params": 0,
"quote-props": 0,
"unicorn/filename-case": 0
}
}

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

@ -0,0 +1,4 @@
module.exports = Object.assign(require('eslint-plugin-jane/prettier-ts'), {
arrowParens: 'avoid',
semi: true,
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

44
ui/package.json vendored
View File

@ -1,19 +1,16 @@
{
"name": "lemmy",
"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"
]
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,68 @@
import { Component, linkEvent } from 'inferno';
import { ListingType } from '../interfaces';
import { UserService } from '../services';
import { i18n } from '../i18next';
interface ListingTypeSelectProps {
type_: ListingType;
onChange?(val: ListingType): any;
}
interface ListingTypeSelectState {
type_: ListingType;
}
export class ListingTypeSelect extends Component<
ListingTypeSelectProps,
ListingTypeSelectState
> {
private emptyState: ListingTypeSelectState = {
type_: this.props.type_,
};
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
}
render() {
return (
<div class="btn-group btn-group-toggle">
<label
className={`btn btn-sm btn-secondary
${this.state.type_ == ListingType.Subscribed && 'active'}
${UserService.Instance.user == undefined ? 'disabled' : 'pointer'}
`}
>
<input
type="radio"
value={ListingType.Subscribed}
checked={this.state.type_ == ListingType.Subscribed}
onChange={linkEvent(this, this.handleTypeChange)}
disabled={UserService.Instance.user == undefined}
/>
{i18n.t('subscribed')}
</label>
<label
className={`pointer btn btn-sm btn-secondary ${this.state.type_ ==
ListingType.All && 'active'}`}
>
<input
type="radio"
value={ListingType.All}
checked={this.state.type_ == ListingType.All}
onChange={linkEvent(this, this.handleTypeChange)}
/>
{i18n.t('all')}
</label>
</div>
);
}
handleTypeChange(i: ListingTypeSelect, event: any) {
i.state.type_ = Number(event.target.value);
i.setState(i.state);
i.props.onChange(i.state.type_);
}
}

View File

@ -1,7 +1,12 @@
import { Component, linkEvent } from 'inferno';
import { 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> {
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,69 @@
import { Component, linkEvent } from 'inferno';
import { SortType } from '../interfaces';
import { T } from 'inferno-i18next';
interface SortSelectProps {
sort: SortType;
onChange?(val: SortType): any;
hideHot?: boolean;
}
interface SortSelectState {
sort: SortType;
}
export class SortSelect extends Component<SortSelectProps, SortSelectState> {
private emptyState: SortSelectState = {
sort: this.props.sort,
};
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
}
render() {
return (
<select
value={this.state.sort}
onChange={linkEvent(this, this.handleSortChange)}
class="custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="sort_type">#</T>
</option>
{!this.props.hideHot && (
<option value={SortType.Hot}>
<T i18nKey="hot">#</T>
</option>
)}
<option value={SortType.New}>
<T i18nKey="new">#</T>
</option>
<option disabled></option>
<option value={SortType.TopDay}>
<T i18nKey="top_day">#</T>
</option>
<option value={SortType.TopWeek}>
<T i18nKey="week">#</T>
</option>
<option value={SortType.TopMonth}>
<T i18nKey="month">#</T>
</option>
<option value={SortType.TopYear}>
<T i18nKey="year">#</T>
</option>
<option value={SortType.TopAll}>
<T i18nKey="all">#</T>
</option>
</select>
);
}
handleSortChange(i: SortSelect, event: any) {
i.state.sort = Number(event.target.value);
i.setState(i.state);
i.props.onChange(i.state.sort);
}
}

View File

@ -3,24 +3,21 @@ import { WebSocketService } from '../services';
import { i18n } from '../i18next';
import { 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>
)
);
}
}

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

28
ui/tslint.json vendored
View File

@ -1,28 +0,0 @@
{
"extends": "tslint:recommended",
"rules": {
"forin": false,
"indent": [ true, "spaces" ],
"interface-name": false,
"ban-types": true,
"max-classes-per-file": true,
"max-line-length": false,
"member-access": true,
"member-ordering": false,
"no-bitwise": false,
"no-conditional-assignment": false,
"no-debugger": false,
"no-empty": true,
"no-namespace": false,
"no-unused-expression": true,
"object-literal-sort-keys": true,
"one-variable-per-declaration": [true, "ignore-for-loop"],
"only-arrow-functions": [false],
"ordered-imports": true,
"prefer-const": true,
"prefer-for-of": false,
"quotemark": [ true, "single", "jsx-double" ],
"trailing-comma": [true, {"multiline": "never", "singleline": "never"}],
"variable-name": false
}
}

2087
ui/yarn.lock vendored

File diff suppressed because it is too large Load Diff