mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-12-23 19:31:33 +00:00
parent
9be8675722
commit
2d5f13ae67
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"))?,
|
||||
};
|
||||
|
||||
// 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
|
||||
let like_form = CommentLikeForm {
|
||||
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"))?,
|
||||
};
|
||||
|
||||
// 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
|
||||
if let Some(removed) = data.removed.to_owned() {
|
||||
let form = ModRemoveCommentForm {
|
||||
|
|
|
@ -8,9 +8,11 @@ use crate::db::moderator_views::*;
|
|||
use crate::db::post::*;
|
||||
use crate::db::post_view::*;
|
||||
use crate::db::user::*;
|
||||
use crate::db::user_mention::*;
|
||||
use crate::db::user_mention_view::*;
|
||||
use crate::db::user_view::*;
|
||||
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 serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -43,6 +45,8 @@ pub enum UserOperation {
|
|||
GetFollowedCommunities,
|
||||
GetUserDetails,
|
||||
GetReplies,
|
||||
GetUserMentions,
|
||||
EditUserMention,
|
||||
GetModlog,
|
||||
BanFromCommunity,
|
||||
AddModToCommunity,
|
||||
|
|
|
@ -60,6 +60,12 @@ pub struct GetRepliesResponse {
|
|||
replies: Vec<ReplyView>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetUserMentionsResponse {
|
||||
op: String,
|
||||
mentions: Vec<UserMentionView>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MarkAllAsRead {
|
||||
auth: String,
|
||||
|
@ -103,6 +109,28 @@ pub struct GetReplies {
|
|||
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)]
|
||||
pub struct DeleteAccount {
|
||||
password: String,
|
||||
|
@ -299,7 +327,6 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
|||
None => false,
|
||||
};
|
||||
|
||||
//TODO add save
|
||||
let sort = SortType::from_str(&data.sort)?;
|
||||
|
||||
let user_details_id = match data.user_id {
|
||||
|
@ -541,7 +568,6 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
|||
data.limit,
|
||||
)?;
|
||||
|
||||
// Return the jwt
|
||||
Ok(GetRepliesResponse {
|
||||
op: self.op.to_string(),
|
||||
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> {
|
||||
fn perform(&self) -> Result<GetRepliesResponse, Error> {
|
||||
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 {
|
||||
op: self.op.to_string(),
|
||||
replies: replies,
|
||||
replies: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,6 @@ impl CommentView {
|
|||
|
||||
let (limit, offset) = limit_and_offset(page, limit);
|
||||
|
||||
// TODO no limits here?
|
||||
let mut query = comment_view.into_boxed();
|
||||
|
||||
// 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_view;
|
||||
pub mod user;
|
||||
pub mod user_mention;
|
||||
pub mod user_mention_view;
|
||||
pub mod user_view;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)]
|
||||
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]
|
||||
fn test_api() {
|
||||
assert_eq!(Settings::get().api_endpoint(), "rrr/api/v1");
|
||||
|
@ -131,9 +145,17 @@ mod tests {
|
|||
assert!(has_slurs(&test));
|
||||
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! {
|
||||
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 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 -> user_ (creator_id));
|
||||
joinable!(comment_like -> comment (comment_id));
|
||||
|
@ -303,6 +313,8 @@ 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,
|
||||
|
@ -329,4 +341,5 @@ allow_tables_to_appear_in_same_query!(
|
|||
site,
|
||||
user_,
|
||||
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()?;
|
||||
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 => {
|
||||
let mark_all_as_read: MarkAllAsRead = serde_json::from_str(data)?;
|
||||
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,
|
||||
CommentLikeForm,
|
||||
CommentForm as CommentFormI,
|
||||
EditUserMentionForm,
|
||||
SaveCommentForm,
|
||||
BanFromCommunityForm,
|
||||
BanUserForm,
|
||||
|
@ -686,16 +687,25 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
}
|
||||
|
||||
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);
|
||||
// if it has a user_mention_id field, then its a mention
|
||||
if (i.props.node.comment.user_mention_id) {
|
||||
let form: EditUserMentionForm = {
|
||||
user_mention_id: i.props.node.comment.user_mention_id,
|
||||
read: !i.props.node.comment.read,
|
||||
};
|
||||
WebSocketService.Instance.editUserMention(form);
|
||||
} else {
|
||||
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) {
|
||||
|
|
168
ui/src/components/inbox.tsx
vendored
168
ui/src/components/inbox.tsx
vendored
|
@ -8,6 +8,9 @@ import {
|
|||
SortType,
|
||||
GetRepliesForm,
|
||||
GetRepliesResponse,
|
||||
GetUserMentionsForm,
|
||||
GetUserMentionsResponse,
|
||||
UserMentionResponse,
|
||||
CommentResponse,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
|
@ -16,14 +19,22 @@ import { CommentNodes } from './comment-nodes';
|
|||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
enum UnreadType {
|
||||
enum UnreadOrAll {
|
||||
Unread,
|
||||
All,
|
||||
}
|
||||
|
||||
enum UnreadType {
|
||||
Both,
|
||||
Replies,
|
||||
Mentions,
|
||||
}
|
||||
|
||||
interface InboxState {
|
||||
unreadOrAll: UnreadOrAll;
|
||||
unreadType: UnreadType;
|
||||
replies: Array<Comment>;
|
||||
mentions: Array<Comment>;
|
||||
sort: SortType;
|
||||
page: number;
|
||||
}
|
||||
|
@ -31,8 +42,10 @@ interface InboxState {
|
|||
export class Inbox extends Component<any, InboxState> {
|
||||
private subscription: Subscription;
|
||||
private emptyState: InboxState = {
|
||||
unreadType: UnreadType.Unread,
|
||||
unreadOrAll: UnreadOrAll.Unread,
|
||||
unreadType: UnreadType.Both,
|
||||
replies: [],
|
||||
mentions: [],
|
||||
sort: SortType.New,
|
||||
page: 1,
|
||||
};
|
||||
|
@ -83,8 +96,8 @@ export class Inbox extends Component<any, InboxState> {
|
|||
</T>
|
||||
</span>
|
||||
</h5>
|
||||
{this.state.replies.length > 0 &&
|
||||
this.state.unreadType == UnreadType.Unread && (
|
||||
{this.state.replies.length + this.state.mentions.length > 0 &&
|
||||
this.state.unreadOrAll == UnreadOrAll.Unread && (
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
<li className="list-inline-item">
|
||||
<span class="pointer" onClick={this.markAllAsRead}>
|
||||
|
@ -94,7 +107,9 @@ export class Inbox extends Component<any, InboxState> {
|
|||
</ul>
|
||||
)}
|
||||
{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()}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -106,24 +121,42 @@ export class Inbox extends Component<any, InboxState> {
|
|||
return (
|
||||
<div className="mb-2">
|
||||
<select
|
||||
value={this.state.unreadType}
|
||||
onChange={linkEvent(this, this.handleUnreadTypeChange)}
|
||||
class="custom-select custom-select-sm w-auto"
|
||||
value={this.state.unreadOrAll}
|
||||
onChange={linkEvent(this, this.handleUnreadOrAllChange)}
|
||||
class="custom-select custom-select-sm w-auto mr-2"
|
||||
>
|
||||
<option disabled>
|
||||
<T i18nKey="type">#</T>
|
||||
</option>
|
||||
<option value={UnreadType.Unread}>
|
||||
<option value={UnreadOrAll.Unread}>
|
||||
<T i18nKey="unread">#</T>
|
||||
</option>
|
||||
<option value={UnreadType.All}>
|
||||
<option value={UnreadOrAll.All}>
|
||||
<T i18nKey="all">#</T>
|
||||
</option>
|
||||
</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
|
||||
value={this.state.sort}
|
||||
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>
|
||||
<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() {
|
||||
return (
|
||||
<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() {
|
||||
return (
|
||||
<div class="mt-2">
|
||||
|
@ -194,6 +268,13 @@ export class Inbox extends Component<any, InboxState> {
|
|||
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) {
|
||||
i.state.unreadType = Number(event.target.value);
|
||||
i.state.page = 1;
|
||||
|
@ -202,13 +283,21 @@ export class Inbox extends Component<any, InboxState> {
|
|||
}
|
||||
|
||||
refetch() {
|
||||
let form: GetRepliesForm = {
|
||||
let repliesForm: GetRepliesForm = {
|
||||
sort: SortType[this.state.sort],
|
||||
unread_only: this.state.unreadType == UnreadType.Unread,
|
||||
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
|
||||
page: this.state.page,
|
||||
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) {
|
||||
|
@ -228,13 +317,21 @@ export class Inbox extends Component<any, InboxState> {
|
|||
if (msg.error) {
|
||||
alert(i18n.t(msg.error));
|
||||
return;
|
||||
} else if (
|
||||
op == UserOperation.GetReplies ||
|
||||
op == UserOperation.MarkAllAsRead
|
||||
) {
|
||||
} else if (op == UserOperation.GetReplies) {
|
||||
let res: GetRepliesResponse = msg;
|
||||
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);
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.EditComment) {
|
||||
|
@ -250,7 +347,7 @@ export class Inbox extends Component<any, InboxState> {
|
|||
found.score = res.comment.score;
|
||||
|
||||
// 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(
|
||||
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);
|
||||
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);
|
||||
} else if (op == UserOperation.CreateComment) {
|
||||
// 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({
|
||||
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,
|
||||
GetRepliesForm,
|
||||
GetRepliesResponse,
|
||||
GetUserMentionsForm,
|
||||
GetUserMentionsResponse,
|
||||
SortType,
|
||||
GetSiteResponse,
|
||||
Comment,
|
||||
|
@ -21,6 +23,7 @@ interface NavbarState {
|
|||
expanded: boolean;
|
||||
expandUserDropdown: boolean;
|
||||
replies: Array<Comment>;
|
||||
mentions: Array<Comment>;
|
||||
fetchCount: number;
|
||||
unreadCount: number;
|
||||
siteName: string;
|
||||
|
@ -34,6 +37,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
unreadCount: 0,
|
||||
fetchCount: 0,
|
||||
replies: [],
|
||||
mentions: [],
|
||||
expanded: false,
|
||||
expandUserDropdown: false,
|
||||
siteName: undefined,
|
||||
|
@ -44,7 +48,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
this.state = this.emptyState;
|
||||
this.handleOverviewClick = this.handleOverviewClick.bind(this);
|
||||
|
||||
this.keepFetchingReplies();
|
||||
this.keepFetchingUnreads();
|
||||
|
||||
// Subscribe to user changes
|
||||
this.userSub = UserService.Instance.sub.subscribe(user => {
|
||||
|
@ -233,7 +237,22 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
}
|
||||
|
||||
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) {
|
||||
let res: GetSiteResponse = msg;
|
||||
|
||||
|
@ -245,12 +264,12 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
}
|
||||
}
|
||||
|
||||
keepFetchingReplies() {
|
||||
this.fetchReplies();
|
||||
setInterval(() => this.fetchReplies(), 15000);
|
||||
keepFetchingUnreads() {
|
||||
this.fetchUnreads();
|
||||
setInterval(() => this.fetchUnreads(), 15000);
|
||||
}
|
||||
|
||||
fetchReplies() {
|
||||
fetchUnreads() {
|
||||
if (this.state.isLoggedIn) {
|
||||
let repliesForm: GetRepliesForm = {
|
||||
sort: SortType[SortType.New],
|
||||
|
@ -258,8 +277,16 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
page: 1,
|
||||
limit: 9999,
|
||||
};
|
||||
|
||||
let userMentionsForm: GetUserMentionsForm = {
|
||||
sort: SortType[SortType.New],
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: 9999,
|
||||
};
|
||||
if (this.currentLocation !== '/inbox') {
|
||||
WebSocketService.Instance.getReplies(repliesForm);
|
||||
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
||||
this.state.fetchCount++;
|
||||
}
|
||||
}
|
||||
|
@ -269,13 +296,20 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
return this.context.router.history.location.pathname;
|
||||
}
|
||||
|
||||
sendRepliesCount(res: GetRepliesResponse) {
|
||||
sendUnreadCount() {
|
||||
UserService.Instance.sub.next({
|
||||
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() {
|
||||
if (UserService.Instance.user) {
|
||||
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;
|
||||
return (
|
||||
<div>
|
||||
{res && res.op && res.posts.length == 0 && res.comments.length == 0 && (
|
||||
<span>
|
||||
<T i18nKey="no_results">#</T>
|
||||
</span>
|
||||
)}
|
||||
{res &&
|
||||
res.op &&
|
||||
res.posts.length == 0 &&
|
||||
res.comments.length == 0 &&
|
||||
res.communities.length == 0 &&
|
||||
res.users.length == 0 && (
|
||||
<span>
|
||||
<T i18nKey="no_results">#</T>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -420,7 +425,6 @@ export class Search extends Component<any, SearchState> {
|
|||
}
|
||||
|
||||
search() {
|
||||
// TODO community
|
||||
let form: SearchForm = {
|
||||
q: this.state.q,
|
||||
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';
|
||||
|
||||
// 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 = {
|
||||
en,
|
||||
eo,
|
||||
|
@ -30,7 +29,7 @@ function format(value: any, format: any, lng: any) {
|
|||
}
|
||||
|
||||
i18n.init({
|
||||
debug: true,
|
||||
debug: false,
|
||||
// load: 'languageOnly',
|
||||
|
||||
// initImmediate: false,
|
||||
|
|
30
ui/src/interfaces.ts
vendored
30
ui/src/interfaces.ts
vendored
|
@ -20,6 +20,8 @@ export enum UserOperation {
|
|||
GetFollowedCommunities,
|
||||
GetUserDetails,
|
||||
GetReplies,
|
||||
GetUserMentions,
|
||||
EditUserMention,
|
||||
GetModlog,
|
||||
BanFromCommunity,
|
||||
AddModToCommunity,
|
||||
|
@ -171,6 +173,8 @@ export interface Comment {
|
|||
user_id?: number;
|
||||
my_vote?: number;
|
||||
saved?: boolean;
|
||||
user_mention_id?: number; // For mention type
|
||||
recipient_id?: number;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
|
@ -229,7 +233,7 @@ export interface UserDetailsResponse {
|
|||
}
|
||||
|
||||
export interface GetRepliesForm {
|
||||
sort: string; // TODO figure this one out
|
||||
sort: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
unread_only: boolean;
|
||||
|
@ -241,6 +245,30 @@ export interface GetRepliesResponse {
|
|||
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 {
|
||||
community_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,
|
||||
UserView,
|
||||
GetRepliesForm,
|
||||
GetUserMentionsForm,
|
||||
EditUserMentionForm,
|
||||
SearchForm,
|
||||
UserSettingsForm,
|
||||
DeleteAccountForm,
|
||||
|
@ -222,6 +224,16 @@ export class WebSocketService {
|
|||
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) {
|
||||
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',
|
||||
type: 'Type',
|
||||
unread: 'Unread',
|
||||
replies: 'Replies',
|
||||
mentions: 'Mentions',
|
||||
reply_sent: 'Reply sent',
|
||||
search: 'Search',
|
||||
overview: 'Overview',
|
||||
|
|
Loading…
Reference in a new issue