Merge branch 'main' into captcha_setup
This commit is contained in:
commit
9509529f69
34 changed files with 4560 additions and 2072 deletions
5
docs/src/contributing_websocket_http_api.md
vendored
5
docs/src/contributing_websocket_http_api.md
vendored
|
@ -970,6 +970,10 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: "GetSite"
|
op: "GetSite"
|
||||||
|
data: {
|
||||||
|
auth: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
##### Response
|
##### Response
|
||||||
|
@ -982,6 +986,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
||||||
banned: Vec<UserView>,
|
banned: Vec<UserView>,
|
||||||
online: usize, // This is currently broken
|
online: usize, // This is currently broken
|
||||||
version: String,
|
version: String,
|
||||||
|
my_user: Option<User_>, // Gives back your user and settings if logged in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -6,8 +6,9 @@ use crate::{
|
||||||
};
|
};
|
||||||
use bcrypt::{hash, DEFAULT_COST};
|
use bcrypt::{hash, DEFAULT_COST};
|
||||||
use diesel::{dsl::*, result::Error, *};
|
use diesel::{dsl::*, result::Error, *};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug)]
|
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
#[table_name = "user_"]
|
#[table_name = "user_"]
|
||||||
pub struct User_ {
|
pub struct User_ {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
|
|
@ -56,14 +56,14 @@ pub struct UserView {
|
||||||
pub actor_id: String,
|
pub actor_id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>, // TODO this shouldn't be in this view
|
||||||
pub matrix_user_id: Option<String>,
|
pub matrix_user_id: Option<String>,
|
||||||
pub bio: Option<String>,
|
pub bio: Option<String>,
|
||||||
pub local: bool,
|
pub local: bool,
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub show_avatars: bool,
|
pub show_avatars: bool, // TODO this is a setting, probably doesn't need to be here
|
||||||
pub send_notifications_to_email: bool,
|
pub send_notifications_to_email: bool, // TODO also never used
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub number_of_posts: i64,
|
pub number_of_posts: i64,
|
||||||
pub post_score: i64,
|
pub post_score: i64,
|
||||||
|
|
|
@ -9,15 +9,7 @@ type Jwt = String;
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub username: String,
|
|
||||||
pub iss: String,
|
pub iss: String,
|
||||||
pub show_nsfw: bool,
|
|
||||||
pub theme: String,
|
|
||||||
pub default_sort_type: i16,
|
|
||||||
pub default_listing_type: i16,
|
|
||||||
pub lang: String,
|
|
||||||
pub avatar: Option<String>,
|
|
||||||
pub show_avatars: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Claims {
|
impl Claims {
|
||||||
|
@ -36,15 +28,7 @@ impl Claims {
|
||||||
pub fn jwt(user: User_, hostname: String) -> Jwt {
|
pub fn jwt(user: User_, hostname: String) -> Jwt {
|
||||||
let my_claims = Claims {
|
let my_claims = Claims {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.name.to_owned(),
|
|
||||||
iss: hostname,
|
iss: hostname,
|
||||||
show_nsfw: user.show_nsfw,
|
|
||||||
theme: user.theme.to_owned(),
|
|
||||||
default_sort_type: user.default_sort_type,
|
|
||||||
default_listing_type: user.default_listing_type,
|
|
||||||
lang: user.lang.to_owned(),
|
|
||||||
avatar: user.avatar.to_owned(),
|
|
||||||
show_avatars: user.show_avatars.to_owned(),
|
|
||||||
};
|
};
|
||||||
encode(
|
encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
|
|
|
@ -591,21 +591,26 @@ impl Perform for Oper<ListCommunities> {
|
||||||
) -> Result<ListCommunitiesResponse, LemmyError> {
|
) -> Result<ListCommunitiesResponse, LemmyError> {
|
||||||
let data: &ListCommunities = &self.data;
|
let data: &ListCommunities = &self.data;
|
||||||
|
|
||||||
let user_claims: Option<Claims> = match &data.auth {
|
// For logged in users, you need to get back subscribed, and settings
|
||||||
|
let user: Option<User_> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
Ok(claims) => Some(claims.claims),
|
Ok(claims) => {
|
||||||
|
let user_id = claims.claims.id;
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
Some(user)
|
||||||
|
}
|
||||||
Err(_e) => None,
|
Err(_e) => None,
|
||||||
},
|
},
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = match &user_claims {
|
let user_id = match &user {
|
||||||
Some(claims) => Some(claims.id),
|
Some(user) => Some(user.id),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let show_nsfw = match &user_claims {
|
let show_nsfw = match &user {
|
||||||
Some(claims) => claims.show_nsfw,
|
Some(user) => user.show_nsfw,
|
||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -370,21 +370,26 @@ impl Perform for Oper<GetPosts> {
|
||||||
) -> Result<GetPostsResponse, LemmyError> {
|
) -> Result<GetPostsResponse, LemmyError> {
|
||||||
let data: &GetPosts = &self.data;
|
let data: &GetPosts = &self.data;
|
||||||
|
|
||||||
let user_claims: Option<Claims> = match &data.auth {
|
// For logged in users, you need to get back subscribed, and settings
|
||||||
|
let user: Option<User_> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
Ok(claims) => Some(claims.claims),
|
Ok(claims) => {
|
||||||
|
let user_id = claims.claims.id;
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
Some(user)
|
||||||
|
}
|
||||||
Err(_e) => None,
|
Err(_e) => None,
|
||||||
},
|
},
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = match &user_claims {
|
let user_id = match &user {
|
||||||
Some(claims) => Some(claims.id),
|
Some(user) => Some(user.id),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let show_nsfw = match &user_claims {
|
let show_nsfw = match &user {
|
||||||
Some(claims) => claims.show_nsfw,
|
Some(user) => user.show_nsfw,
|
||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ use lemmy_db::{
|
||||||
post_view::*,
|
post_view::*,
|
||||||
site::*,
|
site::*,
|
||||||
site_view::*,
|
site_view::*,
|
||||||
|
user::*,
|
||||||
user_view::*,
|
user_view::*,
|
||||||
Crud,
|
Crud,
|
||||||
SearchType,
|
SearchType,
|
||||||
|
@ -98,7 +99,9 @@ pub struct EditSite {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GetSite {}
|
pub struct GetSite {
|
||||||
|
auth: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct SiteResponse {
|
pub struct SiteResponse {
|
||||||
|
@ -112,6 +115,7 @@ pub struct GetSiteResponse {
|
||||||
banned: Vec<UserView>,
|
banned: Vec<UserView>,
|
||||||
pub online: usize,
|
pub online: usize,
|
||||||
version: String,
|
version: String,
|
||||||
|
my_user: Option<User_>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -352,7 +356,7 @@ impl Perform for Oper<GetSite> {
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
websocket_info: Option<WebsocketInfo>,
|
websocket_info: Option<WebsocketInfo>,
|
||||||
) -> Result<GetSiteResponse, LemmyError> {
|
) -> Result<GetSiteResponse, LemmyError> {
|
||||||
let _data: &GetSite = &self.data;
|
let data: &GetSite = &self.data;
|
||||||
|
|
||||||
// TODO refactor this a little
|
// TODO refactor this a little
|
||||||
let res = blocking(pool, move |conn| Site::read(conn, 1)).await?;
|
let res = blocking(pool, move |conn| Site::read(conn, 1)).await?;
|
||||||
|
@ -417,12 +421,29 @@ impl Perform for Oper<GetSite> {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Giving back your user, if you're logged in
|
||||||
|
let my_user: Option<User_> = match &data.auth {
|
||||||
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
|
Ok(claims) => {
|
||||||
|
let user_id = claims.claims.id;
|
||||||
|
let mut user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
user.password_encrypted = "".to_string();
|
||||||
|
user.private_key = None;
|
||||||
|
user.public_key = None;
|
||||||
|
Some(user)
|
||||||
|
}
|
||||||
|
Err(_e) => None,
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
Ok(GetSiteResponse {
|
Ok(GetSiteResponse {
|
||||||
site: site_view,
|
site: site_view,
|
||||||
admins,
|
admins,
|
||||||
banned,
|
banned,
|
||||||
online,
|
online,
|
||||||
version: version::VERSION.to_string(),
|
version: version::VERSION.to_string(),
|
||||||
|
my_user,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -616,6 +637,11 @@ impl Perform for Oper<TransferSite> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
let mut user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
// TODO add a User_::read_safe() for this.
|
||||||
|
user.password_encrypted = "".to_string();
|
||||||
|
user.private_key = None;
|
||||||
|
user.public_key = None;
|
||||||
|
|
||||||
let read_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
|
let read_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
|
||||||
|
|
||||||
|
@ -666,6 +692,7 @@ impl Perform for Oper<TransferSite> {
|
||||||
banned,
|
banned,
|
||||||
online: 0,
|
online: 0,
|
||||||
version: version::VERSION.to_string(),
|
version: version::VERSION.to_string(),
|
||||||
|
my_user: Some(user),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -651,21 +651,26 @@ impl Perform for Oper<GetUserDetails> {
|
||||||
) -> Result<GetUserDetailsResponse, LemmyError> {
|
) -> Result<GetUserDetailsResponse, LemmyError> {
|
||||||
let data: &GetUserDetails = &self.data;
|
let data: &GetUserDetails = &self.data;
|
||||||
|
|
||||||
let user_claims: Option<Claims> = match &data.auth {
|
// For logged in users, you need to get back subscribed, and settings
|
||||||
|
let user: Option<User_> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
Ok(claims) => Some(claims.claims),
|
Ok(claims) => {
|
||||||
|
let user_id = claims.claims.id;
|
||||||
|
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||||
|
Some(user)
|
||||||
|
}
|
||||||
Err(_e) => None,
|
Err(_e) => None,
|
||||||
},
|
},
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = match &user_claims {
|
let user_id = match &user {
|
||||||
Some(claims) => Some(claims.id),
|
Some(user) => Some(user.id),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let show_nsfw = match &user_claims {
|
let show_nsfw = match &user {
|
||||||
Some(claims) => claims.show_nsfw,
|
Some(user) => user.show_nsfw,
|
||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1278,11 +1283,11 @@ impl Perform for Oper<CreatePrivateMessage> {
|
||||||
let subject = &format!(
|
let subject = &format!(
|
||||||
"{} - Private Message from {}",
|
"{} - Private Message from {}",
|
||||||
Settings::get().hostname,
|
Settings::get().hostname,
|
||||||
claims.username
|
user.name,
|
||||||
);
|
);
|
||||||
let html = &format!(
|
let html = &format!(
|
||||||
"<h1>Private Message</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
"<h1>Private Message</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||||
claims.username, &content_slurs_removed, hostname
|
user.name, &content_slurs_removed, hostname
|
||||||
);
|
);
|
||||||
match send_email(subject, &email, &recipient_user.name, html) {
|
match send_email(subject, &email, &recipient_user.name, html) {
|
||||||
Ok(_o) => _o,
|
Ok(_o) => _o,
|
||||||
|
|
|
@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||||
pub struct PageExtension {
|
pub struct PageExtension {
|
||||||
pub comments_enabled: bool,
|
pub comments_enabled: bool,
|
||||||
pub sensitive: bool,
|
pub sensitive: bool,
|
||||||
|
pub stickied: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<U> UnparsedExtension<U> for PageExtension
|
impl<U> UnparsedExtension<U> for PageExtension
|
||||||
|
@ -19,12 +20,14 @@ where
|
||||||
Ok(PageExtension {
|
Ok(PageExtension {
|
||||||
comments_enabled: unparsed_mut.remove("commentsEnabled")?,
|
comments_enabled: unparsed_mut.remove("commentsEnabled")?,
|
||||||
sensitive: unparsed_mut.remove("sensitive")?,
|
sensitive: unparsed_mut.remove("sensitive")?,
|
||||||
|
stickied: unparsed_mut.remove("stickied")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
|
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
|
||||||
unparsed_mut.insert("commentsEnabled", self.comments_enabled)?;
|
unparsed_mut.insert("commentsEnabled", self.comments_enabled)?;
|
||||||
unparsed_mut.insert("sensitive", self.sensitive)?;
|
unparsed_mut.insert("sensitive", self.sensitive)?;
|
||||||
|
unparsed_mut.insert("stickied", self.stickied)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,6 +133,7 @@ impl ToApub for Post {
|
||||||
let ext = PageExtension {
|
let ext = PageExtension {
|
||||||
comments_enabled: !self.locked,
|
comments_enabled: !self.locked,
|
||||||
sensitive: self.nsfw,
|
sensitive: self.nsfw,
|
||||||
|
stickied: self.stickied,
|
||||||
};
|
};
|
||||||
Ok(Ext1::new(page, ext))
|
Ok(Ext1::new(page, ext))
|
||||||
}
|
}
|
||||||
|
@ -244,7 +245,7 @@ impl FromApub for PostForm {
|
||||||
.map(|u| u.to_owned().naive_local()),
|
.map(|u| u.to_owned().naive_local()),
|
||||||
deleted: None,
|
deleted: None,
|
||||||
nsfw: ext.sensitive,
|
nsfw: ext.sensitive,
|
||||||
stickied: None, // -> put it in "featured" collection of the community
|
stickied: Some(ext.stickied),
|
||||||
embed_title,
|
embed_title,
|
||||||
embed_description,
|
embed_description,
|
||||||
embed_html,
|
embed_html,
|
||||||
|
|
21
ui/package.json
vendored
21
ui/package.json
vendored
|
@ -16,11 +16,13 @@
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/autosize": "^3.0.6",
|
"@types/autosize": "^3.0.6",
|
||||||
|
"@types/jest": "^26.0.7",
|
||||||
"@types/js-cookie": "^2.2.6",
|
"@types/js-cookie": "^2.2.6",
|
||||||
"@types/jwt-decode": "^2.2.1",
|
"@types/jwt-decode": "^2.2.1",
|
||||||
"@types/markdown-it": "^0.0.9",
|
"@types/markdown-it": "^10.0.1",
|
||||||
"@types/markdown-it-container": "^2.0.2",
|
"@types/markdown-it-container": "^2.0.2",
|
||||||
"@types/node": "^13.11.1",
|
"@types/node": "^14.0.26",
|
||||||
|
"@types/node-fetch": "^2.5.6",
|
||||||
"autosize": "^4.0.2",
|
"autosize": "^4.0.2",
|
||||||
"bootswatch": "^4.3.1",
|
"bootswatch": "^4.3.1",
|
||||||
"choices.js": "^9.0.1",
|
"choices.js": "^9.0.1",
|
||||||
|
@ -30,12 +32,13 @@
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.2.5",
|
||||||
"i18next": "^19.4.1",
|
"i18next": "^19.4.1",
|
||||||
"inferno": "^7.4.2",
|
"inferno": "^7.4.2",
|
||||||
|
"inferno-helmet": "^5.2.1",
|
||||||
"inferno-i18next": "nimbusec-oss/inferno-i18next",
|
"inferno-i18next": "nimbusec-oss/inferno-i18next",
|
||||||
"inferno-router": "^7.4.2",
|
"inferno-router": "^7.4.2",
|
||||||
"js-cookie": "^2.2.0",
|
"js-cookie": "^2.2.0",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"markdown-it": "^10.0.0",
|
"markdown-it": "^11.0.0",
|
||||||
"markdown-it-container": "^2.0.0",
|
"markdown-it-container": "^3.0.0",
|
||||||
"markdown-it-emoji": "^1.4.0",
|
"markdown-it-emoji": "^1.4.0",
|
||||||
"markdown-it-sub": "^1.0.0",
|
"markdown-it-sub": "^1.0.0",
|
||||||
"markdown-it-sup": "^1.0.0",
|
"markdown-it-sup": "^1.0.0",
|
||||||
|
@ -51,16 +54,14 @@
|
||||||
"ws": "^7.2.3"
|
"ws": "^7.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^25.2.1",
|
"eslint": "^7.5.0",
|
||||||
"@types/node-fetch": "^2.5.6",
|
|
||||||
"eslint": "^6.5.1",
|
|
||||||
"eslint-plugin-inferno": "^7.14.3",
|
"eslint-plugin-inferno": "^7.14.3",
|
||||||
"eslint-plugin-jane": "^7.2.1",
|
"eslint-plugin-jane": "^8.0.4",
|
||||||
"fuse-box": "^3.1.3",
|
"fuse-box": "^3.1.3",
|
||||||
"jest": "^25.4.0",
|
"jest": "^26.0.7",
|
||||||
"lint-staged": "^10.1.3",
|
"lint-staged": "^10.1.3",
|
||||||
"sortpack": "^2.1.4",
|
"sortpack": "^2.1.4",
|
||||||
"ts-jest": "^25.4.0",
|
"ts-jest": "^26.1.3",
|
||||||
"ts-node": "^8.8.2",
|
"ts-node": "^8.8.2",
|
||||||
"ts-transform-classcat": "^1.0.0",
|
"ts-transform-classcat": "^1.0.0",
|
||||||
"ts-transform-inferno": "^4.0.3",
|
"ts-transform-inferno": "^4.0.3",
|
||||||
|
|
94
ui/src/api_tests/api.spec.ts
vendored
94
ui/src/api_tests/api.spec.ts
vendored
|
@ -6,7 +6,8 @@ import {
|
||||||
PostForm,
|
PostForm,
|
||||||
DeletePostForm,
|
DeletePostForm,
|
||||||
RemovePostForm,
|
RemovePostForm,
|
||||||
// TODO need to test LockPost and StickyPost federated
|
StickyPostForm,
|
||||||
|
LockPostForm,
|
||||||
PostResponse,
|
PostResponse,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
FollowCommunityForm,
|
FollowCommunityForm,
|
||||||
|
@ -345,6 +346,27 @@ describe('main', () => {
|
||||||
expect(updateResponse.post.community_local).toBe(false);
|
expect(updateResponse.post.community_local).toBe(false);
|
||||||
expect(updateResponse.post.creator_local).toBe(true);
|
expect(updateResponse.post.creator_local).toBe(true);
|
||||||
|
|
||||||
|
let stickyPostForm: StickyPostForm = {
|
||||||
|
edit_id: 2,
|
||||||
|
stickied: true,
|
||||||
|
auth: lemmyAlphaAuth,
|
||||||
|
};
|
||||||
|
|
||||||
|
let stickyRes: PostResponse = await fetch(
|
||||||
|
`${lemmyAlphaApiUrl}/post/sticky`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: wrapper(stickyPostForm),
|
||||||
|
}
|
||||||
|
).then(d => d.json());
|
||||||
|
|
||||||
|
expect(stickyRes.post.name).toBe(name);
|
||||||
|
expect(stickyRes.post.stickied).toBe(true);
|
||||||
|
|
||||||
|
// Fetch from B
|
||||||
let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
|
let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
|
||||||
let getPostRes: GetPostResponse = await fetch(getPostUrl, {
|
let getPostRes: GetPostResponse = await fetch(getPostUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
@ -353,6 +375,76 @@ describe('main', () => {
|
||||||
expect(getPostRes.post.name).toBe(name);
|
expect(getPostRes.post.name).toBe(name);
|
||||||
expect(getPostRes.post.community_local).toBe(true);
|
expect(getPostRes.post.community_local).toBe(true);
|
||||||
expect(getPostRes.post.creator_local).toBe(false);
|
expect(getPostRes.post.creator_local).toBe(false);
|
||||||
|
expect(getPostRes.post.stickied).toBe(true);
|
||||||
|
|
||||||
|
let lockPostForm: LockPostForm = {
|
||||||
|
edit_id: 2,
|
||||||
|
locked: true,
|
||||||
|
auth: lemmyAlphaAuth,
|
||||||
|
};
|
||||||
|
|
||||||
|
let lockedRes: PostResponse = await fetch(
|
||||||
|
`${lemmyAlphaApiUrl}/post/lock`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: wrapper(lockPostForm),
|
||||||
|
}
|
||||||
|
).then(d => d.json());
|
||||||
|
|
||||||
|
expect(lockedRes.post.name).toBe(name);
|
||||||
|
expect(lockedRes.post.locked).toBe(true);
|
||||||
|
|
||||||
|
// Fetch from B to make sure its locked
|
||||||
|
getPostRes = await fetch(getPostUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
}).then(d => d.json());
|
||||||
|
expect(getPostRes.post.locked).toBe(true);
|
||||||
|
|
||||||
|
// Create a test comment on a locked post, it should be undefined
|
||||||
|
// since it shouldn't get created.
|
||||||
|
let content = 'A rejected comment on a locked post';
|
||||||
|
let commentForm: CommentForm = {
|
||||||
|
content,
|
||||||
|
post_id: 2,
|
||||||
|
auth: lemmyAlphaAuth,
|
||||||
|
};
|
||||||
|
|
||||||
|
let createResponse: CommentResponse = await fetch(
|
||||||
|
`${lemmyAlphaApiUrl}/comment`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: wrapper(commentForm),
|
||||||
|
}
|
||||||
|
).then(d => d.json());
|
||||||
|
|
||||||
|
expect(createResponse['error']).toBe('locked');
|
||||||
|
|
||||||
|
// Unlock the post for later actions
|
||||||
|
let unlockPostForm: LockPostForm = {
|
||||||
|
edit_id: 2,
|
||||||
|
locked: false,
|
||||||
|
auth: lemmyAlphaAuth,
|
||||||
|
};
|
||||||
|
|
||||||
|
let unlockedRes: PostResponse = await fetch(
|
||||||
|
`${lemmyAlphaApiUrl}/post/lock`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: wrapper(unlockPostForm),
|
||||||
|
}
|
||||||
|
).then(d => d.json());
|
||||||
|
|
||||||
|
expect(unlockedRes.post.name).toBe(name);
|
||||||
|
expect(unlockedRes.post.locked).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
17
ui/src/components/admin-settings.tsx
vendored
17
ui/src/components/admin-settings.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -80,9 +81,18 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.siteRes.site.name) {
|
||||||
|
return `${i18n.t('admin_settings')} - ${this.state.siteRes.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -92,7 +102,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
) : (
|
) : (
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<SiteForm site={this.state.siteRes.site} />
|
{this.state.siteRes.site.id && (
|
||||||
|
<SiteForm site={this.state.siteRes.site} />
|
||||||
|
)}
|
||||||
{this.admins()}
|
{this.admins()}
|
||||||
{this.bannedUsers()}
|
{this.bannedUsers()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -220,9 +232,6 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
}
|
}
|
||||||
this.state.siteRes = data;
|
this.state.siteRes = data;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `${i18n.t('admin_settings')} - ${
|
|
||||||
this.state.siteRes.site.name
|
|
||||||
}`;
|
|
||||||
} else if (res.op == UserOperation.EditSite) {
|
} else if (res.op == UserOperation.EditSite) {
|
||||||
let data = res.data as SiteResponse;
|
let data = res.data as SiteResponse;
|
||||||
this.state.siteRes.site = data.site;
|
this.state.siteRes.site = data.site;
|
||||||
|
|
16
ui/src/components/communities.tsx
vendored
16
ui/src/components/communities.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -11,6 +12,7 @@ import {
|
||||||
SortType,
|
SortType,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { wsJsonToRes, toast, getPageFromProps } from '../utils';
|
import { wsJsonToRes, toast, getPageFromProps } from '../utils';
|
||||||
|
@ -25,6 +27,7 @@ interface CommunitiesState {
|
||||||
communities: Array<Community>;
|
communities: Array<Community>;
|
||||||
page: number;
|
page: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
site: Site;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommunitiesProps {
|
interface CommunitiesProps {
|
||||||
|
@ -37,6 +40,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
communities: [],
|
communities: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
page: getPageFromProps(this.props),
|
page: getPageFromProps(this.props),
|
||||||
|
site: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -71,9 +75,18 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site) {
|
||||||
|
return `${i18n.t('communities')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5 class="">
|
<h5 class="">
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -240,7 +253,8 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
document.title = `${i18n.t('communities')} - ${data.site.name}`;
|
this.state.site = data.site;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
11
ui/src/components/community.tsx
vendored
11
ui/src/components/community.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -174,9 +175,18 @@ export class Community extends Component<any, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.community.name) {
|
||||||
|
return `/c/${this.state.community.name} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
{this.selects()}
|
{this.selects()}
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
|
@ -356,7 +366,6 @@ export class Community extends Component<any, State> {
|
||||||
this.state.community = data.community;
|
this.state.community = data.community;
|
||||||
this.state.moderators = data.moderators;
|
this.state.moderators = data.moderators;
|
||||||
this.state.online = data.online;
|
this.state.online = data.online;
|
||||||
document.title = `/c/${this.state.community.name} - ${this.state.site.name}`;
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
} else if (
|
} else if (
|
||||||
|
|
33
ui/src/components/create-community.tsx
vendored
33
ui/src/components/create-community.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component } from 'inferno';
|
import { Component } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { CommunityForm } from './community-form';
|
import { CommunityForm } from './community-form';
|
||||||
|
@ -7,19 +8,33 @@ import {
|
||||||
UserOperation,
|
UserOperation,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { toast, wsJsonToRes } from '../utils';
|
import { toast, wsJsonToRes } from '../utils';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
interface CreateCommunityState {
|
interface CreateCommunityState {
|
||||||
enableNsfw: boolean;
|
site: Site;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateCommunity extends Component<any, CreateCommunityState> {
|
export class CreateCommunity extends Component<any, CreateCommunityState> {
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
private emptyState: CreateCommunityState = {
|
private emptyState: CreateCommunityState = {
|
||||||
enableNsfw: null,
|
site: {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
creator_id: undefined,
|
||||||
|
published: undefined,
|
||||||
|
creator_name: undefined,
|
||||||
|
number_of_users: undefined,
|
||||||
|
number_of_posts: undefined,
|
||||||
|
number_of_comments: undefined,
|
||||||
|
number_of_communities: undefined,
|
||||||
|
enable_downvotes: undefined,
|
||||||
|
open_registration: undefined,
|
||||||
|
enable_nsfw: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -46,15 +61,24 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site.name) {
|
||||||
|
return `${i18n.t('create_community')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
<h5>{i18n.t('create_community')}</h5>
|
<h5>{i18n.t('create_community')}</h5>
|
||||||
<CommunityForm
|
<CommunityForm
|
||||||
onCreate={this.handleCommunityCreate}
|
onCreate={this.handleCommunityCreate}
|
||||||
enableNsfw={this.state.enableNsfw}
|
enableNsfw={this.state.site.enable_nsfw}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -74,9 +98,8 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
|
||||||
return;
|
return;
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
this.state.enableNsfw = data.site.enable_nsfw;
|
this.state.site = data.site;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `${i18n.t('create_community')} - ${data.site.name}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
ui/src/components/create-post.tsx
vendored
13
ui/src/components/create-post.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component } from 'inferno';
|
import { Component } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { PostForm } from './post-form';
|
import { PostForm } from './post-form';
|
||||||
|
@ -61,9 +62,18 @@ export class CreatePost extends Component<any, CreatePostState> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site.name) {
|
||||||
|
return `${i18n.t('create_post')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
<h5>{i18n.t('create_post')}</h5>
|
<h5>{i18n.t('create_post')}</h5>
|
||||||
|
@ -100,7 +110,7 @@ export class CreatePost extends Component<any, CreatePostState> {
|
||||||
return lastLocation.split('/c/')[1];
|
return lastLocation.split('/c/')[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePostCreate(id: number) {
|
handlePostCreate(id: number) {
|
||||||
|
@ -117,7 +127,6 @@ export class CreatePost extends Component<any, CreatePostState> {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
this.state.site = data.site;
|
this.state.site = data.site;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `${i18n.t('create_post')} - ${data.site.name}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
29
ui/src/components/create-private-message.tsx
vendored
29
ui/src/components/create-private-message.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component } from 'inferno';
|
import { Component } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { PrivateMessageForm } from './private-message-form';
|
import { PrivateMessageForm } from './private-message-form';
|
||||||
|
@ -7,15 +8,27 @@ import {
|
||||||
UserOperation,
|
UserOperation,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
PrivateMessageFormParams,
|
PrivateMessageFormParams,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { toast, wsJsonToRes } from '../utils';
|
import { toast, wsJsonToRes } from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
export class CreatePrivateMessage extends Component<any, any> {
|
interface CreatePrivateMessageState {
|
||||||
|
site: Site;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreatePrivateMessage extends Component<
|
||||||
|
any,
|
||||||
|
CreatePrivateMessageState
|
||||||
|
> {
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
|
private emptyState: CreatePrivateMessageState = {
|
||||||
|
site: undefined,
|
||||||
|
};
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
this.state = this.emptyState;
|
||||||
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
|
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
|
@ -40,9 +53,18 @@ export class CreatePrivateMessage extends Component<any, any> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site) {
|
||||||
|
return `${i18n.t('create_private_message')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
<h5>{i18n.t('create_private_message')}</h5>
|
<h5>{i18n.t('create_private_message')}</h5>
|
||||||
|
@ -80,9 +102,8 @@ export class CreatePrivateMessage extends Component<any, any> {
|
||||||
return;
|
return;
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
document.title = `${i18n.t('create_private_message')} - ${
|
this.state.site = data.site;
|
||||||
data.site.name
|
this.setState(this.state);
|
||||||
}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
46
ui/src/components/inbox.tsx
vendored
46
ui/src/components/inbox.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -17,6 +18,7 @@ import {
|
||||||
PrivateMessagesResponse,
|
PrivateMessagesResponse,
|
||||||
PrivateMessageResponse,
|
PrivateMessageResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import {
|
import {
|
||||||
|
@ -57,7 +59,7 @@ interface InboxState {
|
||||||
messages: Array<PrivateMessageI>;
|
messages: Array<PrivateMessageI>;
|
||||||
sort: SortType;
|
sort: SortType;
|
||||||
page: number;
|
page: number;
|
||||||
enableDownvotes: boolean;
|
site: Site;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Inbox extends Component<any, InboxState> {
|
export class Inbox extends Component<any, InboxState> {
|
||||||
|
@ -70,7 +72,20 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
messages: [],
|
messages: [],
|
||||||
sort: SortType.New,
|
sort: SortType.New,
|
||||||
page: 1,
|
page: 1,
|
||||||
enableDownvotes: undefined,
|
site: {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
creator_id: undefined,
|
||||||
|
published: undefined,
|
||||||
|
creator_name: undefined,
|
||||||
|
number_of_users: undefined,
|
||||||
|
number_of_posts: undefined,
|
||||||
|
number_of_comments: undefined,
|
||||||
|
number_of_communities: undefined,
|
||||||
|
enable_downvotes: undefined,
|
||||||
|
open_registration: undefined,
|
||||||
|
enable_nsfw: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -95,9 +110,20 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site.name) {
|
||||||
|
return `/u/${UserService.Instance.user.name} ${i18n.t('inbox')} - ${
|
||||||
|
this.state.site.name
|
||||||
|
}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h5 class="mb-1">
|
<h5 class="mb-1">
|
||||||
|
@ -269,7 +295,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
markable
|
markable
|
||||||
showCommunity
|
showCommunity
|
||||||
showContext
|
showContext
|
||||||
enableDownvotes={this.state.enableDownvotes}
|
enableDownvotes={this.state.site.enable_downvotes}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PrivateMessage privateMessage={i} />
|
<PrivateMessage privateMessage={i} />
|
||||||
|
@ -288,7 +314,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
markable
|
markable
|
||||||
showCommunity
|
showCommunity
|
||||||
showContext
|
showContext
|
||||||
enableDownvotes={this.state.enableDownvotes}
|
enableDownvotes={this.state.site.enable_downvotes}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -304,7 +330,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
markable
|
markable
|
||||||
showCommunity
|
showCommunity
|
||||||
showContext
|
showContext
|
||||||
enableDownvotes={this.state.enableDownvotes}
|
enableDownvotes={this.state.site.enable_downvotes}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -557,19 +583,13 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
this.state.enableDownvotes = data.site.enable_downvotes;
|
this.state.site = data.site;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `/u/${UserService.Instance.user.username} ${i18n.t(
|
|
||||||
'inbox'
|
|
||||||
)} - ${data.site.name}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendUnreadCount() {
|
sendUnreadCount() {
|
||||||
UserService.Instance.user.unreadCount = this.unreadCount();
|
UserService.Instance.unreadCountSub.next(this.unreadCount());
|
||||||
UserService.Instance.sub.next({
|
|
||||||
user: UserService.Instance.user,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unreadCount(): number {
|
unreadCount(): number {
|
||||||
|
|
33
ui/src/components/login.tsx
vendored
33
ui/src/components/login.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -10,6 +11,7 @@ import {
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
GetCaptchaResponse,
|
GetCaptchaResponse,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
|
Site,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { wsJsonToRes, validEmail, toast } from '../utils';
|
import { wsJsonToRes, validEmail, toast } from '../utils';
|
||||||
|
@ -20,9 +22,9 @@ interface State {
|
||||||
registerForm: RegisterForm;
|
registerForm: RegisterForm;
|
||||||
loginLoading: boolean;
|
loginLoading: boolean;
|
||||||
registerLoading: boolean;
|
registerLoading: boolean;
|
||||||
enable_nsfw: boolean;
|
|
||||||
captcha: GetCaptchaResponse;
|
captcha: GetCaptchaResponse;
|
||||||
captchaPlaying: boolean;
|
captchaPlaying: boolean;
|
||||||
|
site: Site;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Login extends Component<any, State> {
|
export class Login extends Component<any, State> {
|
||||||
|
@ -44,9 +46,22 @@ export class Login extends Component<any, State> {
|
||||||
},
|
},
|
||||||
loginLoading: false,
|
loginLoading: false,
|
||||||
registerLoading: false,
|
registerLoading: false,
|
||||||
enable_nsfw: undefined,
|
|
||||||
captcha: undefined,
|
captcha: undefined,
|
||||||
captchaPlaying: false,
|
captchaPlaying: false,
|
||||||
|
site: {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
creator_id: undefined,
|
||||||
|
published: undefined,
|
||||||
|
creator_name: undefined,
|
||||||
|
number_of_users: undefined,
|
||||||
|
number_of_posts: undefined,
|
||||||
|
number_of_comments: undefined,
|
||||||
|
number_of_communities: undefined,
|
||||||
|
enable_downvotes: undefined,
|
||||||
|
open_registration: undefined,
|
||||||
|
enable_nsfw: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -70,9 +85,18 @@ export class Login extends Component<any, State> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site.name) {
|
||||||
|
return `${i18n.t('login')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
|
<div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
|
||||||
<div class="col-12 col-lg-6">{this.registerForm()}</div>
|
<div class="col-12 col-lg-6">{this.registerForm()}</div>
|
||||||
|
@ -263,7 +287,7 @@ export class Login extends Component<any, State> {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{this.state.enable_nsfw && (
|
{this.state.site.enable_nsfw && (
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
|
@ -456,9 +480,8 @@ export class Login extends Component<any, State> {
|
||||||
toast(i18n.t('reset_password_mail_sent'));
|
toast(i18n.t('reset_password_mail_sent'));
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
this.state.enable_nsfw = data.site.enable_nsfw;
|
this.state.site = data.site;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `${i18n.t('login')} - ${data.site.name}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
11
ui/src/components/main.tsx
vendored
11
ui/src/components/main.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
|
@ -177,9 +178,18 @@ export class Main extends Component<any, MainState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.siteRes.site.name) {
|
||||||
|
return `${this.state.siteRes.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<main role="main" class="col-12 col-md-8">
|
<main role="main" class="col-12 col-md-8">
|
||||||
{this.posts()}
|
{this.posts()}
|
||||||
|
@ -627,7 +637,6 @@ export class Main extends Component<any, MainState> {
|
||||||
this.state.siteRes.banned = data.banned;
|
this.state.siteRes.banned = data.banned;
|
||||||
this.state.siteRes.online = data.online;
|
this.state.siteRes.online = data.online;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `${this.state.siteRes.site.name}`;
|
|
||||||
} else if (res.op == UserOperation.EditSite) {
|
} else if (res.op == UserOperation.EditSite) {
|
||||||
let data = res.data as SiteResponse;
|
let data = res.data as SiteResponse;
|
||||||
this.state.siteRes.site = data.site;
|
this.state.siteRes.site = data.site;
|
||||||
|
|
16
ui/src/components/modlog.tsx
vendored
16
ui/src/components/modlog.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
|
@ -17,6 +18,7 @@ import {
|
||||||
ModAdd,
|
ModAdd,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { wsJsonToRes, addTypeInfo, fetchLimit, toast } from '../utils';
|
import { wsJsonToRes, addTypeInfo, fetchLimit, toast } from '../utils';
|
||||||
|
@ -38,6 +40,7 @@ interface ModlogState {
|
||||||
communityId?: number;
|
communityId?: number;
|
||||||
communityName?: string;
|
communityName?: string;
|
||||||
page: number;
|
page: number;
|
||||||
|
site: Site;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +50,7 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
combined: [],
|
combined: [],
|
||||||
page: 1,
|
page: 1,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
site: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -338,9 +342,18 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site) {
|
||||||
|
return `Modlog - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5 class="">
|
<h5 class="">
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -434,7 +447,8 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
this.setCombined(data);
|
this.setCombined(data);
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
document.title = `Modlog - ${data.site.name}`;
|
this.state.site = data.site;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
391
ui/src/components/navbar.tsx
vendored
391
ui/src/components/navbar.tsx
vendored
|
@ -29,8 +29,9 @@ import {
|
||||||
toast,
|
toast,
|
||||||
messageToastify,
|
messageToastify,
|
||||||
md,
|
md,
|
||||||
|
setTheme,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n, i18nextSetup } from '../i18next';
|
||||||
|
|
||||||
interface NavbarState {
|
interface NavbarState {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
|
@ -44,14 +45,16 @@ interface NavbarState {
|
||||||
admins: Array<UserView>;
|
admins: Array<UserView>;
|
||||||
searchParam: string;
|
searchParam: string;
|
||||||
toggleSearch: boolean;
|
toggleSearch: boolean;
|
||||||
|
siteLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Navbar extends Component<any, NavbarState> {
|
export class Navbar extends Component<any, NavbarState> {
|
||||||
private wsSub: Subscription;
|
private wsSub: Subscription;
|
||||||
private userSub: Subscription;
|
private userSub: Subscription;
|
||||||
|
private unreadCountSub: Subscription;
|
||||||
private searchTextField: RefObject<HTMLInputElement>;
|
private searchTextField: RefObject<HTMLInputElement>;
|
||||||
emptyState: NavbarState = {
|
emptyState: NavbarState = {
|
||||||
isLoggedIn: UserService.Instance.user !== undefined,
|
isLoggedIn: false,
|
||||||
unreadCount: 0,
|
unreadCount: 0,
|
||||||
replies: [],
|
replies: [],
|
||||||
mentions: [],
|
mentions: [],
|
||||||
|
@ -62,22 +65,13 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
admins: [],
|
admins: [],
|
||||||
searchParam: '',
|
searchParam: '',
|
||||||
toggleSearch: false,
|
toggleSearch: false,
|
||||||
|
siteLoading: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
|
|
||||||
// Subscribe to user changes
|
|
||||||
this.userSub = UserService.Instance.sub.subscribe(user => {
|
|
||||||
this.state.isLoggedIn = user.user !== undefined;
|
|
||||||
if (this.state.isLoggedIn) {
|
|
||||||
this.state.unreadCount = user.user.unreadCount;
|
|
||||||
this.requestNotificationPermission();
|
|
||||||
}
|
|
||||||
this.setState(this.state);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.wsSub = WebSocketService.Instance.subject
|
this.wsSub = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
@ -86,17 +80,30 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.state.isLoggedIn) {
|
|
||||||
this.requestNotificationPermission();
|
|
||||||
// TODO couldn't get re-logging in to re-fetch unreads
|
|
||||||
this.fetchUnreads();
|
|
||||||
}
|
|
||||||
|
|
||||||
WebSocketService.Instance.getSite();
|
WebSocketService.Instance.getSite();
|
||||||
|
|
||||||
this.searchTextField = createRef();
|
this.searchTextField = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// Subscribe to jwt changes
|
||||||
|
this.userSub = UserService.Instance.jwtSub.subscribe(res => {
|
||||||
|
// A login
|
||||||
|
if (res !== undefined) {
|
||||||
|
this.requestNotificationPermission();
|
||||||
|
} else {
|
||||||
|
this.state.isLoggedIn = false;
|
||||||
|
}
|
||||||
|
WebSocketService.Instance.getSite();
|
||||||
|
this.setState(this.state);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to unread count changes
|
||||||
|
this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(res => {
|
||||||
|
this.setState({ unreadCount: res });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleSearchParam(i: Navbar, event: any) {
|
handleSearchParam(i: Navbar, event: any) {
|
||||||
i.state.searchParam = event.target.value;
|
i.state.searchParam = event.target.value;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
|
@ -145,6 +152,7 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.wsSub.unsubscribe();
|
this.wsSub.unsubscribe();
|
||||||
this.userSub.unsubscribe();
|
this.userSub.unsubscribe();
|
||||||
|
this.unreadCountSub.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO class active corresponding to current page
|
// TODO class active corresponding to current page
|
||||||
|
@ -152,9 +160,17 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
return (
|
return (
|
||||||
<nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
|
<nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Link title={this.state.version} class="navbar-brand" to="/">
|
{!this.state.siteLoading ? (
|
||||||
{this.state.siteName}
|
<Link title={this.state.version} class="navbar-brand" to="/">
|
||||||
</Link>
|
{this.state.siteName}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div class="navbar-item">
|
||||||
|
<svg class="icon icon-spinner spin">
|
||||||
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{this.state.isLoggedIn && (
|
{this.state.isLoggedIn && (
|
||||||
<Link
|
<Link
|
||||||
class="ml-auto p-0 navbar-toggler nav-link border-0"
|
class="ml-auto p-0 navbar-toggler nav-link border-0"
|
||||||
|
@ -180,151 +196,160 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
>
|
>
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div
|
{!this.state.siteLoading && (
|
||||||
className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
|
<div
|
||||||
>
|
className={`${
|
||||||
<ul class="navbar-nav my-2 mr-auto">
|
!this.state.expanded && 'collapse'
|
||||||
<li class="nav-item">
|
} navbar-collapse`}
|
||||||
<Link
|
>
|
||||||
class="nav-link"
|
<ul class="navbar-nav my-2 mr-auto">
|
||||||
to="/communities"
|
<li class="nav-item">
|
||||||
title={i18n.t('communities')}
|
<Link
|
||||||
>
|
class="nav-link"
|
||||||
{i18n.t('communities')}
|
to="/communities"
|
||||||
</Link>
|
title={i18n.t('communities')}
|
||||||
</li>
|
>
|
||||||
<li class="nav-item">
|
{i18n.t('communities')}
|
||||||
<Link
|
</Link>
|
||||||
class="nav-link"
|
</li>
|
||||||
to={{
|
<li class="nav-item">
|
||||||
pathname: '/create_post',
|
<Link
|
||||||
state: { prevPath: this.currentLocation },
|
class="nav-link"
|
||||||
}}
|
to={{
|
||||||
title={i18n.t('create_post')}
|
pathname: '/create_post',
|
||||||
>
|
state: { prevPath: this.currentLocation },
|
||||||
{i18n.t('create_post')}
|
}}
|
||||||
</Link>
|
title={i18n.t('create_post')}
|
||||||
</li>
|
>
|
||||||
<li class="nav-item">
|
{i18n.t('create_post')}
|
||||||
<Link
|
</Link>
|
||||||
class="nav-link"
|
</li>
|
||||||
to="/create_community"
|
<li class="nav-item">
|
||||||
title={i18n.t('create_community')}
|
<Link
|
||||||
>
|
class="nav-link"
|
||||||
{i18n.t('create_community')}
|
to="/create_community"
|
||||||
</Link>
|
title={i18n.t('create_community')}
|
||||||
</li>
|
>
|
||||||
<li className="nav-item">
|
{i18n.t('create_community')}
|
||||||
<Link
|
</Link>
|
||||||
class="nav-link"
|
</li>
|
||||||
to="/sponsors"
|
|
||||||
title={i18n.t('donate_to_lemmy')}
|
|
||||||
>
|
|
||||||
<svg class="icon">
|
|
||||||
<use xlinkHref="#icon-coffee"></use>
|
|
||||||
</svg>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
{!this.context.router.history.location.pathname.match(
|
|
||||||
/^\/search/
|
|
||||||
) && (
|
|
||||||
<form
|
|
||||||
class="form-inline"
|
|
||||||
onSubmit={linkEvent(this, this.handleSearchSubmit)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
class={`form-control mr-0 search-input ${
|
|
||||||
this.state.toggleSearch ? 'show-input' : 'hide-input'
|
|
||||||
}`}
|
|
||||||
onInput={linkEvent(this, this.handleSearchParam)}
|
|
||||||
value={this.state.searchParam}
|
|
||||||
ref={this.searchTextField}
|
|
||||||
type="text"
|
|
||||||
placeholder={i18n.t('search')}
|
|
||||||
onBlur={linkEvent(this, this.handleSearchBlur)}
|
|
||||||
></input>
|
|
||||||
<button
|
|
||||||
name="search-btn"
|
|
||||||
onClick={linkEvent(this, this.handleSearchBtn)}
|
|
||||||
class="btn btn-link"
|
|
||||||
style="color: var(--gray)"
|
|
||||||
>
|
|
||||||
<svg class="icon">
|
|
||||||
<use xlinkHref="#icon-search"></use>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
<ul class="navbar-nav my-2">
|
|
||||||
{this.canAdmin && (
|
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<Link
|
<Link
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
to={`/admin`}
|
to="/sponsors"
|
||||||
title={i18n.t('admin_settings')}
|
title={i18n.t('donate_to_lemmy')}
|
||||||
>
|
>
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
<use xlinkHref="#icon-settings"></use>
|
<use xlinkHref="#icon-coffee"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
</ul>
|
||||||
|
{!this.context.router.history.location.pathname.match(
|
||||||
|
/^\/search/
|
||||||
|
) && (
|
||||||
|
<form
|
||||||
|
class="form-inline"
|
||||||
|
onSubmit={linkEvent(this, this.handleSearchSubmit)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class={`form-control mr-0 search-input ${
|
||||||
|
this.state.toggleSearch ? 'show-input' : 'hide-input'
|
||||||
|
}`}
|
||||||
|
onInput={linkEvent(this, this.handleSearchParam)}
|
||||||
|
value={this.state.searchParam}
|
||||||
|
ref={this.searchTextField}
|
||||||
|
type="text"
|
||||||
|
placeholder={i18n.t('search')}
|
||||||
|
onBlur={linkEvent(this, this.handleSearchBlur)}
|
||||||
|
></input>
|
||||||
|
<button
|
||||||
|
name="search-btn"
|
||||||
|
onClick={linkEvent(this, this.handleSearchBtn)}
|
||||||
|
class="btn btn-link"
|
||||||
|
style="color: var(--gray)"
|
||||||
|
>
|
||||||
|
<svg class="icon">
|
||||||
|
<use xlinkHref="#icon-search"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
)}
|
)}
|
||||||
</ul>
|
<ul class="navbar-nav my-2">
|
||||||
{this.state.isLoggedIn ? (
|
{this.canAdmin && (
|
||||||
<>
|
|
||||||
<ul class="navbar-nav my-2">
|
|
||||||
<li className="nav-item">
|
|
||||||
<Link class="nav-link" to="/inbox" title={i18n.t('inbox')}>
|
|
||||||
<svg class="icon">
|
|
||||||
<use xlinkHref="#icon-bell"></use>
|
|
||||||
</svg>
|
|
||||||
{this.state.unreadCount > 0 && (
|
|
||||||
<span class="ml-1 badge badge-light">
|
|
||||||
{this.state.unreadCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<Link
|
<Link
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
to={`/u/${UserService.Instance.user.username}`}
|
to={`/admin`}
|
||||||
title={i18n.t('settings')}
|
title={i18n.t('admin_settings')}
|
||||||
>
|
>
|
||||||
<span>
|
<svg class="icon">
|
||||||
{UserService.Instance.user.avatar && showAvatars() && (
|
<use xlinkHref="#icon-settings"></use>
|
||||||
<img
|
</svg>
|
||||||
src={pictrsAvatarThumbnail(
|
</Link>
|
||||||
UserService.Instance.user.avatar
|
</li>
|
||||||
)}
|
)}
|
||||||
height="32"
|
</ul>
|
||||||
width="32"
|
{this.state.isLoggedIn ? (
|
||||||
class="rounded-circle mr-2"
|
<>
|
||||||
/>
|
<ul class="navbar-nav my-2">
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link
|
||||||
|
class="nav-link"
|
||||||
|
to="/inbox"
|
||||||
|
title={i18n.t('inbox')}
|
||||||
|
>
|
||||||
|
<svg class="icon">
|
||||||
|
<use xlinkHref="#icon-bell"></use>
|
||||||
|
</svg>
|
||||||
|
{this.state.unreadCount > 0 && (
|
||||||
|
<span class="ml-1 badge badge-light">
|
||||||
|
{this.state.unreadCount}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
{UserService.Instance.user.username}
|
</Link>
|
||||||
</span>
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link
|
||||||
|
class="nav-link"
|
||||||
|
to={`/u/${UserService.Instance.user.name}`}
|
||||||
|
title={i18n.t('settings')}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{UserService.Instance.user.avatar &&
|
||||||
|
showAvatars() && (
|
||||||
|
<img
|
||||||
|
src={pictrsAvatarThumbnail(
|
||||||
|
UserService.Instance.user.avatar
|
||||||
|
)}
|
||||||
|
height="32"
|
||||||
|
width="32"
|
||||||
|
class="rounded-circle mr-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{UserService.Instance.user.name}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ul class="navbar-nav my-2">
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link
|
||||||
|
class="btn btn-success"
|
||||||
|
to="/login"
|
||||||
|
title={i18n.t('login_sign_up')}
|
||||||
|
>
|
||||||
|
{i18n.t('login_sign_up')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
)}
|
||||||
) : (
|
</div>
|
||||||
<ul class="navbar-nav my-2">
|
)}
|
||||||
<li className="nav-item">
|
|
||||||
<Link
|
|
||||||
class="btn btn-success"
|
|
||||||
to="/login"
|
|
||||||
title={i18n.t('login_sign_up')}
|
|
||||||
>
|
|
||||||
{i18n.t('login_sign_up')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
@ -400,38 +425,53 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
this.state.siteName = data.site.name;
|
this.state.siteName = data.site.name;
|
||||||
this.state.version = data.version;
|
this.state.version = data.version;
|
||||||
this.state.admins = data.admins;
|
this.state.admins = data.admins;
|
||||||
this.setState(this.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The login
|
||||||
|
if (data.my_user) {
|
||||||
|
UserService.Instance.user = data.my_user;
|
||||||
|
// On the first load, check the unreads
|
||||||
|
if (this.state.isLoggedIn == false) {
|
||||||
|
this.requestNotificationPermission();
|
||||||
|
this.fetchUnreads();
|
||||||
|
setTheme(data.my_user.theme, true);
|
||||||
|
}
|
||||||
|
this.state.isLoggedIn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
i18nextSetup();
|
||||||
|
|
||||||
|
this.state.siteLoading = false;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchUnreads() {
|
fetchUnreads() {
|
||||||
if (this.state.isLoggedIn) {
|
console.log('Fetching unreads...');
|
||||||
let repliesForm: GetRepliesForm = {
|
let repliesForm: GetRepliesForm = {
|
||||||
sort: SortType[SortType.New],
|
sort: SortType[SortType.New],
|
||||||
unread_only: true,
|
unread_only: true,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
};
|
};
|
||||||
|
|
||||||
let userMentionsForm: GetUserMentionsForm = {
|
let userMentionsForm: GetUserMentionsForm = {
|
||||||
sort: SortType[SortType.New],
|
sort: SortType[SortType.New],
|
||||||
unread_only: true,
|
unread_only: true,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
};
|
};
|
||||||
|
|
||||||
let privateMessagesForm: GetPrivateMessagesForm = {
|
let privateMessagesForm: GetPrivateMessagesForm = {
|
||||||
unread_only: true,
|
unread_only: true,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.currentLocation !== '/inbox') {
|
if (this.currentLocation !== '/inbox') {
|
||||||
WebSocketService.Instance.getReplies(repliesForm);
|
WebSocketService.Instance.getReplies(repliesForm);
|
||||||
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
||||||
WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
|
WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -440,10 +480,7 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendUnreadCount() {
|
sendUnreadCount() {
|
||||||
UserService.Instance.user.unreadCount = this.state.unreadCount;
|
UserService.Instance.unreadCountSub.next(this.state.unreadCount);
|
||||||
UserService.Instance.sub.next({
|
|
||||||
user: UserService.Instance.user,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateUnreadCount(): number {
|
calculateUnreadCount(): number {
|
||||||
|
|
16
ui/src/components/password_change.tsx
vendored
16
ui/src/components/password_change.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -7,6 +8,7 @@ import {
|
||||||
PasswordChangeForm,
|
PasswordChangeForm,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
|
import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
|
||||||
|
@ -15,6 +17,7 @@ import { i18n } from '../i18next';
|
||||||
interface State {
|
interface State {
|
||||||
passwordChangeForm: PasswordChangeForm;
|
passwordChangeForm: PasswordChangeForm;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
site: Site;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PasswordChange extends Component<any, State> {
|
export class PasswordChange extends Component<any, State> {
|
||||||
|
@ -27,6 +30,7 @@ export class PasswordChange extends Component<any, State> {
|
||||||
password_verify: undefined,
|
password_verify: undefined,
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
|
site: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -48,9 +52,18 @@ export class PasswordChange extends Component<any, State> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site) {
|
||||||
|
return `${i18n.t('password_change')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||||
<h5>{i18n.t('password_change')}</h5>
|
<h5>{i18n.t('password_change')}</h5>
|
||||||
|
@ -142,7 +155,8 @@ export class PasswordChange extends Component<any, State> {
|
||||||
this.props.history.push('/');
|
this.props.history.push('/');
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
document.title = `${i18n.t('password_change')} - ${data.site.name}`;
|
this.state.site = data.site;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
ui/src/components/post.tsx
vendored
18
ui/src/components/post.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -174,16 +175,24 @@ export class Post extends Component<any, PostState> {
|
||||||
auth: null,
|
auth: null,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.markCommentAsRead(form);
|
WebSocketService.Instance.markCommentAsRead(form);
|
||||||
UserService.Instance.user.unreadCount--;
|
UserService.Instance.unreadCountSub.next(
|
||||||
UserService.Instance.sub.next({
|
UserService.Instance.unreadCountSub.value - 1
|
||||||
user: UserService.Instance.user,
|
);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.post) {
|
||||||
|
return `${this.state.post.name} - ${this.state.siteRes.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
{this.state.loading ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -407,7 +416,6 @@ export class Post extends Component<any, PostState> {
|
||||||
this.state.moderators = data.moderators;
|
this.state.moderators = data.moderators;
|
||||||
this.state.online = data.online;
|
this.state.online = data.online;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
document.title = `${this.state.post.name} - ${this.state.siteRes.site.name}`;
|
|
||||||
|
|
||||||
// Get cross-posts
|
// Get cross-posts
|
||||||
if (this.state.post.url) {
|
if (this.state.post.url) {
|
||||||
|
|
21
ui/src/components/search.tsx
vendored
21
ui/src/components/search.tsx
vendored
|
@ -1,5 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -156,9 +156,24 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site.name) {
|
||||||
|
if (this.state.q) {
|
||||||
|
return `${i18n.t('search')} - ${this.state.q} - ${
|
||||||
|
this.state.site.name
|
||||||
|
}`;
|
||||||
|
} else {
|
||||||
|
return `${i18n.t('search')} - ${this.state.site.name}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<h5>{i18n.t('search')}</h5>
|
<h5>{i18n.t('search')}</h5>
|
||||||
{this.selects()}
|
{this.selects()}
|
||||||
{this.searchForm()}
|
{this.searchForm()}
|
||||||
|
@ -500,9 +515,6 @@ export class Search extends Component<any, SearchState> {
|
||||||
let data = res.data as SearchResponse;
|
let data = res.data as SearchResponse;
|
||||||
this.state.searchResponse = data;
|
this.state.searchResponse = data;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
document.title = `${i18n.t('search')} - ${this.state.q} - ${
|
|
||||||
this.state.site.name
|
|
||||||
}`;
|
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (res.op == UserOperation.CreateCommentLike) {
|
} else if (res.op == UserOperation.CreateCommentLike) {
|
||||||
|
@ -517,7 +529,6 @@ export class Search extends Component<any, SearchState> {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
this.state.site = data.site;
|
this.state.site = data.site;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
document.title = `${i18n.t('search')} - ${data.site.name}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
ui/src/components/setup.tsx
vendored
6
ui/src/components/setup.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
|
@ -54,13 +55,14 @@ export class Setup extends Component<any, State> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
get documentTitle(): string {
|
||||||
document.title = `${i18n.t('setup')} - Lemmy`;
|
return `${i18n.t('setup')} - Lemmy`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 offset-lg-3 col-lg-6">
|
<div class="col-12 offset-lg-3 col-lg-6">
|
||||||
<h3>{i18n.t('lemmy_instance_setup')}</h3>
|
<h3>{i18n.t('lemmy_instance_setup')}</h3>
|
||||||
|
|
26
ui/src/components/sponsors.tsx
vendored
26
ui/src/components/sponsors.tsx
vendored
|
@ -1,9 +1,11 @@
|
||||||
import { Component } from 'inferno';
|
import { Component } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import {
|
import {
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
|
Site,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
UserOperation,
|
UserOperation,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
|
@ -32,7 +34,7 @@ let general = [
|
||||||
'Andre Vallestero',
|
'Andre Vallestero',
|
||||||
'NotTooHighToHack',
|
'NotTooHighToHack',
|
||||||
];
|
];
|
||||||
let highlighted = ['DiscountFuneral', 'Oskenso Kashi', 'Alex Benishek'];
|
let highlighted = ['DQW', 'DiscountFuneral', 'Oskenso Kashi', 'Alex Benishek'];
|
||||||
let silver: Array<SilverUser> = [
|
let silver: Array<SilverUser> = [
|
||||||
{
|
{
|
||||||
name: 'Redjoker',
|
name: 'Redjoker',
|
||||||
|
@ -42,10 +44,18 @@ let silver: Array<SilverUser> = [
|
||||||
// let gold = [];
|
// let gold = [];
|
||||||
// let latinum = [];
|
// let latinum = [];
|
||||||
|
|
||||||
export class Sponsors extends Component<any, any> {
|
interface SponsorsState {
|
||||||
|
site: Site;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Sponsors extends Component<any, SponsorsState> {
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
|
private emptyState: SponsorsState = {
|
||||||
|
site: undefined,
|
||||||
|
};
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
this.state = this.emptyState;
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
@ -65,9 +75,18 @@ export class Sponsors extends Component<any, any> {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.site) {
|
||||||
|
return `${i18n.t('sponsors')} - ${this.state.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container text-center">
|
<div class="container text-center">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
{this.topMessage()}
|
{this.topMessage()}
|
||||||
<hr />
|
<hr />
|
||||||
{this.sponsors()}
|
{this.sponsors()}
|
||||||
|
@ -183,7 +202,8 @@ export class Sponsors extends Component<any, any> {
|
||||||
return;
|
return;
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
document.title = `${i18n.t('sponsors')} - ${data.site.name}`;
|
this.state.site = data.site;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
11
ui/src/components/user.tsx
vendored
11
ui/src/components/user.tsx
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { Component, linkEvent } from 'inferno';
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Helmet } from 'inferno-helmet';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
|
@ -207,13 +208,21 @@ export class User extends Component<any, UserState> {
|
||||||
// Couldnt get a refresh working. This does for now.
|
// Couldnt get a refresh working. This does for now.
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
document.title = `/u/${this.state.username} - ${this.state.siteRes.site.name}`;
|
|
||||||
setupTippy();
|
setupTippy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get documentTitle(): string {
|
||||||
|
if (this.state.siteRes.site.name) {
|
||||||
|
return `/u/${this.state.username} - ${this.state.siteRes.site.name}`;
|
||||||
|
} else {
|
||||||
|
return 'Lemmy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<Helmet title={this.documentTitle} />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-8">
|
<div class="col-12 col-md-8">
|
||||||
<h5>
|
<h5>
|
||||||
|
|
21
ui/src/i18next.ts
vendored
21
ui/src/i18next.ts
vendored
|
@ -65,15 +65,16 @@ function format(value: any, format: any, lng: any): any {
|
||||||
return format === 'uppercase' ? value.toUpperCase() : value;
|
return format === 'uppercase' ? value.toUpperCase() : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
i18next.init({
|
export function i18nextSetup() {
|
||||||
debug: false,
|
i18next.init({
|
||||||
// load: 'languageOnly',
|
debug: false,
|
||||||
|
// load: 'languageOnly',
|
||||||
// initImmediate: false,
|
|
||||||
lng: getLanguage(),
|
|
||||||
fallbackLng: 'en',
|
|
||||||
resources,
|
|
||||||
interpolation: { format },
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// initImmediate: false,
|
||||||
|
lng: getLanguage(),
|
||||||
|
fallbackLng: 'en',
|
||||||
|
resources,
|
||||||
|
interpolation: { format },
|
||||||
|
});
|
||||||
|
}
|
||||||
export { i18next as i18n, resources };
|
export { i18next as i18n, resources };
|
||||||
|
|
31
ui/src/interfaces.ts
vendored
31
ui/src/interfaces.ts
vendored
|
@ -101,18 +101,33 @@ export enum SearchType {
|
||||||
Url,
|
Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface Claims {
|
||||||
id: number;
|
id: number;
|
||||||
iss: string;
|
iss: string;
|
||||||
username: string;
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
preferred_username?: string;
|
||||||
|
email?: string;
|
||||||
|
avatar?: string;
|
||||||
|
admin: boolean;
|
||||||
|
banned: boolean;
|
||||||
|
published: string;
|
||||||
|
updated?: string;
|
||||||
show_nsfw: boolean;
|
show_nsfw: boolean;
|
||||||
theme: string;
|
theme: string;
|
||||||
default_sort_type: SortType;
|
default_sort_type: SortType;
|
||||||
default_listing_type: ListingType;
|
default_listing_type: ListingType;
|
||||||
lang: string;
|
lang: string;
|
||||||
avatar?: string;
|
|
||||||
show_avatars: boolean;
|
show_avatars: boolean;
|
||||||
unreadCount?: number;
|
send_notifications_to_email: boolean;
|
||||||
|
matrix_user_id?: string;
|
||||||
|
actor_id: string;
|
||||||
|
bio?: string;
|
||||||
|
local: boolean;
|
||||||
|
last_refreshed_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserView {
|
export interface UserView {
|
||||||
|
@ -806,6 +821,10 @@ export interface GetSiteConfig {
|
||||||
auth?: string;
|
auth?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetSiteForm {
|
||||||
|
auth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GetSiteConfigResponse {
|
export interface GetSiteConfigResponse {
|
||||||
config_hjson: string;
|
config_hjson: string;
|
||||||
}
|
}
|
||||||
|
@ -821,6 +840,7 @@ export interface GetSiteResponse {
|
||||||
banned: Array<UserView>;
|
banned: Array<UserView>;
|
||||||
online: number;
|
online: number;
|
||||||
version: string;
|
version: string;
|
||||||
|
my_user?: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SiteResponse {
|
export interface SiteResponse {
|
||||||
|
@ -1008,7 +1028,8 @@ type ResponseType =
|
||||||
| AddAdminResponse
|
| AddAdminResponse
|
||||||
| PrivateMessageResponse
|
| PrivateMessageResponse
|
||||||
| PrivateMessagesResponse
|
| PrivateMessagesResponse
|
||||||
| GetSiteConfigResponse;
|
| GetSiteConfigResponse
|
||||||
|
| GetSiteResponse;
|
||||||
|
|
||||||
export interface WebSocketResponse {
|
export interface WebSocketResponse {
|
||||||
op: UserOperation;
|
op: UserOperation;
|
||||||
|
|
27
ui/src/services/UserService.ts
vendored
27
ui/src/services/UserService.ts
vendored
|
@ -1,20 +1,22 @@
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { User, LoginResponse } from '../interfaces';
|
import { User, Claims, LoginResponse } from '../interfaces';
|
||||||
import { setTheme } from '../utils';
|
import { setTheme } from '../utils';
|
||||||
import jwt_decode from 'jwt-decode';
|
import jwt_decode from 'jwt-decode';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject, BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
export class UserService {
|
export class UserService {
|
||||||
private static _instance: UserService;
|
private static _instance: UserService;
|
||||||
public user: User;
|
public user: User;
|
||||||
public sub: Subject<{ user: User }> = new Subject<{
|
public claims: Claims;
|
||||||
user: User;
|
public jwtSub: Subject<string> = new Subject<string>();
|
||||||
}>();
|
public unreadCountSub: BehaviorSubject<number> = new BehaviorSubject<number>(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
let jwt = Cookies.get('jwt');
|
let jwt = Cookies.get('jwt');
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
this.setUser(jwt);
|
this.setClaims(jwt);
|
||||||
} else {
|
} else {
|
||||||
setTheme();
|
setTheme();
|
||||||
console.log('No JWT cookie found.');
|
console.log('No JWT cookie found.');
|
||||||
|
@ -22,16 +24,17 @@ export class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public login(res: LoginResponse) {
|
public login(res: LoginResponse) {
|
||||||
this.setUser(res.jwt);
|
this.setClaims(res.jwt);
|
||||||
Cookies.set('jwt', res.jwt, { expires: 365 });
|
Cookies.set('jwt', res.jwt, { expires: 365 });
|
||||||
console.log('jwt cookie set');
|
console.log('jwt cookie set');
|
||||||
}
|
}
|
||||||
|
|
||||||
public logout() {
|
public logout() {
|
||||||
|
this.claims = undefined;
|
||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
Cookies.remove('jwt');
|
Cookies.remove('jwt');
|
||||||
setTheme();
|
setTheme();
|
||||||
this.sub.next({ user: undefined });
|
this.jwtSub.next();
|
||||||
console.log('Logged out.');
|
console.log('Logged out.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,11 +42,9 @@ export class UserService {
|
||||||
return Cookies.get('jwt');
|
return Cookies.get('jwt');
|
||||||
}
|
}
|
||||||
|
|
||||||
private setUser(jwt: string) {
|
private setClaims(jwt: string) {
|
||||||
this.user = jwt_decode(jwt);
|
this.claims = jwt_decode(jwt);
|
||||||
setTheme(this.user.theme, true);
|
this.jwtSub.next(jwt);
|
||||||
this.sub.next({ user: this.user });
|
|
||||||
console.log(this.user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get Instance() {
|
public static get Instance() {
|
||||||
|
|
6
ui/src/services/WebSocketService.ts
vendored
6
ui/src/services/WebSocketService.ts
vendored
|
@ -51,6 +51,7 @@ import {
|
||||||
GetCommentsForm,
|
GetCommentsForm,
|
||||||
UserJoinForm,
|
UserJoinForm,
|
||||||
GetSiteConfig,
|
GetSiteConfig,
|
||||||
|
GetSiteForm,
|
||||||
SiteConfigForm,
|
SiteConfigForm,
|
||||||
MessageType,
|
MessageType,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
|
@ -320,8 +321,9 @@ export class WebSocketService {
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.EditSite, siteForm));
|
this.ws.send(this.wsSendWrapper(UserOperation.EditSite, siteForm));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSite() {
|
public getSite(form: GetSiteForm = {}) {
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.GetSite, {}));
|
this.setAuth(form, false);
|
||||||
|
this.ws.send(this.wsSendWrapper(UserOperation.GetSite, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSiteConfig() {
|
public getSiteConfig() {
|
||||||
|
|
5596
ui/yarn.lock
vendored
5596
ui/yarn.lock
vendored
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue