Merge branch 'main' into captcha_setup

This commit is contained in:
Dessalines 2020-07-27 12:43:09 -04:00
commit 9509529f69
34 changed files with 4560 additions and 2072 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

File diff suppressed because it is too large Load diff