parent
5547ecdeaf
commit
02dd9ac32a
21 changed files with 1151 additions and 57 deletions
2
server/migrations/2019-10-19-052737_create_user_mention/down.sql
vendored
Normal file
2
server/migrations/2019-10-19-052737_create_user_mention/down.sql
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
drop view user_mention_view;
|
||||||
|
drop table user_mention;
|
35
server/migrations/2019-10-19-052737_create_user_mention/up.sql
vendored
Normal file
35
server/migrations/2019-10-19-052737_create_user_mention/up.sql
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
create table user_mention (
|
||||||
|
id serial primary key,
|
||||||
|
recipient_id int references user_ on update cascade on delete cascade not null,
|
||||||
|
comment_id int references comment on update cascade on delete cascade not null,
|
||||||
|
read boolean default false not null,
|
||||||
|
published timestamp not null default now(),
|
||||||
|
unique(recipient_id, comment_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create view user_mention_view as
|
||||||
|
select
|
||||||
|
c.id,
|
||||||
|
um.id as user_mention_id,
|
||||||
|
c.creator_id,
|
||||||
|
c.post_id,
|
||||||
|
c.parent_id,
|
||||||
|
c.content,
|
||||||
|
c.removed,
|
||||||
|
um.read,
|
||||||
|
c.published,
|
||||||
|
c.updated,
|
||||||
|
c.deleted,
|
||||||
|
c.community_id,
|
||||||
|
c.banned,
|
||||||
|
c.banned_from_community,
|
||||||
|
c.creator_name,
|
||||||
|
c.score,
|
||||||
|
c.upvotes,
|
||||||
|
c.downvotes,
|
||||||
|
c.user_id,
|
||||||
|
c.my_vote,
|
||||||
|
c.saved,
|
||||||
|
um.recipient_id
|
||||||
|
from user_mention um, comment_view c
|
||||||
|
where um.comment_id = c.id;
|
|
@ -85,6 +85,35 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment"))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Scan the comment for user mentions, add those rows
|
||||||
|
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||||
|
|
||||||
|
for username_mention in &extracted_usernames {
|
||||||
|
let mention_user = User_::read_from_name(&conn, username_mention.to_string());
|
||||||
|
|
||||||
|
if mention_user.is_ok() {
|
||||||
|
let mention_user_id = mention_user?.id;
|
||||||
|
|
||||||
|
// You can't mention yourself
|
||||||
|
// At some point, make it so you can't tag the parent creator either
|
||||||
|
// This can cause two notifications, one for reply and the other for mention
|
||||||
|
if mention_user_id != user_id {
|
||||||
|
let user_mention_form = UserMentionForm {
|
||||||
|
recipient_id: mention_user_id,
|
||||||
|
comment_id: inserted_comment.id,
|
||||||
|
read: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow this to fail softly, since comment edits might re-update or replace it
|
||||||
|
// Let the uniqueness handle this fail
|
||||||
|
match UserMention::create(&conn, &user_mention_form) {
|
||||||
|
Ok(_mention) => (),
|
||||||
|
Err(_e) => eprintln!("{}", &_e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// You like your own comment by default
|
// You like your own comment by default
|
||||||
let like_form = CommentLikeForm {
|
let like_form = CommentLikeForm {
|
||||||
comment_id: inserted_comment.id,
|
comment_id: inserted_comment.id,
|
||||||
|
@ -170,6 +199,35 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Scan the comment for user mentions, add those rows
|
||||||
|
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||||
|
|
||||||
|
for username_mention in &extracted_usernames {
|
||||||
|
let mention_user = User_::read_from_name(&conn, username_mention.to_string());
|
||||||
|
|
||||||
|
if mention_user.is_ok() {
|
||||||
|
let mention_user_id = mention_user?.id;
|
||||||
|
|
||||||
|
// You can't mention yourself
|
||||||
|
// At some point, make it so you can't tag the parent creator either
|
||||||
|
// This can cause two notifications, one for reply and the other for mention
|
||||||
|
if mention_user_id != user_id {
|
||||||
|
let user_mention_form = UserMentionForm {
|
||||||
|
recipient_id: mention_user_id,
|
||||||
|
comment_id: data.edit_id,
|
||||||
|
read: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow this to fail softly, since comment edits might re-update or replace it
|
||||||
|
// Let the uniqueness handle this fail
|
||||||
|
match UserMention::create(&conn, &user_mention_form) {
|
||||||
|
Ok(_mention) => (),
|
||||||
|
Err(_e) => eprintln!("{}", &_e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
if let Some(removed) = data.removed.to_owned() {
|
if let Some(removed) = data.removed.to_owned() {
|
||||||
let form = ModRemoveCommentForm {
|
let form = ModRemoveCommentForm {
|
||||||
|
|
|
@ -8,9 +8,11 @@ use crate::db::moderator_views::*;
|
||||||
use crate::db::post::*;
|
use crate::db::post::*;
|
||||||
use crate::db::post_view::*;
|
use crate::db::post_view::*;
|
||||||
use crate::db::user::*;
|
use crate::db::user::*;
|
||||||
|
use crate::db::user_mention::*;
|
||||||
|
use crate::db::user_mention_view::*;
|
||||||
use crate::db::user_view::*;
|
use crate::db::user_view::*;
|
||||||
use crate::db::*;
|
use crate::db::*;
|
||||||
use crate::{has_slurs, naive_from_unix, naive_now, remove_slurs, Settings};
|
use crate::{extract_usernames, has_slurs, naive_from_unix, naive_now, remove_slurs, Settings};
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -43,6 +45,8 @@ pub enum UserOperation {
|
||||||
GetFollowedCommunities,
|
GetFollowedCommunities,
|
||||||
GetUserDetails,
|
GetUserDetails,
|
||||||
GetReplies,
|
GetReplies,
|
||||||
|
GetUserMentions,
|
||||||
|
EditUserMention,
|
||||||
GetModlog,
|
GetModlog,
|
||||||
BanFromCommunity,
|
BanFromCommunity,
|
||||||
AddModToCommunity,
|
AddModToCommunity,
|
||||||
|
|
|
@ -60,6 +60,12 @@ pub struct GetRepliesResponse {
|
||||||
replies: Vec<ReplyView>,
|
replies: Vec<ReplyView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GetUserMentionsResponse {
|
||||||
|
op: String,
|
||||||
|
mentions: Vec<UserMentionView>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct MarkAllAsRead {
|
pub struct MarkAllAsRead {
|
||||||
auth: String,
|
auth: String,
|
||||||
|
@ -103,6 +109,28 @@ pub struct GetReplies {
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GetUserMentions {
|
||||||
|
sort: String,
|
||||||
|
page: Option<i64>,
|
||||||
|
limit: Option<i64>,
|
||||||
|
unread_only: bool,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct EditUserMention {
|
||||||
|
user_mention_id: i32,
|
||||||
|
read: Option<bool>,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct UserMentionResponse {
|
||||||
|
op: String,
|
||||||
|
mention: UserMentionView,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct DeleteAccount {
|
pub struct DeleteAccount {
|
||||||
password: String,
|
password: String,
|
||||||
|
@ -299,7 +327,6 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
//TODO add save
|
|
||||||
let sort = SortType::from_str(&data.sort)?;
|
let sort = SortType::from_str(&data.sort)?;
|
||||||
|
|
||||||
let user_details_id = match data.user_id {
|
let user_details_id = match data.user_id {
|
||||||
|
@ -541,7 +568,6 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
||||||
data.limit,
|
data.limit,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Return the jwt
|
|
||||||
Ok(GetRepliesResponse {
|
Ok(GetRepliesResponse {
|
||||||
op: self.op.to_string(),
|
op: self.op.to_string(),
|
||||||
replies: replies,
|
replies: replies,
|
||||||
|
@ -549,6 +575,71 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
|
||||||
|
fn perform(&self) -> Result<GetUserMentionsResponse, Error> {
|
||||||
|
let data: &GetUserMentions = &self.data;
|
||||||
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let sort = SortType::from_str(&data.sort)?;
|
||||||
|
|
||||||
|
let mentions = UserMentionView::get_mentions(
|
||||||
|
&conn,
|
||||||
|
user_id,
|
||||||
|
&sort,
|
||||||
|
data.unread_only,
|
||||||
|
data.page,
|
||||||
|
data.limit,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(GetUserMentionsResponse {
|
||||||
|
op: self.op.to_string(),
|
||||||
|
mentions: mentions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Perform<UserMentionResponse> for Oper<EditUserMention> {
|
||||||
|
fn perform(&self) -> Result<UserMentionResponse, Error> {
|
||||||
|
let data: &EditUserMention = &self.data;
|
||||||
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let user_mention = UserMention::read(&conn, data.user_mention_id)?;
|
||||||
|
|
||||||
|
let user_mention_form = UserMentionForm {
|
||||||
|
recipient_id: user_id,
|
||||||
|
comment_id: user_mention.comment_id,
|
||||||
|
read: data.read.to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _updated_user_mention =
|
||||||
|
match UserMention::update(&conn, user_mention.id, &user_mention_form) {
|
||||||
|
Ok(comment) => comment,
|
||||||
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?;
|
||||||
|
|
||||||
|
Ok(UserMentionResponse {
|
||||||
|
op: self.op.to_string(),
|
||||||
|
mention: user_mention_view,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||||
fn perform(&self) -> Result<GetRepliesResponse, Error> {
|
fn perform(&self) -> Result<GetRepliesResponse, Error> {
|
||||||
let data: &MarkAllAsRead = &self.data;
|
let data: &MarkAllAsRead = &self.data;
|
||||||
|
@ -581,11 +672,27 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let replies = ReplyView::get_replies(&conn, user_id, &SortType::New, true, Some(1), Some(999))?;
|
// Mentions
|
||||||
|
let mentions =
|
||||||
|
UserMentionView::get_mentions(&conn, user_id, &SortType::New, true, Some(1), Some(999))?;
|
||||||
|
|
||||||
|
for mention in &mentions {
|
||||||
|
let mention_form = UserMentionForm {
|
||||||
|
recipient_id: mention.to_owned().recipient_id,
|
||||||
|
comment_id: mention.to_owned().id,
|
||||||
|
read: Some(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _updated_mention =
|
||||||
|
match UserMention::update(&conn, mention.user_mention_id, &mention_form) {
|
||||||
|
Ok(mention) => mention,
|
||||||
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Ok(GetRepliesResponse {
|
Ok(GetRepliesResponse {
|
||||||
op: self.op.to_string(),
|
op: self.op.to_string(),
|
||||||
replies: replies,
|
replies: vec![],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,6 @@ impl CommentView {
|
||||||
|
|
||||||
let (limit, offset) = limit_and_offset(page, limit);
|
let (limit, offset) = limit_and_offset(page, limit);
|
||||||
|
|
||||||
// TODO no limits here?
|
|
||||||
let mut query = comment_view.into_boxed();
|
let mut query = comment_view.into_boxed();
|
||||||
|
|
||||||
// The view lets you pass a null user_id, if you're not logged in
|
// The view lets you pass a null user_id, if you're not logged in
|
||||||
|
|
|
@ -14,6 +14,8 @@ pub mod moderator_views;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod post_view;
|
pub mod post_view;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
pub mod user_mention;
|
||||||
|
pub mod user_mention_view;
|
||||||
pub mod user_view;
|
pub mod user_view;
|
||||||
|
|
||||||
pub trait Crud<T> {
|
pub trait Crud<T> {
|
||||||
|
|
345
server/src/db/src/schema.rs
Normal file
345
server/src/db/src/schema.rs
Normal file
|
@ -0,0 +1,345 @@
|
||||||
|
table! {
|
||||||
|
category (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Varchar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
comment (id) {
|
||||||
|
id -> Int4,
|
||||||
|
creator_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
parent_id -> Nullable<Int4>,
|
||||||
|
content -> Text,
|
||||||
|
removed -> Bool,
|
||||||
|
read -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
comment_like (id) {
|
||||||
|
id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
comment_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
score -> Int2,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
comment_saved (id) {
|
||||||
|
id -> Int4,
|
||||||
|
comment_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
community (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Varchar,
|
||||||
|
title -> Varchar,
|
||||||
|
description -> Nullable<Text>,
|
||||||
|
category_id -> Int4,
|
||||||
|
creator_id -> Int4,
|
||||||
|
removed -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
|
nsfw -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
community_follower (id) {
|
||||||
|
id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
community_moderator (id) {
|
||||||
|
id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
community_user_ban (id) {
|
||||||
|
id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_add (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
other_user_id -> Int4,
|
||||||
|
removed -> Nullable<Bool>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_add_community (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
other_user_id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
removed -> Nullable<Bool>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_ban (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
other_user_id -> Int4,
|
||||||
|
reason -> Nullable<Text>,
|
||||||
|
banned -> Nullable<Bool>,
|
||||||
|
expires -> Nullable<Timestamp>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_ban_from_community (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
other_user_id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
reason -> Nullable<Text>,
|
||||||
|
banned -> Nullable<Bool>,
|
||||||
|
expires -> Nullable<Timestamp>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_lock_post (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
locked -> Nullable<Bool>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_remove_comment (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
comment_id -> Int4,
|
||||||
|
reason -> Nullable<Text>,
|
||||||
|
removed -> Nullable<Bool>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_remove_community (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
reason -> Nullable<Text>,
|
||||||
|
removed -> Nullable<Bool>,
|
||||||
|
expires -> Nullable<Timestamp>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_remove_post (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
reason -> Nullable<Text>,
|
||||||
|
removed -> Nullable<Bool>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
mod_sticky_post (id) {
|
||||||
|
id -> Int4,
|
||||||
|
mod_user_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
stickied -> Nullable<Bool>,
|
||||||
|
when_ -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
post (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Varchar,
|
||||||
|
url -> Nullable<Text>,
|
||||||
|
body -> Nullable<Text>,
|
||||||
|
creator_id -> Int4,
|
||||||
|
community_id -> Int4,
|
||||||
|
removed -> Bool,
|
||||||
|
locked -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
|
nsfw -> Bool,
|
||||||
|
stickied -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
post_like (id) {
|
||||||
|
id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
score -> Int2,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
post_read (id) {
|
||||||
|
id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
post_saved (id) {
|
||||||
|
id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
site (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Varchar,
|
||||||
|
description -> Nullable<Text>,
|
||||||
|
creator_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
user_ (id) {
|
||||||
|
id -> Int4,
|
||||||
|
name -> Varchar,
|
||||||
|
fedi_name -> Varchar,
|
||||||
|
preferred_username -> Nullable<Varchar>,
|
||||||
|
password_encrypted -> Text,
|
||||||
|
email -> Nullable<Text>,
|
||||||
|
icon -> Nullable<Bytea>,
|
||||||
|
admin -> Bool,
|
||||||
|
banned -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
show_nsfw -> Bool,
|
||||||
|
theme -> Varchar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
user_ban (id) {
|
||||||
|
id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
user_mention (id) {
|
||||||
|
id -> Int4,
|
||||||
|
recipient_id -> Int4,
|
||||||
|
comment_id -> Int4,
|
||||||
|
read -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
joinable!(comment -> post (post_id));
|
||||||
|
joinable!(comment -> user_ (creator_id));
|
||||||
|
joinable!(comment_like -> comment (comment_id));
|
||||||
|
joinable!(comment_like -> post (post_id));
|
||||||
|
joinable!(comment_like -> user_ (user_id));
|
||||||
|
joinable!(comment_saved -> comment (comment_id));
|
||||||
|
joinable!(comment_saved -> user_ (user_id));
|
||||||
|
joinable!(community -> category (category_id));
|
||||||
|
joinable!(community -> user_ (creator_id));
|
||||||
|
joinable!(community_follower -> community (community_id));
|
||||||
|
joinable!(community_follower -> user_ (user_id));
|
||||||
|
joinable!(community_moderator -> community (community_id));
|
||||||
|
joinable!(community_moderator -> user_ (user_id));
|
||||||
|
joinable!(community_user_ban -> community (community_id));
|
||||||
|
joinable!(community_user_ban -> user_ (user_id));
|
||||||
|
joinable!(mod_add_community -> community (community_id));
|
||||||
|
joinable!(mod_ban_from_community -> community (community_id));
|
||||||
|
joinable!(mod_lock_post -> post (post_id));
|
||||||
|
joinable!(mod_lock_post -> user_ (mod_user_id));
|
||||||
|
joinable!(mod_remove_comment -> comment (comment_id));
|
||||||
|
joinable!(mod_remove_comment -> user_ (mod_user_id));
|
||||||
|
joinable!(mod_remove_community -> community (community_id));
|
||||||
|
joinable!(mod_remove_community -> user_ (mod_user_id));
|
||||||
|
joinable!(mod_remove_post -> post (post_id));
|
||||||
|
joinable!(mod_remove_post -> user_ (mod_user_id));
|
||||||
|
joinable!(mod_sticky_post -> post (post_id));
|
||||||
|
joinable!(mod_sticky_post -> user_ (mod_user_id));
|
||||||
|
joinable!(post -> community (community_id));
|
||||||
|
joinable!(post -> user_ (creator_id));
|
||||||
|
joinable!(post_like -> post (post_id));
|
||||||
|
joinable!(post_like -> user_ (user_id));
|
||||||
|
joinable!(post_read -> post (post_id));
|
||||||
|
joinable!(post_read -> user_ (user_id));
|
||||||
|
joinable!(post_saved -> post (post_id));
|
||||||
|
joinable!(post_saved -> user_ (user_id));
|
||||||
|
joinable!(site -> user_ (creator_id));
|
||||||
|
joinable!(user_ban -> user_ (user_id));
|
||||||
|
joinable!(user_mention -> comment (comment_id));
|
||||||
|
joinable!(user_mention -> user_ (recipient_id));
|
||||||
|
|
||||||
|
allow_tables_to_appear_in_same_query!(
|
||||||
|
category,
|
||||||
|
comment,
|
||||||
|
comment_like,
|
||||||
|
comment_saved,
|
||||||
|
community,
|
||||||
|
community_follower,
|
||||||
|
community_moderator,
|
||||||
|
community_user_ban,
|
||||||
|
mod_add,
|
||||||
|
mod_add_community,
|
||||||
|
mod_ban,
|
||||||
|
mod_ban_from_community,
|
||||||
|
mod_lock_post,
|
||||||
|
mod_remove_comment,
|
||||||
|
mod_remove_community,
|
||||||
|
mod_remove_post,
|
||||||
|
mod_sticky_post,
|
||||||
|
post,
|
||||||
|
post_like,
|
||||||
|
post_read,
|
||||||
|
post_saved,
|
||||||
|
site,
|
||||||
|
user_,
|
||||||
|
user_ban,
|
||||||
|
user_mention,
|
||||||
|
);
|
169
server/src/db/user_mention.rs
Normal file
169
server/src/db/user_mention.rs
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
use super::comment::Comment;
|
||||||
|
use super::*;
|
||||||
|
use crate::schema::user_mention;
|
||||||
|
|
||||||
|
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
|
#[belongs_to(Comment)]
|
||||||
|
#[table_name = "user_mention"]
|
||||||
|
pub struct UserMention {
|
||||||
|
pub id: i32,
|
||||||
|
pub recipient_id: i32,
|
||||||
|
pub comment_id: i32,
|
||||||
|
pub read: bool,
|
||||||
|
pub published: chrono::NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, AsChangeset, Clone)]
|
||||||
|
#[table_name = "user_mention"]
|
||||||
|
pub struct UserMentionForm {
|
||||||
|
pub recipient_id: i32,
|
||||||
|
pub comment_id: i32,
|
||||||
|
pub read: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Crud<UserMentionForm> for UserMention {
|
||||||
|
fn read(conn: &PgConnection, user_mention_id: i32) -> Result<Self, Error> {
|
||||||
|
use crate::schema::user_mention::dsl::*;
|
||||||
|
user_mention.find(user_mention_id).first::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(conn: &PgConnection, user_mention_id: i32) -> Result<usize, Error> {
|
||||||
|
use crate::schema::user_mention::dsl::*;
|
||||||
|
diesel::delete(user_mention.find(user_mention_id)).execute(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create(conn: &PgConnection, user_mention_form: &UserMentionForm) -> Result<Self, Error> {
|
||||||
|
use crate::schema::user_mention::dsl::*;
|
||||||
|
insert_into(user_mention)
|
||||||
|
.values(user_mention_form)
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
conn: &PgConnection,
|
||||||
|
user_mention_id: i32,
|
||||||
|
user_mention_form: &UserMentionForm,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::user_mention::dsl::*;
|
||||||
|
diesel::update(user_mention.find(user_mention_id))
|
||||||
|
.set(user_mention_form)
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::super::comment::*;
|
||||||
|
use super::super::community::*;
|
||||||
|
use super::super::post::*;
|
||||||
|
use super::super::user::*;
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn test_crud() {
|
||||||
|
let conn = establish_connection();
|
||||||
|
|
||||||
|
let new_user = UserForm {
|
||||||
|
name: "terrylake".into(),
|
||||||
|
fedi_name: "rrf".into(),
|
||||||
|
preferred_username: None,
|
||||||
|
password_encrypted: "nope".into(),
|
||||||
|
email: None,
|
||||||
|
admin: false,
|
||||||
|
banned: false,
|
||||||
|
updated: None,
|
||||||
|
show_nsfw: false,
|
||||||
|
theme: "darkly".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
||||||
|
let recipient_form = UserForm {
|
||||||
|
name: "terrylakes recipient".into(),
|
||||||
|
fedi_name: "rrf".into(),
|
||||||
|
preferred_username: None,
|
||||||
|
password_encrypted: "nope".into(),
|
||||||
|
email: None,
|
||||||
|
admin: false,
|
||||||
|
banned: false,
|
||||||
|
updated: None,
|
||||||
|
show_nsfw: false,
|
||||||
|
theme: "darkly".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
|
||||||
|
|
||||||
|
let new_community = CommunityForm {
|
||||||
|
name: "test community lake".to_string(),
|
||||||
|
title: "nada".to_owned(),
|
||||||
|
description: None,
|
||||||
|
category_id: 1,
|
||||||
|
creator_id: inserted_user.id,
|
||||||
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
|
updated: None,
|
||||||
|
nsfw: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||||
|
|
||||||
|
let new_post = PostForm {
|
||||||
|
name: "A test post".into(),
|
||||||
|
creator_id: inserted_user.id,
|
||||||
|
url: None,
|
||||||
|
body: None,
|
||||||
|
community_id: inserted_community.id,
|
||||||
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
|
locked: None,
|
||||||
|
stickied: None,
|
||||||
|
updated: None,
|
||||||
|
nsfw: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_post = Post::create(&conn, &new_post).unwrap();
|
||||||
|
|
||||||
|
let comment_form = CommentForm {
|
||||||
|
content: "A test comment".into(),
|
||||||
|
creator_id: inserted_user.id,
|
||||||
|
post_id: inserted_post.id,
|
||||||
|
removed: None,
|
||||||
|
deleted: None,
|
||||||
|
read: None,
|
||||||
|
parent_id: None,
|
||||||
|
updated: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
|
||||||
|
|
||||||
|
let user_mention_form = UserMentionForm {
|
||||||
|
recipient_id: inserted_recipient.id,
|
||||||
|
comment_id: inserted_comment.id,
|
||||||
|
read: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_mention = UserMention::create(&conn, &user_mention_form).unwrap();
|
||||||
|
|
||||||
|
let expected_mention = UserMention {
|
||||||
|
id: inserted_mention.id,
|
||||||
|
recipient_id: inserted_mention.recipient_id,
|
||||||
|
comment_id: inserted_mention.comment_id,
|
||||||
|
read: false,
|
||||||
|
published: inserted_mention.published,
|
||||||
|
};
|
||||||
|
|
||||||
|
let read_mention = UserMention::read(&conn, inserted_mention.id).unwrap();
|
||||||
|
let updated_mention =
|
||||||
|
UserMention::update(&conn, inserted_mention.id, &user_mention_form).unwrap();
|
||||||
|
let num_deleted = UserMention::delete(&conn, inserted_mention.id).unwrap();
|
||||||
|
Comment::delete(&conn, inserted_comment.id).unwrap();
|
||||||
|
Post::delete(&conn, inserted_post.id).unwrap();
|
||||||
|
Community::delete(&conn, inserted_community.id).unwrap();
|
||||||
|
User_::delete(&conn, inserted_user.id).unwrap();
|
||||||
|
User_::delete(&conn, inserted_recipient.id).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected_mention, read_mention);
|
||||||
|
assert_eq!(expected_mention, inserted_mention);
|
||||||
|
assert_eq!(expected_mention, updated_mention);
|
||||||
|
assert_eq!(1, num_deleted);
|
||||||
|
}
|
||||||
|
}
|
117
server/src/db/user_mention_view.rs
Normal file
117
server/src/db/user_mention_view.rs
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// The faked schema since diesel doesn't do views
|
||||||
|
table! {
|
||||||
|
user_mention_view (id) {
|
||||||
|
id -> Int4,
|
||||||
|
user_mention_id -> Int4,
|
||||||
|
creator_id -> Int4,
|
||||||
|
post_id -> Int4,
|
||||||
|
parent_id -> Nullable<Int4>,
|
||||||
|
content -> Text,
|
||||||
|
removed -> Bool,
|
||||||
|
read -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
deleted -> Bool,
|
||||||
|
community_id -> Int4,
|
||||||
|
banned -> Bool,
|
||||||
|
banned_from_community -> Bool,
|
||||||
|
creator_name -> Varchar,
|
||||||
|
score -> BigInt,
|
||||||
|
upvotes -> BigInt,
|
||||||
|
downvotes -> BigInt,
|
||||||
|
user_id -> Nullable<Int4>,
|
||||||
|
my_vote -> Nullable<Int4>,
|
||||||
|
saved -> Nullable<Bool>,
|
||||||
|
recipient_id -> Int4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
|
||||||
|
)]
|
||||||
|
#[table_name = "user_mention_view"]
|
||||||
|
pub struct UserMentionView {
|
||||||
|
pub id: i32,
|
||||||
|
pub user_mention_id: i32,
|
||||||
|
pub creator_id: i32,
|
||||||
|
pub post_id: i32,
|
||||||
|
pub parent_id: Option<i32>,
|
||||||
|
pub content: String,
|
||||||
|
pub removed: bool,
|
||||||
|
pub read: bool,
|
||||||
|
pub published: chrono::NaiveDateTime,
|
||||||
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub deleted: bool,
|
||||||
|
pub community_id: i32,
|
||||||
|
pub banned: bool,
|
||||||
|
pub banned_from_community: bool,
|
||||||
|
pub creator_name: String,
|
||||||
|
pub score: i64,
|
||||||
|
pub upvotes: i64,
|
||||||
|
pub downvotes: i64,
|
||||||
|
pub user_id: Option<i32>,
|
||||||
|
pub my_vote: Option<i32>,
|
||||||
|
pub saved: Option<bool>,
|
||||||
|
pub recipient_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserMentionView {
|
||||||
|
pub fn get_mentions(
|
||||||
|
conn: &PgConnection,
|
||||||
|
for_user_id: i32,
|
||||||
|
sort: &SortType,
|
||||||
|
unread_only: bool,
|
||||||
|
page: Option<i64>,
|
||||||
|
limit: Option<i64>,
|
||||||
|
) -> Result<Vec<Self>, Error> {
|
||||||
|
use super::user_mention_view::user_mention_view::dsl::*;
|
||||||
|
|
||||||
|
let (limit, offset) = limit_and_offset(page, limit);
|
||||||
|
|
||||||
|
let mut query = user_mention_view.into_boxed();
|
||||||
|
|
||||||
|
query = query
|
||||||
|
.filter(user_id.eq(for_user_id))
|
||||||
|
.filter(recipient_id.eq(for_user_id));
|
||||||
|
|
||||||
|
if unread_only {
|
||||||
|
query = query.filter(read.eq(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
query = match sort {
|
||||||
|
// SortType::Hot => query.order_by(hot_rank.desc()),
|
||||||
|
SortType::New => query.order_by(published.desc()),
|
||||||
|
SortType::TopAll => query.order_by(score.desc()),
|
||||||
|
SortType::TopYear => query
|
||||||
|
.filter(published.gt(now - 1.years()))
|
||||||
|
.order_by(score.desc()),
|
||||||
|
SortType::TopMonth => query
|
||||||
|
.filter(published.gt(now - 1.months()))
|
||||||
|
.order_by(score.desc()),
|
||||||
|
SortType::TopWeek => query
|
||||||
|
.filter(published.gt(now - 1.weeks()))
|
||||||
|
.order_by(score.desc()),
|
||||||
|
SortType::TopDay => query
|
||||||
|
.filter(published.gt(now - 1.days()))
|
||||||
|
.order_by(score.desc()),
|
||||||
|
_ => query.order_by(published.desc()),
|
||||||
|
};
|
||||||
|
|
||||||
|
query.limit(limit).offset(offset).load::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(
|
||||||
|
conn: &PgConnection,
|
||||||
|
from_user_mention_id: i32,
|
||||||
|
from_recipient_id: i32,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use super::user_mention_view::user_mention_view::dsl::*;
|
||||||
|
|
||||||
|
user_mention_view
|
||||||
|
.filter(user_mention_id.eq(from_user_mention_id))
|
||||||
|
.filter(user_id.eq(from_recipient_id))
|
||||||
|
.first::<Self>(conn)
|
||||||
|
}
|
||||||
|
}
|
|
@ -104,9 +104,23 @@ pub fn has_slurs(test: &str) -> bool {
|
||||||
SLUR_REGEX.is_match(test)
|
SLUR_REGEX.is_match(test)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn extract_usernames(test: &str) -> Vec<&str> {
|
||||||
|
let mut matches: Vec<&str> = USERNAME_MATCHES_REGEX
|
||||||
|
.find_iter(test)
|
||||||
|
.map(|mat| mat.as_str())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Unique
|
||||||
|
matches.sort_unstable();
|
||||||
|
matches.dedup();
|
||||||
|
|
||||||
|
// Remove /u/
|
||||||
|
matches.iter().map(|t| &t[3..]).collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{has_slurs, is_email_regex, remove_slurs, Settings};
|
use crate::{extract_usernames, has_slurs, is_email_regex, remove_slurs, Settings};
|
||||||
#[test]
|
#[test]
|
||||||
fn test_api() {
|
fn test_api() {
|
||||||
assert_eq!(Settings::get().api_endpoint(), "rrr/api/v1");
|
assert_eq!(Settings::get().api_endpoint(), "rrr/api/v1");
|
||||||
|
@ -131,9 +145,17 @@ mod tests {
|
||||||
assert!(has_slurs(&test));
|
assert!(has_slurs(&test));
|
||||||
assert!(!has_slurs(slur_free));
|
assert!(!has_slurs(slur_free));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_usernames() {
|
||||||
|
let usernames = extract_usernames("this is a user mention for [/u/testme](/u/testme) and thats all. Oh [/u/another](/u/another) user. And the first again [/u/testme](/u/testme) okay");
|
||||||
|
let expected = vec!["another", "testme"];
|
||||||
|
assert_eq!(usernames, expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
||||||
static ref SLUR_REGEX: Regex = Regex::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?s?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").unwrap();
|
static ref SLUR_REGEX: Regex = Regex::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?s?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").unwrap();
|
||||||
|
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -266,6 +266,16 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
user_mention (id) {
|
||||||
|
id -> Int4,
|
||||||
|
recipient_id -> Int4,
|
||||||
|
comment_id -> Int4,
|
||||||
|
read -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
joinable!(comment -> post (post_id));
|
joinable!(comment -> post (post_id));
|
||||||
joinable!(comment -> user_ (creator_id));
|
joinable!(comment -> user_ (creator_id));
|
||||||
joinable!(comment_like -> comment (comment_id));
|
joinable!(comment_like -> comment (comment_id));
|
||||||
|
@ -303,6 +313,8 @@ joinable!(post_saved -> post (post_id));
|
||||||
joinable!(post_saved -> user_ (user_id));
|
joinable!(post_saved -> user_ (user_id));
|
||||||
joinable!(site -> user_ (creator_id));
|
joinable!(site -> user_ (creator_id));
|
||||||
joinable!(user_ban -> user_ (user_id));
|
joinable!(user_ban -> user_ (user_id));
|
||||||
|
joinable!(user_mention -> comment (comment_id));
|
||||||
|
joinable!(user_mention -> user_ (recipient_id));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
allow_tables_to_appear_in_same_query!(
|
||||||
category,
|
category,
|
||||||
|
@ -329,4 +341,5 @@ allow_tables_to_appear_in_same_query!(
|
||||||
site,
|
site,
|
||||||
user_,
|
user_,
|
||||||
user_ban,
|
user_ban,
|
||||||
|
user_mention,
|
||||||
);
|
);
|
||||||
|
|
|
@ -343,6 +343,16 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
let res = Oper::new(user_operation, get_replies).perform()?;
|
let res = Oper::new(user_operation, get_replies).perform()?;
|
||||||
Ok(serde_json::to_string(&res)?)
|
Ok(serde_json::to_string(&res)?)
|
||||||
}
|
}
|
||||||
|
UserOperation::GetUserMentions => {
|
||||||
|
let get_user_mentions: GetUserMentions = serde_json::from_str(data)?;
|
||||||
|
let res = Oper::new(user_operation, get_user_mentions).perform()?;
|
||||||
|
Ok(serde_json::to_string(&res)?)
|
||||||
|
}
|
||||||
|
UserOperation::EditUserMention => {
|
||||||
|
let edit_user_mention: EditUserMention = serde_json::from_str(data)?;
|
||||||
|
let res = Oper::new(user_operation, edit_user_mention).perform()?;
|
||||||
|
Ok(serde_json::to_string(&res)?)
|
||||||
|
}
|
||||||
UserOperation::MarkAllAsRead => {
|
UserOperation::MarkAllAsRead => {
|
||||||
let mark_all_as_read: MarkAllAsRead = serde_json::from_str(data)?;
|
let mark_all_as_read: MarkAllAsRead = serde_json::from_str(data)?;
|
||||||
let res = Oper::new(user_operation, mark_all_as_read).perform()?;
|
let res = Oper::new(user_operation, mark_all_as_read).perform()?;
|
||||||
|
|
30
ui/src/components/comment-node.tsx
vendored
30
ui/src/components/comment-node.tsx
vendored
|
@ -4,6 +4,7 @@ import {
|
||||||
CommentNode as CommentNodeI,
|
CommentNode as CommentNodeI,
|
||||||
CommentLikeForm,
|
CommentLikeForm,
|
||||||
CommentForm as CommentFormI,
|
CommentForm as CommentFormI,
|
||||||
|
EditUserMentionForm,
|
||||||
SaveCommentForm,
|
SaveCommentForm,
|
||||||
BanFromCommunityForm,
|
BanFromCommunityForm,
|
||||||
BanUserForm,
|
BanUserForm,
|
||||||
|
@ -686,16 +687,25 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMarkRead(i: CommentNode) {
|
handleMarkRead(i: CommentNode) {
|
||||||
let form: CommentFormI = {
|
// if it has a user_mention_id field, then its a mention
|
||||||
content: i.props.node.comment.content,
|
if (i.props.node.comment.user_mention_id) {
|
||||||
edit_id: i.props.node.comment.id,
|
let form: EditUserMentionForm = {
|
||||||
creator_id: i.props.node.comment.creator_id,
|
user_mention_id: i.props.node.comment.user_mention_id,
|
||||||
post_id: i.props.node.comment.post_id,
|
read: !i.props.node.comment.read,
|
||||||
parent_id: i.props.node.comment.parent_id,
|
};
|
||||||
read: !i.props.node.comment.read,
|
WebSocketService.Instance.editUserMention(form);
|
||||||
auth: null,
|
} else {
|
||||||
};
|
let form: CommentFormI = {
|
||||||
WebSocketService.Instance.editComment(form);
|
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) {
|
||||||
|
|
168
ui/src/components/inbox.tsx
vendored
168
ui/src/components/inbox.tsx
vendored
|
@ -8,6 +8,9 @@ import {
|
||||||
SortType,
|
SortType,
|
||||||
GetRepliesForm,
|
GetRepliesForm,
|
||||||
GetRepliesResponse,
|
GetRepliesResponse,
|
||||||
|
GetUserMentionsForm,
|
||||||
|
GetUserMentionsResponse,
|
||||||
|
UserMentionResponse,
|
||||||
CommentResponse,
|
CommentResponse,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
|
@ -16,14 +19,22 @@ import { CommentNodes } from './comment-nodes';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
enum UnreadType {
|
enum UnreadOrAll {
|
||||||
Unread,
|
Unread,
|
||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum UnreadType {
|
||||||
|
Both,
|
||||||
|
Replies,
|
||||||
|
Mentions,
|
||||||
|
}
|
||||||
|
|
||||||
interface InboxState {
|
interface InboxState {
|
||||||
|
unreadOrAll: UnreadOrAll;
|
||||||
unreadType: UnreadType;
|
unreadType: UnreadType;
|
||||||
replies: Array<Comment>;
|
replies: Array<Comment>;
|
||||||
|
mentions: Array<Comment>;
|
||||||
sort: SortType;
|
sort: SortType;
|
||||||
page: number;
|
page: number;
|
||||||
}
|
}
|
||||||
|
@ -31,8 +42,10 @@ interface InboxState {
|
||||||
export class Inbox extends Component<any, InboxState> {
|
export class Inbox extends Component<any, InboxState> {
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
private emptyState: InboxState = {
|
private emptyState: InboxState = {
|
||||||
unreadType: UnreadType.Unread,
|
unreadOrAll: UnreadOrAll.Unread,
|
||||||
|
unreadType: UnreadType.Both,
|
||||||
replies: [],
|
replies: [],
|
||||||
|
mentions: [],
|
||||||
sort: SortType.New,
|
sort: SortType.New,
|
||||||
page: 1,
|
page: 1,
|
||||||
};
|
};
|
||||||
|
@ -83,8 +96,8 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
</T>
|
</T>
|
||||||
</span>
|
</span>
|
||||||
</h5>
|
</h5>
|
||||||
{this.state.replies.length > 0 &&
|
{this.state.replies.length + this.state.mentions.length > 0 &&
|
||||||
this.state.unreadType == UnreadType.Unread && (
|
this.state.unreadOrAll == UnreadOrAll.Unread && (
|
||||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span class="pointer" onClick={this.markAllAsRead}>
|
<span class="pointer" onClick={this.markAllAsRead}>
|
||||||
|
@ -94,7 +107,9 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
{this.selects()}
|
{this.selects()}
|
||||||
{this.replies()}
|
{this.state.unreadType == UnreadType.Both && this.both()}
|
||||||
|
{this.state.unreadType == UnreadType.Replies && this.replies()}
|
||||||
|
{this.state.unreadType == UnreadType.Mentions && this.mentions()}
|
||||||
{this.paginator()}
|
{this.paginator()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -106,24 +121,42 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<select
|
<select
|
||||||
value={this.state.unreadType}
|
value={this.state.unreadOrAll}
|
||||||
onChange={linkEvent(this, this.handleUnreadTypeChange)}
|
onChange={linkEvent(this, this.handleUnreadOrAllChange)}
|
||||||
class="custom-select custom-select-sm w-auto"
|
class="custom-select custom-select-sm w-auto mr-2"
|
||||||
>
|
>
|
||||||
<option disabled>
|
<option disabled>
|
||||||
<T i18nKey="type">#</T>
|
<T i18nKey="type">#</T>
|
||||||
</option>
|
</option>
|
||||||
<option value={UnreadType.Unread}>
|
<option value={UnreadOrAll.Unread}>
|
||||||
<T i18nKey="unread">#</T>
|
<T i18nKey="unread">#</T>
|
||||||
</option>
|
</option>
|
||||||
<option value={UnreadType.All}>
|
<option value={UnreadOrAll.All}>
|
||||||
<T i18nKey="all">#</T>
|
<T i18nKey="all">#</T>
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
<select
|
||||||
|
value={this.state.unreadType}
|
||||||
|
onChange={linkEvent(this, this.handleUnreadTypeChange)}
|
||||||
|
class="custom-select custom-select-sm w-auto mr-2"
|
||||||
|
>
|
||||||
|
<option disabled>
|
||||||
|
<T i18nKey="type">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={UnreadType.Both}>
|
||||||
|
<T i18nKey="both">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={UnreadType.Replies}>
|
||||||
|
<T i18nKey="replies">#</T>
|
||||||
|
</option>
|
||||||
|
<option value={UnreadType.Mentions}>
|
||||||
|
<T i18nKey="mentions">#</T>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
<select
|
<select
|
||||||
value={this.state.sort}
|
value={this.state.sort}
|
||||||
onChange={linkEvent(this, this.handleSortChange)}
|
onChange={linkEvent(this, this.handleSortChange)}
|
||||||
class="custom-select custom-select-sm w-auto ml-2"
|
class="custom-select custom-select-sm w-auto"
|
||||||
>
|
>
|
||||||
<option disabled>
|
<option disabled>
|
||||||
<T i18nKey="sort_type">#</T>
|
<T i18nKey="sort_type">#</T>
|
||||||
|
@ -151,6 +184,37 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
both() {
|
||||||
|
let combined: Array<{
|
||||||
|
type_: string;
|
||||||
|
data: Comment;
|
||||||
|
}> = [];
|
||||||
|
let replies = this.state.replies.map(e => {
|
||||||
|
return { type_: 'replies', data: e };
|
||||||
|
});
|
||||||
|
let mentions = this.state.mentions.map(e => {
|
||||||
|
return { type_: 'mentions', data: e };
|
||||||
|
});
|
||||||
|
|
||||||
|
combined.push(...replies);
|
||||||
|
combined.push(...mentions);
|
||||||
|
|
||||||
|
// Sort it
|
||||||
|
if (this.state.sort == SortType.New) {
|
||||||
|
combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
|
||||||
|
} else {
|
||||||
|
combined.sort((a, b) => b.data.score - a.data.score);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{combined.map(i => (
|
||||||
|
<CommentNodes nodes={[{ comment: i.data }]} noIndent markable />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
replies() {
|
replies() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -161,6 +225,16 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mentions() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{this.state.mentions.map(mention => (
|
||||||
|
<CommentNodes nodes={[{ comment: mention }]} noIndent markable />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
paginator() {
|
paginator() {
|
||||||
return (
|
return (
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
|
@ -194,6 +268,13 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
i.refetch();
|
i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleUnreadOrAllChange(i: Inbox, event: any) {
|
||||||
|
i.state.unreadOrAll = Number(event.target.value);
|
||||||
|
i.state.page = 1;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
handleUnreadTypeChange(i: Inbox, event: any) {
|
handleUnreadTypeChange(i: Inbox, event: any) {
|
||||||
i.state.unreadType = Number(event.target.value);
|
i.state.unreadType = Number(event.target.value);
|
||||||
i.state.page = 1;
|
i.state.page = 1;
|
||||||
|
@ -202,13 +283,21 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
refetch() {
|
refetch() {
|
||||||
let form: GetRepliesForm = {
|
let repliesForm: GetRepliesForm = {
|
||||||
sort: SortType[this.state.sort],
|
sort: SortType[this.state.sort],
|
||||||
unread_only: this.state.unreadType == UnreadType.Unread,
|
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
|
||||||
page: this.state.page,
|
page: this.state.page,
|
||||||
limit: 9999,
|
limit: 9999,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.getReplies(form);
|
WebSocketService.Instance.getReplies(repliesForm);
|
||||||
|
|
||||||
|
let userMentionsForm: GetUserMentionsForm = {
|
||||||
|
sort: SortType[this.state.sort],
|
||||||
|
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
|
||||||
|
page: this.state.page,
|
||||||
|
limit: 9999,
|
||||||
|
};
|
||||||
|
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortChange(i: Inbox, event: any) {
|
handleSortChange(i: Inbox, event: any) {
|
||||||
|
@ -228,13 +317,21 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
alert(i18n.t(msg.error));
|
alert(i18n.t(msg.error));
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (op == UserOperation.GetReplies) {
|
||||||
op == UserOperation.GetReplies ||
|
|
||||||
op == UserOperation.MarkAllAsRead
|
|
||||||
) {
|
|
||||||
let res: GetRepliesResponse = msg;
|
let res: GetRepliesResponse = msg;
|
||||||
this.state.replies = res.replies;
|
this.state.replies = res.replies;
|
||||||
this.sendRepliesCount();
|
this.sendUnreadCount();
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (op == UserOperation.GetUserMentions) {
|
||||||
|
let res: GetUserMentionsResponse = msg;
|
||||||
|
this.state.mentions = res.mentions;
|
||||||
|
this.sendUnreadCount();
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (op == UserOperation.MarkAllAsRead) {
|
||||||
|
this.state.replies = [];
|
||||||
|
this.state.mentions = [];
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.EditComment) {
|
} else if (op == UserOperation.EditComment) {
|
||||||
|
@ -250,7 +347,7 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
found.score = res.comment.score;
|
found.score = res.comment.score;
|
||||||
|
|
||||||
// If youre in the unread view, just remove it from the list
|
// If youre in the unread view, just remove it from the list
|
||||||
if (this.state.unreadType == UnreadType.Unread && res.comment.read) {
|
if (this.state.unreadOrAll == UnreadOrAll.Unread && res.comment.read) {
|
||||||
this.state.replies = this.state.replies.filter(
|
this.state.replies = this.state.replies.filter(
|
||||||
r => r.id !== res.comment.id
|
r => r.id !== res.comment.id
|
||||||
);
|
);
|
||||||
|
@ -258,8 +355,30 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
let found = this.state.replies.find(c => c.id == res.comment.id);
|
let found = this.state.replies.find(c => c.id == res.comment.id);
|
||||||
found.read = res.comment.read;
|
found.read = res.comment.read;
|
||||||
}
|
}
|
||||||
this.sendRepliesCount();
|
this.sendUnreadCount();
|
||||||
|
this.setState(this.state);
|
||||||
|
} else if (op == UserOperation.EditUserMention) {
|
||||||
|
let res: UserMentionResponse = msg;
|
||||||
|
|
||||||
|
let found = this.state.mentions.find(c => c.id == res.mention.id);
|
||||||
|
found.content = res.mention.content;
|
||||||
|
found.updated = res.mention.updated;
|
||||||
|
found.removed = res.mention.removed;
|
||||||
|
found.deleted = res.mention.deleted;
|
||||||
|
found.upvotes = res.mention.upvotes;
|
||||||
|
found.downvotes = res.mention.downvotes;
|
||||||
|
found.score = res.mention.score;
|
||||||
|
|
||||||
|
// If youre in the unread view, just remove it from the list
|
||||||
|
if (this.state.unreadOrAll == UnreadOrAll.Unread && res.mention.read) {
|
||||||
|
this.state.mentions = this.state.mentions.filter(
|
||||||
|
r => r.id !== res.mention.id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let found = this.state.mentions.find(c => c.id == res.mention.id);
|
||||||
|
found.read = res.mention.read;
|
||||||
|
}
|
||||||
|
this.sendUnreadCount();
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.CreateComment) {
|
} else if (op == UserOperation.CreateComment) {
|
||||||
// let res: CommentResponse = msg;
|
// let res: CommentResponse = msg;
|
||||||
|
@ -284,10 +403,13 @@ export class Inbox extends Component<any, InboxState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendRepliesCount() {
|
sendUnreadCount() {
|
||||||
|
let count =
|
||||||
|
this.state.replies.filter(r => !r.read).length +
|
||||||
|
this.state.mentions.filter(r => !r.read).length;
|
||||||
UserService.Instance.sub.next({
|
UserService.Instance.sub.next({
|
||||||
user: UserService.Instance.user,
|
user: UserService.Instance.user,
|
||||||
unreadCount: this.state.replies.filter(r => !r.read).length,
|
unreadCount: count,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
50
ui/src/components/navbar.tsx
vendored
50
ui/src/components/navbar.tsx
vendored
|
@ -7,6 +7,8 @@ import {
|
||||||
UserOperation,
|
UserOperation,
|
||||||
GetRepliesForm,
|
GetRepliesForm,
|
||||||
GetRepliesResponse,
|
GetRepliesResponse,
|
||||||
|
GetUserMentionsForm,
|
||||||
|
GetUserMentionsResponse,
|
||||||
SortType,
|
SortType,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
Comment,
|
Comment,
|
||||||
|
@ -21,6 +23,7 @@ interface NavbarState {
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
expandUserDropdown: boolean;
|
expandUserDropdown: boolean;
|
||||||
replies: Array<Comment>;
|
replies: Array<Comment>;
|
||||||
|
mentions: Array<Comment>;
|
||||||
fetchCount: number;
|
fetchCount: number;
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
siteName: string;
|
siteName: string;
|
||||||
|
@ -34,6 +37,7 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
unreadCount: 0,
|
unreadCount: 0,
|
||||||
fetchCount: 0,
|
fetchCount: 0,
|
||||||
replies: [],
|
replies: [],
|
||||||
|
mentions: [],
|
||||||
expanded: false,
|
expanded: false,
|
||||||
expandUserDropdown: false,
|
expandUserDropdown: false,
|
||||||
siteName: undefined,
|
siteName: undefined,
|
||||||
|
@ -44,7 +48,7 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
this.handleOverviewClick = this.handleOverviewClick.bind(this);
|
this.handleOverviewClick = this.handleOverviewClick.bind(this);
|
||||||
|
|
||||||
this.keepFetchingReplies();
|
this.keepFetchingUnreads();
|
||||||
|
|
||||||
// Subscribe to user changes
|
// Subscribe to user changes
|
||||||
this.userSub = UserService.Instance.sub.subscribe(user => {
|
this.userSub = UserService.Instance.sub.subscribe(user => {
|
||||||
|
@ -233,7 +237,22 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.replies = unreadReplies;
|
this.state.replies = unreadReplies;
|
||||||
this.sendRepliesCount(res);
|
this.setState(this.state);
|
||||||
|
this.sendUnreadCount();
|
||||||
|
} else if (op == UserOperation.GetUserMentions) {
|
||||||
|
let res: GetUserMentionsResponse = msg;
|
||||||
|
let unreadMentions = res.mentions.filter(r => !r.read);
|
||||||
|
if (
|
||||||
|
unreadMentions.length > 0 &&
|
||||||
|
this.state.fetchCount > 1 &&
|
||||||
|
JSON.stringify(this.state.mentions) !== JSON.stringify(unreadMentions)
|
||||||
|
) {
|
||||||
|
this.notify(unreadMentions);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.mentions = unreadMentions;
|
||||||
|
this.setState(this.state);
|
||||||
|
this.sendUnreadCount();
|
||||||
} else if (op == UserOperation.GetSite) {
|
} else if (op == UserOperation.GetSite) {
|
||||||
let res: GetSiteResponse = msg;
|
let res: GetSiteResponse = msg;
|
||||||
|
|
||||||
|
@ -245,12 +264,12 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keepFetchingReplies() {
|
keepFetchingUnreads() {
|
||||||
this.fetchReplies();
|
this.fetchUnreads();
|
||||||
setInterval(() => this.fetchReplies(), 15000);
|
setInterval(() => this.fetchUnreads(), 15000);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchReplies() {
|
fetchUnreads() {
|
||||||
if (this.state.isLoggedIn) {
|
if (this.state.isLoggedIn) {
|
||||||
let repliesForm: GetRepliesForm = {
|
let repliesForm: GetRepliesForm = {
|
||||||
sort: SortType[SortType.New],
|
sort: SortType[SortType.New],
|
||||||
|
@ -258,8 +277,16 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 9999,
|
limit: 9999,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let userMentionsForm: GetUserMentionsForm = {
|
||||||
|
sort: SortType[SortType.New],
|
||||||
|
unread_only: true,
|
||||||
|
page: 1,
|
||||||
|
limit: 9999,
|
||||||
|
};
|
||||||
if (this.currentLocation !== '/inbox') {
|
if (this.currentLocation !== '/inbox') {
|
||||||
WebSocketService.Instance.getReplies(repliesForm);
|
WebSocketService.Instance.getReplies(repliesForm);
|
||||||
|
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
||||||
this.state.fetchCount++;
|
this.state.fetchCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -269,13 +296,20 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
return this.context.router.history.location.pathname;
|
return this.context.router.history.location.pathname;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendRepliesCount(res: GetRepliesResponse) {
|
sendUnreadCount() {
|
||||||
UserService.Instance.sub.next({
|
UserService.Instance.sub.next({
|
||||||
user: UserService.Instance.user,
|
user: UserService.Instance.user,
|
||||||
unreadCount: res.replies.filter(r => !r.read).length,
|
unreadCount: this.unreadCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get unreadCount() {
|
||||||
|
return (
|
||||||
|
this.state.replies.filter(r => !r.read).length +
|
||||||
|
this.state.mentions.filter(r => !r.read).length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
requestNotificationPermission() {
|
requestNotificationPermission() {
|
||||||
if (UserService.Instance.user) {
|
if (UserService.Instance.user) {
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
16
ui/src/components/search.tsx
vendored
16
ui/src/components/search.tsx
vendored
|
@ -396,11 +396,16 @@ export class Search extends Component<any, SearchState> {
|
||||||
let res = this.state.searchResponse;
|
let res = this.state.searchResponse;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{res && res.op && res.posts.length == 0 && res.comments.length == 0 && (
|
{res &&
|
||||||
<span>
|
res.op &&
|
||||||
<T i18nKey="no_results">#</T>
|
res.posts.length == 0 &&
|
||||||
</span>
|
res.comments.length == 0 &&
|
||||||
)}
|
res.communities.length == 0 &&
|
||||||
|
res.users.length == 0 && (
|
||||||
|
<span>
|
||||||
|
<T i18nKey="no_results">#</T>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -420,7 +425,6 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
search() {
|
search() {
|
||||||
// TODO community
|
|
||||||
let form: SearchForm = {
|
let form: SearchForm = {
|
||||||
q: this.state.q,
|
q: this.state.q,
|
||||||
type_: SearchType[this.state.type_],
|
type_: SearchType[this.state.type_],
|
||||||
|
|
3
ui/src/i18next.ts
vendored
3
ui/src/i18next.ts
vendored
|
@ -11,7 +11,6 @@ import { zh } from './translations/zh';
|
||||||
import { nl } from './translations/nl';
|
import { nl } from './translations/nl';
|
||||||
|
|
||||||
// https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66
|
// https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66
|
||||||
// TODO don't forget to add moment locales for new languages.
|
|
||||||
const resources = {
|
const resources = {
|
||||||
en,
|
en,
|
||||||
eo,
|
eo,
|
||||||
|
@ -30,7 +29,7 @@ function format(value: any, format: any, lng: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
i18n.init({
|
i18n.init({
|
||||||
debug: true,
|
debug: false,
|
||||||
// load: 'languageOnly',
|
// load: 'languageOnly',
|
||||||
|
|
||||||
// initImmediate: false,
|
// initImmediate: false,
|
||||||
|
|
30
ui/src/interfaces.ts
vendored
30
ui/src/interfaces.ts
vendored
|
@ -20,6 +20,8 @@ export enum UserOperation {
|
||||||
GetFollowedCommunities,
|
GetFollowedCommunities,
|
||||||
GetUserDetails,
|
GetUserDetails,
|
||||||
GetReplies,
|
GetReplies,
|
||||||
|
GetUserMentions,
|
||||||
|
EditUserMention,
|
||||||
GetModlog,
|
GetModlog,
|
||||||
BanFromCommunity,
|
BanFromCommunity,
|
||||||
AddModToCommunity,
|
AddModToCommunity,
|
||||||
|
@ -171,6 +173,8 @@ export interface Comment {
|
||||||
user_id?: number;
|
user_id?: number;
|
||||||
my_vote?: number;
|
my_vote?: number;
|
||||||
saved?: boolean;
|
saved?: boolean;
|
||||||
|
user_mention_id?: number; // For mention type
|
||||||
|
recipient_id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
|
@ -229,7 +233,7 @@ export interface UserDetailsResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetRepliesForm {
|
export interface GetRepliesForm {
|
||||||
sort: string; // TODO figure this one out
|
sort: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
unread_only: boolean;
|
unread_only: boolean;
|
||||||
|
@ -241,6 +245,30 @@ export interface GetRepliesResponse {
|
||||||
replies: Array<Comment>;
|
replies: Array<Comment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetUserMentionsForm {
|
||||||
|
sort: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
unread_only: boolean;
|
||||||
|
auth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetUserMentionsResponse {
|
||||||
|
op: string;
|
||||||
|
mentions: Array<Comment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditUserMentionForm {
|
||||||
|
user_mention_id: number;
|
||||||
|
read?: boolean;
|
||||||
|
auth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserMentionResponse {
|
||||||
|
op: string;
|
||||||
|
mention: Comment;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BanFromCommunityForm {
|
export interface BanFromCommunityForm {
|
||||||
community_id: number;
|
community_id: number;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
|
|
12
ui/src/services/WebSocketService.ts
vendored
12
ui/src/services/WebSocketService.ts
vendored
|
@ -25,6 +25,8 @@ import {
|
||||||
Site,
|
Site,
|
||||||
UserView,
|
UserView,
|
||||||
GetRepliesForm,
|
GetRepliesForm,
|
||||||
|
GetUserMentionsForm,
|
||||||
|
EditUserMentionForm,
|
||||||
SearchForm,
|
SearchForm,
|
||||||
UserSettingsForm,
|
UserSettingsForm,
|
||||||
DeleteAccountForm,
|
DeleteAccountForm,
|
||||||
|
@ -222,6 +224,16 @@ export class WebSocketService {
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetReplies, form));
|
this.subject.next(this.wsSendWrapper(UserOperation.GetReplies, form));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getUserMentions(form: GetUserMentionsForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.subject.next(this.wsSendWrapper(UserOperation.GetUserMentions, form));
|
||||||
|
}
|
||||||
|
|
||||||
|
public editUserMention(form: EditUserMentionForm) {
|
||||||
|
this.setAuth(form);
|
||||||
|
this.subject.next(this.wsSendWrapper(UserOperation.EditUserMention, form));
|
||||||
|
}
|
||||||
|
|
||||||
public getModlog(form: GetModlogForm) {
|
public getModlog(form: GetModlogForm) {
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, form));
|
this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, form));
|
||||||
}
|
}
|
||||||
|
|
2
ui/src/translations/en.ts
vendored
2
ui/src/translations/en.ts
vendored
|
@ -101,6 +101,8 @@ export const en = {
|
||||||
mark_all_as_read: 'mark all as read',
|
mark_all_as_read: 'mark all as read',
|
||||||
type: 'Type',
|
type: 'Type',
|
||||||
unread: 'Unread',
|
unread: 'Unread',
|
||||||
|
replies: 'Replies',
|
||||||
|
mentions: 'Mentions',
|
||||||
reply_sent: 'Reply sent',
|
reply_sent: 'Reply sent',
|
||||||
search: 'Search',
|
search: 'Search',
|
||||||
overview: 'Overview',
|
overview: 'Overview',
|
||||||
|
|
Loading…
Reference in a new issue