mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-12-23 03:11:32 +00:00
Remove extra jwt claims (for user settings) (#1025)
* Remove extra jwt claims (for user settings) - The JWT token only contains the issuer, and your user id now. - Now only a page refresh is necessary to pick up your settings on all clients, including theme, language, etc. - GetSiteResponse now gives you your user and settings if logged in. - Fixes #773 * Remove extra comment line, I tested nsfw * Adding a todo to add a User_::readSafe()
This commit is contained in:
parent
571d0a6500
commit
d1342afe93
15 changed files with 348 additions and 258 deletions
5
docs/src/contributing_websocket_http_api.md
vendored
5
docs/src/contributing_websocket_http_api.md
vendored
|
@ -942,6 +942,10 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
|||
```rust
|
||||
{
|
||||
op: "GetSite"
|
||||
data: {
|
||||
auth: Option<String>,
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
|
@ -954,6 +958,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
|||
banned: Vec<UserView>,
|
||||
online: usize, // This is currently broken
|
||||
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 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_"]
|
||||
pub struct User_ {
|
||||
pub id: i32,
|
||||
|
|
|
@ -56,14 +56,14 @@ pub struct UserView {
|
|||
pub actor_id: String,
|
||||
pub name: 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 bio: Option<String>,
|
||||
pub local: bool,
|
||||
pub admin: bool,
|
||||
pub banned: bool,
|
||||
pub show_avatars: bool,
|
||||
pub send_notifications_to_email: bool,
|
||||
pub show_avatars: bool, // TODO this is a setting, probably doesn't need to be here
|
||||
pub send_notifications_to_email: bool, // TODO also never used
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub number_of_posts: i64,
|
||||
pub post_score: i64,
|
||||
|
|
|
@ -9,15 +9,7 @@ type Jwt = String;
|
|||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub id: i32,
|
||||
pub username: 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 {
|
||||
|
@ -36,15 +28,7 @@ impl Claims {
|
|||
pub fn jwt(user: User_, hostname: String) -> Jwt {
|
||||
let my_claims = Claims {
|
||||
id: user.id,
|
||||
username: user.name.to_owned(),
|
||||
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(
|
||||
&Header::default(),
|
||||
|
|
|
@ -591,21 +591,26 @@ impl Perform for Oper<ListCommunities> {
|
|||
) -> Result<ListCommunitiesResponse, LemmyError> {
|
||||
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) {
|
||||
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,
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
let user_id = match &user_claims {
|
||||
Some(claims) => Some(claims.id),
|
||||
let user_id = match &user {
|
||||
Some(user) => Some(user.id),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let show_nsfw = match &user_claims {
|
||||
Some(claims) => claims.show_nsfw,
|
||||
let show_nsfw = match &user {
|
||||
Some(user) => user.show_nsfw,
|
||||
None => false,
|
||||
};
|
||||
|
||||
|
|
|
@ -370,21 +370,26 @@ impl Perform for Oper<GetPosts> {
|
|||
) -> Result<GetPostsResponse, LemmyError> {
|
||||
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) {
|
||||
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,
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
let user_id = match &user_claims {
|
||||
Some(claims) => Some(claims.id),
|
||||
let user_id = match &user {
|
||||
Some(user) => Some(user.id),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let show_nsfw = match &user_claims {
|
||||
Some(claims) => claims.show_nsfw,
|
||||
let show_nsfw = match &user {
|
||||
Some(user) => user.show_nsfw,
|
||||
None => false,
|
||||
};
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ use lemmy_db::{
|
|||
post_view::*,
|
||||
site::*,
|
||||
site_view::*,
|
||||
user::*,
|
||||
user_view::*,
|
||||
Crud,
|
||||
SearchType,
|
||||
|
@ -98,7 +99,9 @@ pub struct EditSite {
|
|||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetSite {}
|
||||
pub struct GetSite {
|
||||
auth: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct SiteResponse {
|
||||
|
@ -112,6 +115,7 @@ pub struct GetSiteResponse {
|
|||
banned: Vec<UserView>,
|
||||
pub online: usize,
|
||||
version: String,
|
||||
my_user: Option<User_>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -352,7 +356,7 @@ impl Perform for Oper<GetSite> {
|
|||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetSiteResponse, LemmyError> {
|
||||
let _data: &GetSite = &self.data;
|
||||
let data: &GetSite = &self.data;
|
||||
|
||||
// TODO refactor this a little
|
||||
let res = blocking(pool, move |conn| Site::read(conn, 1)).await?;
|
||||
|
@ -415,12 +419,29 @@ impl Perform for Oper<GetSite> {
|
|||
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 {
|
||||
site: site_view,
|
||||
admins,
|
||||
banned,
|
||||
online,
|
||||
version: version::VERSION.to_string(),
|
||||
my_user,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -614,6 +635,11 @@ impl Perform for Oper<TransferSite> {
|
|||
};
|
||||
|
||||
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??;
|
||||
|
||||
|
@ -664,6 +690,7 @@ impl Perform for Oper<TransferSite> {
|
|||
banned,
|
||||
online: 0,
|
||||
version: version::VERSION.to_string(),
|
||||
my_user: Some(user),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -561,21 +561,26 @@ impl Perform for Oper<GetUserDetails> {
|
|||
) -> Result<GetUserDetailsResponse, LemmyError> {
|
||||
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) {
|
||||
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,
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
let user_id = match &user_claims {
|
||||
Some(claims) => Some(claims.id),
|
||||
let user_id = match &user {
|
||||
Some(user) => Some(user.id),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let show_nsfw = match &user_claims {
|
||||
Some(claims) => claims.show_nsfw,
|
||||
let show_nsfw = match &user {
|
||||
Some(user) => user.show_nsfw,
|
||||
None => false,
|
||||
};
|
||||
|
||||
|
@ -1188,11 +1193,11 @@ impl Perform for Oper<CreatePrivateMessage> {
|
|||
let subject = &format!(
|
||||
"{} - Private Message from {}",
|
||||
Settings::get().hostname,
|
||||
claims.username
|
||||
user.name,
|
||||
);
|
||||
let html = &format!(
|
||||
"<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) {
|
||||
Ok(_o) => _o,
|
||||
|
|
7
ui/src/components/inbox.tsx
vendored
7
ui/src/components/inbox.tsx
vendored
|
@ -559,17 +559,14 @@ export class Inbox extends Component<any, InboxState> {
|
|||
let data = res.data as GetSiteResponse;
|
||||
this.state.enableDownvotes = data.site.enable_downvotes;
|
||||
this.setState(this.state);
|
||||
document.title = `/u/${UserService.Instance.user.username} ${i18n.t(
|
||||
document.title = `/u/${UserService.Instance.user.name} ${i18n.t(
|
||||
'inbox'
|
||||
)} - ${data.site.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
sendUnreadCount() {
|
||||
UserService.Instance.user.unreadCount = this.unreadCount();
|
||||
UserService.Instance.sub.next({
|
||||
user: UserService.Instance.user,
|
||||
});
|
||||
UserService.Instance.unreadCountSub.next(this.unreadCount());
|
||||
}
|
||||
|
||||
unreadCount(): number {
|
||||
|
|
391
ui/src/components/navbar.tsx
vendored
391
ui/src/components/navbar.tsx
vendored
|
@ -29,8 +29,9 @@ import {
|
|||
toast,
|
||||
messageToastify,
|
||||
md,
|
||||
setTheme,
|
||||
} from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
import { i18n, i18nextSetup } from '../i18next';
|
||||
|
||||
interface NavbarState {
|
||||
isLoggedIn: boolean;
|
||||
|
@ -44,14 +45,16 @@ interface NavbarState {
|
|||
admins: Array<UserView>;
|
||||
searchParam: string;
|
||||
toggleSearch: boolean;
|
||||
siteLoading: boolean;
|
||||
}
|
||||
|
||||
export class Navbar extends Component<any, NavbarState> {
|
||||
private wsSub: Subscription;
|
||||
private userSub: Subscription;
|
||||
private unreadCountSub: Subscription;
|
||||
private searchTextField: RefObject<HTMLInputElement>;
|
||||
emptyState: NavbarState = {
|
||||
isLoggedIn: UserService.Instance.user !== undefined,
|
||||
isLoggedIn: false,
|
||||
unreadCount: 0,
|
||||
replies: [],
|
||||
mentions: [],
|
||||
|
@ -62,22 +65,13 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
admins: [],
|
||||
searchParam: '',
|
||||
toggleSearch: false,
|
||||
siteLoading: true,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
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
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
|
@ -86,17 +80,30 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
() => 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();
|
||||
|
||||
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) {
|
||||
i.state.searchParam = event.target.value;
|
||||
i.setState(i.state);
|
||||
|
@ -145,6 +152,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
componentWillUnmount() {
|
||||
this.wsSub.unsubscribe();
|
||||
this.userSub.unsubscribe();
|
||||
this.unreadCountSub.unsubscribe();
|
||||
}
|
||||
|
||||
// TODO class active corresponding to current page
|
||||
|
@ -152,9 +160,17 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
return (
|
||||
<nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
|
||||
<div class="container">
|
||||
<Link title={this.state.version} class="navbar-brand" to="/">
|
||||
{this.state.siteName}
|
||||
</Link>
|
||||
{!this.state.siteLoading ? (
|
||||
<Link title={this.state.version} class="navbar-brand" to="/">
|
||||
{this.state.siteName}
|
||||
</Link>
|
||||
) : (
|
||||
<div class="navbar-item">
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{this.state.isLoggedIn && (
|
||||
<Link
|
||||
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>
|
||||
</button>
|
||||
<div
|
||||
className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
|
||||
>
|
||||
<ul class="navbar-nav my-2 mr-auto">
|
||||
<li class="nav-item">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to="/communities"
|
||||
title={i18n.t('communities')}
|
||||
>
|
||||
{i18n.t('communities')}
|
||||
</Link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to={{
|
||||
pathname: '/create_post',
|
||||
state: { prevPath: this.currentLocation },
|
||||
}}
|
||||
title={i18n.t('create_post')}
|
||||
>
|
||||
{i18n.t('create_post')}
|
||||
</Link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to="/create_community"
|
||||
title={i18n.t('create_community')}
|
||||
>
|
||||
{i18n.t('create_community')}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
class="nav-link"
|
||||
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 && (
|
||||
{!this.state.siteLoading && (
|
||||
<div
|
||||
className={`${
|
||||
!this.state.expanded && 'collapse'
|
||||
} navbar-collapse`}
|
||||
>
|
||||
<ul class="navbar-nav my-2 mr-auto">
|
||||
<li class="nav-item">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to="/communities"
|
||||
title={i18n.t('communities')}
|
||||
>
|
||||
{i18n.t('communities')}
|
||||
</Link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to={{
|
||||
pathname: '/create_post',
|
||||
state: { prevPath: this.currentLocation },
|
||||
}}
|
||||
title={i18n.t('create_post')}
|
||||
>
|
||||
{i18n.t('create_post')}
|
||||
</Link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to="/create_community"
|
||||
title={i18n.t('create_community')}
|
||||
>
|
||||
{i18n.t('create_community')}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to={`/admin`}
|
||||
title={i18n.t('admin_settings')}
|
||||
to="/sponsors"
|
||||
title={i18n.t('donate_to_lemmy')}
|
||||
>
|
||||
<svg class="icon">
|
||||
<use xlinkHref="#icon-settings"></use>
|
||||
<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>
|
||||
{this.state.isLoggedIn ? (
|
||||
<>
|
||||
<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">
|
||||
<ul class="navbar-nav my-2">
|
||||
{this.canAdmin && (
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to={`/u/${UserService.Instance.user.username}`}
|
||||
title={i18n.t('settings')}
|
||||
to={`/admin`}
|
||||
title={i18n.t('admin_settings')}
|
||||
>
|
||||
<span>
|
||||
{UserService.Instance.user.avatar && showAvatars() && (
|
||||
<img
|
||||
src={pictrsAvatarThumbnail(
|
||||
UserService.Instance.user.avatar
|
||||
)}
|
||||
height="32"
|
||||
width="32"
|
||||
class="rounded-circle mr-2"
|
||||
/>
|
||||
<svg class="icon">
|
||||
<use xlinkHref="#icon-settings"></use>
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
{this.state.isLoggedIn ? (
|
||||
<>
|
||||
<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}
|
||||
</span>
|
||||
</Link>
|
||||
</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>
|
||||
</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>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
@ -400,38 +425,53 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
this.state.siteName = data.site.name;
|
||||
this.state.version = data.version;
|
||||
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() {
|
||||
if (this.state.isLoggedIn) {
|
||||
let repliesForm: GetRepliesForm = {
|
||||
sort: SortType[SortType.New],
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
console.log('Fetching unreads...');
|
||||
let repliesForm: GetRepliesForm = {
|
||||
sort: SortType[SortType.New],
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
|
||||
let userMentionsForm: GetUserMentionsForm = {
|
||||
sort: SortType[SortType.New],
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
let userMentionsForm: GetUserMentionsForm = {
|
||||
sort: SortType[SortType.New],
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
|
||||
let privateMessagesForm: GetPrivateMessagesForm = {
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
let privateMessagesForm: GetPrivateMessagesForm = {
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
|
||||
if (this.currentLocation !== '/inbox') {
|
||||
WebSocketService.Instance.getReplies(repliesForm);
|
||||
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
||||
WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
|
||||
}
|
||||
if (this.currentLocation !== '/inbox') {
|
||||
WebSocketService.Instance.getReplies(repliesForm);
|
||||
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
||||
WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -440,10 +480,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
}
|
||||
|
||||
sendUnreadCount() {
|
||||
UserService.Instance.user.unreadCount = this.state.unreadCount;
|
||||
UserService.Instance.sub.next({
|
||||
user: UserService.Instance.user,
|
||||
});
|
||||
UserService.Instance.unreadCountSub.next(this.state.unreadCount);
|
||||
}
|
||||
|
||||
calculateUnreadCount(): number {
|
||||
|
|
7
ui/src/components/post.tsx
vendored
7
ui/src/components/post.tsx
vendored
|
@ -174,10 +174,9 @@ export class Post extends Component<any, PostState> {
|
|||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.markCommentAsRead(form);
|
||||
UserService.Instance.user.unreadCount--;
|
||||
UserService.Instance.sub.next({
|
||||
user: UserService.Instance.user,
|
||||
});
|
||||
UserService.Instance.unreadCountSub.next(
|
||||
UserService.Instance.unreadCountSub.value - 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
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;
|
||||
}
|
||||
|
||||
i18next.init({
|
||||
debug: false,
|
||||
// load: 'languageOnly',
|
||||
|
||||
// initImmediate: false,
|
||||
lng: getLanguage(),
|
||||
fallbackLng: 'en',
|
||||
resources,
|
||||
interpolation: { format },
|
||||
});
|
||||
export function i18nextSetup() {
|
||||
i18next.init({
|
||||
debug: false,
|
||||
// load: 'languageOnly',
|
||||
|
||||
// initImmediate: false,
|
||||
lng: getLanguage(),
|
||||
fallbackLng: 'en',
|
||||
resources,
|
||||
interpolation: { format },
|
||||
});
|
||||
}
|
||||
export { i18next as i18n, resources };
|
||||
|
|
31
ui/src/interfaces.ts
vendored
31
ui/src/interfaces.ts
vendored
|
@ -100,18 +100,33 @@ export enum SearchType {
|
|||
Url,
|
||||
}
|
||||
|
||||
export interface User {
|
||||
export interface Claims {
|
||||
id: number;
|
||||
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;
|
||||
theme: string;
|
||||
default_sort_type: SortType;
|
||||
default_listing_type: ListingType;
|
||||
lang: string;
|
||||
avatar?: string;
|
||||
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 {
|
||||
|
@ -797,6 +812,10 @@ export interface GetSiteConfig {
|
|||
auth?: string;
|
||||
}
|
||||
|
||||
export interface GetSiteForm {
|
||||
auth?: string;
|
||||
}
|
||||
|
||||
export interface GetSiteConfigResponse {
|
||||
config_hjson: string;
|
||||
}
|
||||
|
@ -812,6 +831,7 @@ export interface GetSiteResponse {
|
|||
banned: Array<UserView>;
|
||||
online: number;
|
||||
version: string;
|
||||
my_user?: User;
|
||||
}
|
||||
|
||||
export interface SiteResponse {
|
||||
|
@ -998,7 +1018,8 @@ type ResponseType =
|
|||
| AddAdminResponse
|
||||
| PrivateMessageResponse
|
||||
| PrivateMessagesResponse
|
||||
| GetSiteConfigResponse;
|
||||
| GetSiteConfigResponse
|
||||
| GetSiteResponse;
|
||||
|
||||
export interface WebSocketResponse {
|
||||
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 { User, LoginResponse } from '../interfaces';
|
||||
import { User, Claims, LoginResponse } from '../interfaces';
|
||||
import { setTheme } from '../utils';
|
||||
import jwt_decode from 'jwt-decode';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Subject, BehaviorSubject } from 'rxjs';
|
||||
|
||||
export class UserService {
|
||||
private static _instance: UserService;
|
||||
public user: User;
|
||||
public sub: Subject<{ user: User }> = new Subject<{
|
||||
user: User;
|
||||
}>();
|
||||
public claims: Claims;
|
||||
public jwtSub: Subject<string> = new Subject<string>();
|
||||
public unreadCountSub: BehaviorSubject<number> = new BehaviorSubject<number>(
|
||||
0
|
||||
);
|
||||
|
||||
private constructor() {
|
||||
let jwt = Cookies.get('jwt');
|
||||
if (jwt) {
|
||||
this.setUser(jwt);
|
||||
this.setClaims(jwt);
|
||||
} else {
|
||||
setTheme();
|
||||
console.log('No JWT cookie found.');
|
||||
|
@ -22,16 +24,17 @@ export class UserService {
|
|||
}
|
||||
|
||||
public login(res: LoginResponse) {
|
||||
this.setUser(res.jwt);
|
||||
this.setClaims(res.jwt);
|
||||
Cookies.set('jwt', res.jwt, { expires: 365 });
|
||||
console.log('jwt cookie set');
|
||||
}
|
||||
|
||||
public logout() {
|
||||
this.claims = undefined;
|
||||
this.user = undefined;
|
||||
Cookies.remove('jwt');
|
||||
setTheme();
|
||||
this.sub.next({ user: undefined });
|
||||
this.jwtSub.next(undefined);
|
||||
console.log('Logged out.');
|
||||
}
|
||||
|
||||
|
@ -39,11 +42,9 @@ export class UserService {
|
|||
return Cookies.get('jwt');
|
||||
}
|
||||
|
||||
private setUser(jwt: string) {
|
||||
this.user = jwt_decode(jwt);
|
||||
setTheme(this.user.theme, true);
|
||||
this.sub.next({ user: this.user });
|
||||
console.log(this.user);
|
||||
private setClaims(jwt: string) {
|
||||
this.claims = jwt_decode(jwt);
|
||||
this.jwtSub.next(jwt);
|
||||
}
|
||||
|
||||
public static get Instance() {
|
||||
|
|
6
ui/src/services/WebSocketService.ts
vendored
6
ui/src/services/WebSocketService.ts
vendored
|
@ -51,6 +51,7 @@ import {
|
|||
GetCommentsForm,
|
||||
UserJoinForm,
|
||||
GetSiteConfig,
|
||||
GetSiteForm,
|
||||
SiteConfigForm,
|
||||
MessageType,
|
||||
WebSocketJsonResponse,
|
||||
|
@ -316,8 +317,9 @@ export class WebSocketService {
|
|||
this.ws.send(this.wsSendWrapper(UserOperation.EditSite, siteForm));
|
||||
}
|
||||
|
||||
public getSite() {
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.GetSite, {}));
|
||||
public getSite(form: GetSiteForm = {}) {
|
||||
this.setAuth(form, false);
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.GetSite, form));
|
||||
}
|
||||
|
||||
public getSiteConfig() {
|
||||
|
|
Loading…
Reference in a new issue