Mostly working, before merge
This commit is contained in:
parent
103a92d6b6
commit
72798d1cc1
16 changed files with 450 additions and 14 deletions
|
@ -1 +1,2 @@
|
||||||
|
drop view reply_view;
|
||||||
drop view comment_view;
|
drop view comment_view;
|
||||||
|
|
|
@ -33,3 +33,28 @@ select
|
||||||
null as saved
|
null as saved
|
||||||
from all_comment ac
|
from all_comment ac
|
||||||
;
|
;
|
||||||
|
|
||||||
|
create view reply_view as
|
||||||
|
with closereply as (
|
||||||
|
select
|
||||||
|
c2.id,
|
||||||
|
c2.creator_id as sender_id,
|
||||||
|
c.creator_id as recipient_id
|
||||||
|
from comment c
|
||||||
|
inner join comment c2 on c.id = c2.parent_id
|
||||||
|
where c2.creator_id != c.creator_id
|
||||||
|
-- Do union where post is null
|
||||||
|
union
|
||||||
|
select
|
||||||
|
c.id,
|
||||||
|
c.creator_id as sender_id,
|
||||||
|
p.creator_id as recipient_id
|
||||||
|
from comment c, post p
|
||||||
|
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
|
||||||
|
)
|
||||||
|
select cv.*,
|
||||||
|
closereply.recipient_id
|
||||||
|
from comment_view cv, closereply
|
||||||
|
where closereply.id = cv.id
|
||||||
|
;
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ pub struct CommentForm {
|
||||||
pub parent_id: Option<i32>,
|
pub parent_id: Option<i32>,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub removed: Option<bool>,
|
pub removed: Option<bool>,
|
||||||
|
pub read: Option<bool>,
|
||||||
pub updated: Option<chrono::NaiveDateTime>
|
pub updated: Option<chrono::NaiveDateTime>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,6 +209,7 @@ mod tests {
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
post_id: inserted_post.id,
|
post_id: inserted_post.id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
read: None,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
@ -232,6 +234,7 @@ mod tests {
|
||||||
post_id: inserted_post.id,
|
post_id: inserted_post.id,
|
||||||
parent_id: Some(inserted_comment.id),
|
parent_id: Some(inserted_comment.id),
|
||||||
removed: None,
|
removed: None,
|
||||||
|
read: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -136,6 +136,107 @@ impl CommentView {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The faked schema since diesel doesn't do views
|
||||||
|
table! {
|
||||||
|
reply_view (id) {
|
||||||
|
id -> Int4,
|
||||||
|
creator_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
parent_id -> Nullable<Int4>,
|
||||||
|
content -> Text,
|
||||||
|
removed -> Bool,
|
||||||
|
read -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
community_id -> Int4,
|
||||||
|
banned -> Bool,
|
||||||
|
banned_from_community -> Bool,
|
||||||
|
creator_name -> Varchar,
|
||||||
|
score -> BigInt,
|
||||||
|
upvotes -> BigInt,
|
||||||
|
downvotes -> BigInt,
|
||||||
|
user_id -> Nullable<Int4>,
|
||||||
|
my_vote -> Nullable<Int4>,
|
||||||
|
saved -> Nullable<Bool>,
|
||||||
|
recipient_id -> Int4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
|
||||||
|
#[table_name="reply_view"]
|
||||||
|
pub struct ReplyView {
|
||||||
|
pub id: i32,
|
||||||
|
pub creator_id: i32,
|
||||||
|
pub post_id: i32,
|
||||||
|
pub parent_id: Option<i32>,
|
||||||
|
pub content: String,
|
||||||
|
pub removed: bool,
|
||||||
|
pub read: bool,
|
||||||
|
pub published: chrono::NaiveDateTime,
|
||||||
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub community_id: i32,
|
||||||
|
pub banned: bool,
|
||||||
|
pub banned_from_community: bool,
|
||||||
|
pub creator_name: String,
|
||||||
|
pub score: i64,
|
||||||
|
pub upvotes: i64,
|
||||||
|
pub downvotes: i64,
|
||||||
|
pub user_id: Option<i32>,
|
||||||
|
pub my_vote: Option<i32>,
|
||||||
|
pub saved: Option<bool>,
|
||||||
|
pub recipient_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReplyView {
|
||||||
|
|
||||||
|
pub fn get_replies(conn: &PgConnection,
|
||||||
|
for_user_id: i32,
|
||||||
|
sort: &SortType,
|
||||||
|
unread_only: bool,
|
||||||
|
page: Option<i64>,
|
||||||
|
limit: Option<i64>,
|
||||||
|
) -> Result<Vec<Self>, Error> {
|
||||||
|
use actions::comment_view::reply_view::dsl::*;
|
||||||
|
|
||||||
|
let (limit, offset) = limit_and_offset(page, limit);
|
||||||
|
|
||||||
|
let mut query = reply_view.into_boxed();
|
||||||
|
|
||||||
|
query = query
|
||||||
|
.filter(user_id.eq(for_user_id))
|
||||||
|
.filter(recipient_id.eq(for_user_id));
|
||||||
|
|
||||||
|
if unread_only {
|
||||||
|
query = query.filter(read.eq(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
query = match sort {
|
||||||
|
// SortType::Hot => query.order_by(hot_rank.desc()),
|
||||||
|
SortType::New => query.order_by(published.desc()),
|
||||||
|
SortType::TopAll => query.order_by(score.desc()),
|
||||||
|
SortType::TopYear => query
|
||||||
|
.filter(published.gt(now - 1.years()))
|
||||||
|
.order_by(score.desc()),
|
||||||
|
SortType::TopMonth => query
|
||||||
|
.filter(published.gt(now - 1.months()))
|
||||||
|
.order_by(score.desc()),
|
||||||
|
SortType::TopWeek => query
|
||||||
|
.filter(published.gt(now - 1.weeks()))
|
||||||
|
.order_by(score.desc()),
|
||||||
|
SortType::TopDay => query
|
||||||
|
.filter(published.gt(now - 1.days()))
|
||||||
|
.order_by(score.desc()),
|
||||||
|
_ => query.order_by(published.desc())
|
||||||
|
};
|
||||||
|
|
||||||
|
query
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
.load::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use establish_connection;
|
use establish_connection;
|
||||||
|
|
|
@ -26,7 +26,7 @@ use actions::moderator::*;
|
||||||
|
|
||||||
#[derive(EnumString,ToString,Debug)]
|
#[derive(EnumString,ToString,Debug)]
|
||||||
pub enum UserOperation {
|
pub enum UserOperation {
|
||||||
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser
|
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -215,6 +215,7 @@ pub struct EditComment {
|
||||||
post_id: i32,
|
post_id: i32,
|
||||||
removed: Option<bool>,
|
removed: Option<bool>,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
|
read: Option<bool>,
|
||||||
auth: String
|
auth: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,6 +440,21 @@ pub struct BanUserResponse {
|
||||||
banned: bool,
|
banned: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GetReplies {
|
||||||
|
sort: String,
|
||||||
|
page: Option<i64>,
|
||||||
|
limit: Option<i64>,
|
||||||
|
unread_only: bool,
|
||||||
|
auth: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GetRepliesResponse {
|
||||||
|
op: String,
|
||||||
|
replies: Vec<ReplyView>,
|
||||||
|
}
|
||||||
|
|
||||||
/// `ChatServer` manages chat rooms and responsible for coordinating chat
|
/// `ChatServer` manages chat rooms and responsible for coordinating chat
|
||||||
/// session. implementation is super primitive
|
/// session. implementation is super primitive
|
||||||
pub struct ChatServer {
|
pub struct ChatServer {
|
||||||
|
@ -671,6 +687,10 @@ impl Handler<StandardMessage> for ChatServer {
|
||||||
let ban_user: BanUser = serde_json::from_str(data).unwrap();
|
let ban_user: BanUser = serde_json::from_str(data).unwrap();
|
||||||
ban_user.perform(self, msg.id)
|
ban_user.perform(self, msg.id)
|
||||||
},
|
},
|
||||||
|
UserOperation::GetReplies => {
|
||||||
|
let get_replies: GetReplies = serde_json::from_str(data).unwrap();
|
||||||
|
get_replies.perform(self, msg.id)
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
MessageResult(res)
|
MessageResult(res)
|
||||||
|
@ -1181,6 +1201,7 @@ impl Perform for CreateComment {
|
||||||
post_id: self.post_id,
|
post_id: self.post_id,
|
||||||
creator_id: user_id,
|
creator_id: user_id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
read: None,
|
||||||
updated: None
|
updated: None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1292,6 +1313,7 @@ impl Perform for EditComment {
|
||||||
post_id: self.post_id,
|
post_id: self.post_id,
|
||||||
creator_id: self.creator_id,
|
creator_id: self.creator_id,
|
||||||
removed: self.removed.to_owned(),
|
removed: self.removed.to_owned(),
|
||||||
|
read: self.read.to_owned(),
|
||||||
updated: Some(naive_now())
|
updated: Some(naive_now())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2027,6 +2049,39 @@ impl Perform for GetModlog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Perform for GetReplies {
|
||||||
|
fn op_type(&self) -> UserOperation {
|
||||||
|
UserOperation::GetReplies
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> String {
|
||||||
|
|
||||||
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&self.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => {
|
||||||
|
return self.error("Not logged in.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let sort = SortType::from_str(&self.sort).expect("listing sort");
|
||||||
|
|
||||||
|
let replies = ReplyView::get_replies(&conn, user_id, &sort, self.unread_only, self.page, self.limit).unwrap();
|
||||||
|
|
||||||
|
// Return the jwt
|
||||||
|
serde_json::to_string(
|
||||||
|
&GetRepliesResponse {
|
||||||
|
op: self.op_type().to_string(),
|
||||||
|
replies: replies,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Perform for BanFromCommunity {
|
impl Perform for BanFromCommunity {
|
||||||
fn op_type(&self) -> UserOperation {
|
fn op_type(&self) -> UserOperation {
|
||||||
UserOperation::BanFromCommunity
|
UserOperation::BanFromCommunity
|
||||||
|
|
|
@ -25,6 +25,7 @@ interface CommentNodeProps {
|
||||||
noIndent?: boolean;
|
noIndent?: boolean;
|
||||||
viewOnly?: boolean;
|
viewOnly?: boolean;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
|
markable?: boolean;
|
||||||
moderators: Array<CommunityUser>;
|
moderators: Array<CommunityUser>;
|
||||||
admins: Array<UserView>;
|
admins: Array<UserView>;
|
||||||
}
|
}
|
||||||
|
@ -146,7 +147,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
}
|
}
|
||||||
{!this.props.node.comment.banned &&
|
{!this.props.node.comment.banned &&
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={linkEvent(this, this.addAdmin)}>{`${this.isAdmin ? 'remove' : 'appoint'} as admin`}</span>
|
<span class="pointer" onClick={linkEvent(this, this.handleAddAdmin)}>{`${this.isAdmin ? 'remove' : 'appoint'} as admin`}</span>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
@ -156,6 +157,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`} target="_blank">link</Link>
|
<Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`} target="_blank">link</Link>
|
||||||
</li>
|
</li>
|
||||||
|
{this.props.markable &&
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<span class="pointer" onClick={linkEvent(this, this.handleMarkRead)}>{`mark as ${node.comment.read ? 'unread' : 'read'}`}</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -309,6 +315,20 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMarkRead(i: CommentNode) {
|
||||||
|
let form: CommentFormI = {
|
||||||
|
content: i.props.node.comment.content,
|
||||||
|
edit_id: i.props.node.comment.id,
|
||||||
|
creator_id: i.props.node.comment.creator_id,
|
||||||
|
post_id: i.props.node.comment.post_id,
|
||||||
|
parent_id: i.props.node.comment.parent_id,
|
||||||
|
read: !i.props.node.comment.read,
|
||||||
|
auth: null
|
||||||
|
};
|
||||||
|
WebSocketService.Instance.editComment(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
handleModBanFromCommunityShow(i: CommentNode) {
|
handleModBanFromCommunityShow(i: CommentNode) {
|
||||||
i.state.showBanDialog = true;
|
i.state.showBanDialog = true;
|
||||||
i.state.banType = BanType.Community;
|
i.state.banType = BanType.Community;
|
||||||
|
@ -382,7 +402,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
addAdmin(i: CommentNode) {
|
handleAddAdmin(i: CommentNode) {
|
||||||
let form: AddAdminForm = {
|
let form: AddAdminForm = {
|
||||||
user_id: i.props.node.comment.creator_id,
|
user_id: i.props.node.comment.creator_id,
|
||||||
added: !i.isAdmin,
|
added: !i.isAdmin,
|
||||||
|
|
|
@ -12,6 +12,7 @@ interface CommentNodesProps {
|
||||||
noIndent?: boolean;
|
noIndent?: boolean;
|
||||||
viewOnly?: boolean;
|
viewOnly?: boolean;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
|
markable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> {
|
export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> {
|
||||||
|
@ -30,6 +31,7 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
|
||||||
locked={this.props.locked}
|
locked={this.props.locked}
|
||||||
moderators={this.props.moderators}
|
moderators={this.props.moderators}
|
||||||
admins={this.props.admins}
|
admins={this.props.admins}
|
||||||
|
markable={this.props.markable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
177
ui/src/components/inbox.tsx
Normal file
177
ui/src/components/inbox.tsx
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { Link } from 'inferno-router';
|
||||||
|
import { Subscription } from "rxjs";
|
||||||
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
|
import { UserOperation, Comment, SortType, GetRepliesForm, GetRepliesResponse, CommentResponse } from '../interfaces';
|
||||||
|
import { WebSocketService, UserService } from '../services';
|
||||||
|
import { msgOp } from '../utils';
|
||||||
|
import { CommentNodes } from './comment-nodes';
|
||||||
|
|
||||||
|
enum UnreadType {
|
||||||
|
Unread, All
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InboxState {
|
||||||
|
unreadType: UnreadType;
|
||||||
|
replies: Array<Comment>;
|
||||||
|
sort: SortType;
|
||||||
|
page: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Inbox extends Component<any, InboxState> {
|
||||||
|
|
||||||
|
private subscription: Subscription;
|
||||||
|
private emptyState: InboxState = {
|
||||||
|
unreadType: UnreadType.Unread,
|
||||||
|
replies: [],
|
||||||
|
sort: SortType.New,
|
||||||
|
page: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props: any, context: any) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = this.emptyState;
|
||||||
|
|
||||||
|
this.subscription = WebSocketService.Instance.subject
|
||||||
|
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||||
|
.subscribe(
|
||||||
|
(msg) => this.parseMessage(msg),
|
||||||
|
(err) => console.error(err),
|
||||||
|
() => console.log('complete')
|
||||||
|
);
|
||||||
|
|
||||||
|
this.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let user = UserService.Instance.user;
|
||||||
|
return (
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5>Inbox for <Link to={`/user/${user.id}`}>{user.username}</Link></h5>
|
||||||
|
{this.selects()}
|
||||||
|
{this.replies()}
|
||||||
|
{this.paginator()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
selects() {
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<select value={this.state.unreadType} onChange={linkEvent(this, this.handleUnreadTypeChange)} class="custom-select w-auto">
|
||||||
|
<option disabled>Type</option>
|
||||||
|
<option value={UnreadType.Unread}>Unread</option>
|
||||||
|
<option value={UnreadType.All}>All</option>
|
||||||
|
</select>
|
||||||
|
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto ml-2">
|
||||||
|
<option disabled>Sort Type</option>
|
||||||
|
<option value={SortType.New}>New</option>
|
||||||
|
<option value={SortType.TopDay}>Top Day</option>
|
||||||
|
<option value={SortType.TopWeek}>Week</option>
|
||||||
|
<option value={SortType.TopMonth}>Month</option>
|
||||||
|
<option value={SortType.TopYear}>Year</option>
|
||||||
|
<option value={SortType.TopAll}>All</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
replies() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{this.state.replies.map(reply =>
|
||||||
|
<CommentNodes nodes={[{comment: reply}]} noIndent viewOnly markable />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
paginator() {
|
||||||
|
return (
|
||||||
|
<div class="mt-2">
|
||||||
|
{this.state.page > 1 &&
|
||||||
|
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button>
|
||||||
|
}
|
||||||
|
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPage(i: Inbox) {
|
||||||
|
i.state.page++;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
prevPage(i: Inbox) {
|
||||||
|
i.state.page--;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUnreadTypeChange(i: Inbox, event: any) {
|
||||||
|
i.state.unreadType = Number(event.target.value);
|
||||||
|
i.state.page = 1;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
refetch() {
|
||||||
|
let form: GetRepliesForm = {
|
||||||
|
sort: SortType[this.state.sort],
|
||||||
|
unread_only: (this.state.unreadType == UnreadType.Unread),
|
||||||
|
page: this.state.page,
|
||||||
|
limit: 9999,
|
||||||
|
};
|
||||||
|
WebSocketService.Instance.getReplies(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSortChange(i: Inbox, event: any) {
|
||||||
|
i.state.sort = Number(event.target.value);
|
||||||
|
i.state.page = 1;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
parseMessage(msg: any) {
|
||||||
|
console.log(msg);
|
||||||
|
let op: UserOperation = msgOp(msg);
|
||||||
|
if (msg.error) {
|
||||||
|
alert(msg.error);
|
||||||
|
return;
|
||||||
|
} else if (op == UserOperation.GetReplies) {
|
||||||
|
let res: GetRepliesResponse = msg;
|
||||||
|
this.state.replies = res.replies;
|
||||||
|
this.sendRepliesCount();
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (op == UserOperation.EditComment) {
|
||||||
|
let res: CommentResponse = msg;
|
||||||
|
|
||||||
|
// If youre in the unread view, just remove it from the list
|
||||||
|
if (this.state.unreadType == UnreadType.Unread && res.comment.read) {
|
||||||
|
this.state.replies = this.state.replies.filter(r => r.id !== res.comment.id);
|
||||||
|
} else {
|
||||||
|
let found = this.state.replies.find(c => c.id == res.comment.id);
|
||||||
|
found.read = res.comment.read;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendRepliesCount();
|
||||||
|
this.setState(this.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRepliesCount() {
|
||||||
|
UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: this.state.replies.filter(r => !r.read).length});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Component } from 'inferno';
|
||||||
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';
|
||||||
import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse } from '../interfaces';
|
import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse, GetRepliesResponse, GetRepliesForm } from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { PostListings } from './post-listings';
|
import { PostListings } from './post-listings';
|
||||||
import { msgOp, repoUrl, mdToHtml } from '../utils';
|
import { msgOp, repoUrl, mdToHtml } from '../utils';
|
||||||
|
@ -55,6 +55,15 @@ export class Main extends Component<any, State> {
|
||||||
|
|
||||||
if (UserService.Instance.user) {
|
if (UserService.Instance.user) {
|
||||||
WebSocketService.Instance.getFollowedCommunities();
|
WebSocketService.Instance.getFollowedCommunities();
|
||||||
|
|
||||||
|
// Get replies for the count
|
||||||
|
let repliesForm: GetRepliesForm = {
|
||||||
|
sort: SortType[SortType.New],
|
||||||
|
unread_only: true,
|
||||||
|
page: 1,
|
||||||
|
limit: 9999,
|
||||||
|
};
|
||||||
|
WebSocketService.Instance.getReplies(repliesForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
let listCommunitiesForm: ListCommunitiesForm = {
|
let listCommunitiesForm: ListCommunitiesForm = {
|
||||||
|
@ -176,7 +185,14 @@ export class Main extends Component<any, State> {
|
||||||
this.state.site.site = res.site;
|
this.state.site.site = res.site;
|
||||||
this.state.site.banned = res.banned;
|
this.state.site.banned = res.banned;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
} else if (op == UserOperation.GetReplies) {
|
||||||
|
let res: GetRepliesResponse = msg;
|
||||||
|
this.sendRepliesCount(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendRepliesCount(res: GetRepliesResponse) {
|
||||||
|
UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: res.replies.filter(r => !r.read).length});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,14 @@ interface NavbarState {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
expandUserDropdown: boolean;
|
expandUserDropdown: boolean;
|
||||||
|
unreadCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Navbar extends Component<any, NavbarState> {
|
export class Navbar extends Component<any, NavbarState> {
|
||||||
|
|
||||||
emptyState: NavbarState = {
|
emptyState: NavbarState = {
|
||||||
isLoggedIn: UserService.Instance.user !== undefined,
|
isLoggedIn: (UserService.Instance.user !== undefined),
|
||||||
|
unreadCount: 0,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
expandUserDropdown: false
|
expandUserDropdown: false
|
||||||
}
|
}
|
||||||
|
@ -24,8 +26,9 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
|
|
||||||
// Subscribe to user changes
|
// Subscribe to user changes
|
||||||
UserService.Instance.sub.subscribe(user => {
|
UserService.Instance.sub.subscribe(user => {
|
||||||
let loggedIn: boolean = user !== undefined;
|
this.state.isLoggedIn = user.user !== undefined;
|
||||||
this.setState({isLoggedIn: loggedIn});
|
this.state.unreadCount = user.unreadCount;
|
||||||
|
this.setState(this.state);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,9 +68,13 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
<ul class="navbar-nav ml-auto mr-2">
|
<ul class="navbar-nav ml-auto mr-2">
|
||||||
{this.state.isLoggedIn ?
|
{this.state.isLoggedIn ?
|
||||||
<>
|
<>
|
||||||
|
{
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<Link class="nav-link" to="/communities">🖂</Link>
|
<Link class="nav-link" to="/inbox">🖂
|
||||||
|
{this.state.unreadCount> 0 && <span class="badge badge-light">{this.state.unreadCount}</span>}
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
}
|
||||||
<li className={`nav-item dropdown ${this.state.expandUserDropdown && 'show'}`}>
|
<li className={`nav-item dropdown ${this.state.expandUserDropdown && 'show'}`}>
|
||||||
<a class="pointer nav-link dropdown-toggle" onClick={linkEvent(this, this.expandUserDropdown)} role="button">
|
<a class="pointer nav-link dropdown-toggle" onClick={linkEvent(this, this.expandUserDropdown)} role="button">
|
||||||
{UserService.Instance.user.username}
|
{UserService.Instance.user.username}
|
||||||
|
@ -95,6 +102,7 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
handleLogoutClick(i: Navbar) {
|
handleLogoutClick(i: Navbar) {
|
||||||
i.state.expandUserDropdown = false;
|
i.state.expandUserDropdown = false;
|
||||||
UserService.Instance.logout();
|
UserService.Instance.logout();
|
||||||
|
i.context.router.history.push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOverviewClick(i: Navbar) {
|
handleOverviewClick(i: Navbar) {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { CommentForm } from './comment-form';
|
||||||
import { CommentNodes } from './comment-nodes';
|
import { CommentNodes } from './comment-nodes';
|
||||||
import * as autosize from 'autosize';
|
import * as autosize from 'autosize';
|
||||||
|
|
||||||
|
|
||||||
interface PostState {
|
interface PostState {
|
||||||
post: PostI;
|
post: PostI;
|
||||||
comments: Array<Comment>;
|
comments: Array<Comment>;
|
||||||
|
|
|
@ -82,3 +82,10 @@ blockquote {
|
||||||
margin: 0.5em 5px;
|
margin: 0.5em 5px;
|
||||||
padding: 0.1em 5px;
|
padding: 0.1em 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-notify{
|
||||||
|
/* background:red; */
|
||||||
|
position:relative;
|
||||||
|
top: -20px;
|
||||||
|
left: -35px;
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { Communities } from './components/communities';
|
||||||
import { User } from './components/user';
|
import { User } from './components/user';
|
||||||
import { Modlog } from './components/modlog';
|
import { Modlog } from './components/modlog';
|
||||||
import { Setup } from './components/setup';
|
import { Setup } from './components/setup';
|
||||||
|
import { Inbox } from './components/inbox';
|
||||||
import { Symbols } from './components/symbols';
|
import { Symbols } from './components/symbols';
|
||||||
|
|
||||||
import './css/bootstrap.min.css';
|
import './css/bootstrap.min.css';
|
||||||
|
@ -46,6 +47,7 @@ class Index extends Component<any, any> {
|
||||||
<Route path={`/community/:id`} component={Community} />
|
<Route path={`/community/:id`} component={Community} />
|
||||||
<Route path={`/user/:id/:heading`} component={User} />
|
<Route path={`/user/:id/:heading`} component={User} />
|
||||||
<Route path={`/user/:id`} component={User} />
|
<Route path={`/user/:id`} component={User} />
|
||||||
|
<Route path={`/inbox`} component={Inbox} />
|
||||||
<Route path={`/modlog/community/:community_id`} component={Modlog} />
|
<Route path={`/modlog/community/:community_id`} component={Modlog} />
|
||||||
<Route path={`/modlog`} component={Modlog} />
|
<Route path={`/modlog`} component={Modlog} />
|
||||||
<Route path={`/setup`} component={Setup} />
|
<Route path={`/setup`} component={Setup} />
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export enum UserOperation {
|
export enum UserOperation {
|
||||||
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser
|
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CommentSortType {
|
export enum CommentSortType {
|
||||||
|
@ -153,6 +153,19 @@ export interface UserDetailsResponse {
|
||||||
posts: Array<Post>;
|
posts: Array<Post>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetRepliesForm {
|
||||||
|
sort: string; // TODO figure this one out
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
unread_only: boolean;
|
||||||
|
auth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetRepliesResponse {
|
||||||
|
op: string;
|
||||||
|
replies: Array<Comment>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BanFromCommunityForm {
|
export interface BanFromCommunityForm {
|
||||||
community_id: number;
|
community_id: number;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
|
@ -404,6 +417,7 @@ export interface CommentForm {
|
||||||
creator_id: number;
|
creator_id: number;
|
||||||
removed?: boolean;
|
removed?: boolean;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
|
read?: boolean;
|
||||||
auth: string;
|
auth: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,10 @@ import * as jwt_decode from 'jwt-decode';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } 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> = new Subject<User>();
|
public sub: Subject<{user: User, unreadCount: number}> = new Subject<{user: User, unreadCount: number}>();
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
let jwt = Cookies.get("jwt");
|
let jwt = Cookies.get("jwt");
|
||||||
|
@ -28,7 +29,7 @@ export class UserService {
|
||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
Cookies.remove("jwt");
|
Cookies.remove("jwt");
|
||||||
console.log("Logged out.");
|
console.log("Logged out.");
|
||||||
this.sub.next(undefined);
|
this.sub.next({user: undefined, unreadCount: 0});
|
||||||
}
|
}
|
||||||
|
|
||||||
public get auth(): string {
|
public get auth(): string {
|
||||||
|
@ -37,7 +38,7 @@ export class UserService {
|
||||||
|
|
||||||
private setUser(jwt: string) {
|
private setUser(jwt: string) {
|
||||||
this.user = jwt_decode(jwt);
|
this.user = jwt_decode(jwt);
|
||||||
this.sub.next(this.user);
|
this.sub.next({user: this.user, unreadCount: 0});
|
||||||
console.log(this.user);
|
console.log(this.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { wsUri } from '../env';
|
import { wsUri } from '../env';
|
||||||
import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, SavePostForm, CommentForm, SaveCommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, AddAdminForm, BanUserForm, SiteForm, Site, UserView } from '../interfaces';
|
import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, SavePostForm, CommentForm, SaveCommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, AddAdminForm, BanUserForm, SiteForm, Site, UserView, GetRepliesForm } from '../interfaces';
|
||||||
import { webSocket } from 'rxjs/webSocket';
|
import { webSocket } from 'rxjs/webSocket';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||||
|
@ -145,6 +145,11 @@ export class WebSocketService {
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form));
|
this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getReplies(form: GetRepliesForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.subject.next(this.wsSendWrapper(UserOperation.GetReplies, form));
|
||||||
|
}
|
||||||
|
|
||||||
public getModlog(form: GetModlogForm) {
|
public getModlog(form: GetModlogForm) {
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, form));
|
this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, form));
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue