--- /dev/null
+drop view user_mention_view;
+drop table user_mention;
--- /dev/null
+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;
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,
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 {
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};
GetFollowedCommunities,
GetUserDetails,
GetReplies,
+ GetUserMentions,
+ EditUserMention,
GetModlog,
BanFromCommunity,
AddModToCommunity,
replies: Vec<ReplyView>,
}
+#[derive(Serialize, Deserialize)]
+pub struct GetUserMentionsResponse {
+ op: String,
+ mentions: Vec<UserMentionView>,
+}
+
#[derive(Serialize, Deserialize)]
pub struct MarkAllAsRead {
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)]
pub struct DeleteAccount {
password: String,
None => false,
};
- //TODO add save
let sort = SortType::from_str(&data.sort)?;
let user_details_id = match data.user_id {
data.limit,
)?;
- // Return the jwt
Ok(GetRepliesResponse {
op: self.op.to_string(),
replies: replies,
}
}
+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;
};
}
- 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![],
})
}
}
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
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> {
--- /dev/null
+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,
+);
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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)
+ }
+}
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");
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();
}
}
}
+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!(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,
site,
user_,
user_ban,
+ user_mention,
);
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()?;
CommentNode as CommentNodeI,
CommentLikeForm,
CommentForm as CommentFormI,
+ EditUserMentionForm,
SaveCommentForm,
BanFromCommunityForm,
BanUserForm,
}
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) {
SortType,
GetRepliesForm,
GetRepliesResponse,
+ GetUserMentionsForm,
+ GetUserMentionsResponse,
+ UserMentionResponse,
CommentResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
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;
}
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,
};
</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}>
</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>
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>
);
}
+ 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>
);
}
+ mentions() {
+ return (
+ <div>
+ {this.state.mentions.map(mention => (
+ <CommentNodes nodes={[{ comment: mention }]} noIndent markable />
+ ))}
+ </div>
+ );
+ }
+
paginator() {
return (
<div class="mt-2">
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;
}
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) {
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) {
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
);
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;
}
}
- 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,
});
}
}
UserOperation,
GetRepliesForm,
GetRepliesResponse,
+ GetUserMentionsForm,
+ GetUserMentionsResponse,
SortType,
GetSiteResponse,
Comment,
expanded: boolean;
expandUserDropdown: boolean;
replies: Array<Comment>;
+ mentions: Array<Comment>;
fetchCount: number;
unreadCount: number;
siteName: string;
unreadCount: 0,
fetchCount: 0,
replies: [],
+ mentions: [],
expanded: false,
expandUserDropdown: false,
siteName: undefined,
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 => {
}
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;
}
}
- 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],
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++;
}
}
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() {
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>
);
}
}
search() {
- // TODO community
let form: SearchForm = {
q: this.state.q,
type_: SearchType[this.state.type_],
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,
}
i18n.init({
- debug: true,
+ debug: false,
// load: 'languageOnly',
// initImmediate: false,
GetFollowedCommunities,
GetUserDetails,
GetReplies,
+ GetUserMentions,
+ EditUserMention,
GetModlog,
BanFromCommunity,
AddModToCommunity,
user_id?: number;
my_vote?: number;
saved?: boolean;
+ user_mention_id?: number; // For mention type
+ recipient_id?: number;
}
export interface Category {
}
export interface GetRepliesForm {
- sort: string; // TODO figure this one out
+ sort: string;
page?: number;
limit?: number;
unread_only: boolean;
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;
Site,
UserView,
GetRepliesForm,
+ GetUserMentionsForm,
+ EditUserMentionForm,
SearchForm,
UserSettingsForm,
DeleteAccountForm,
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));
}
mark_all_as_read: 'mark all as read',
type: 'Type',
unread: 'Unread',
+ replies: 'Replies',
+ mentions: 'Mentions',
reply_sent: 'Reply sent',
search: 'Search',
overview: 'Overview',