mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-12-23 11:21:32 +00:00
Merge branch 'private_messaging' into dev
This commit is contained in:
commit
0d2b4e6f18
34 changed files with 1560 additions and 46 deletions
18
README.md
vendored
18
README.md
vendored
|
@ -157,15 +157,15 @@ If you'd like to add translations, take a look a look at the [English translatio
|
|||
|
||||
lang | done | missing
|
||||
--- | --- | ---
|
||||
de | 93% | avatar,upload_avatar,show_avatars,docs,old_password,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists
|
||||
eo | 80% | number_of_communities,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,donate_to_lemmy,donate,are_you_sure,yes,no,email_already_exists
|
||||
es | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists
|
||||
fr | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists
|
||||
it | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists
|
||||
nl | 99% | donate_to_lemmy,donate,email_already_exists
|
||||
ru | 77% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,donate_to_lemmy,donate,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no,email_already_exists
|
||||
sv | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists
|
||||
zh | 75% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no,email_already_exists
|
||||
de | 88% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||
eo | 76% | number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,donate_to_lemmy,donate,from,are_you_sure,yes,no,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||
es | 84% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||
fr | 84% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||
it | 85% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||
nl | 93% | create_private_message,send_secure_message,send_message,message,message_sent,messages,matrix_user_id,private_message_disclaimer,donate_to_lemmy,donate,from,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||
ru | 72% | cross_posts,cross_post,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||
sv | 84% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||
zh | 71% | cross_posts,cross_post,users,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||
|
||||
<!-- translationsstop -->
|
||||
|
||||
|
|
34
server/migrations/2020-01-21-001001_create_private_message/down.sql
vendored
Normal file
34
server/migrations/2020-01-21-001001_create_private_message/down.sql
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
-- Drop the triggers
|
||||
drop trigger refresh_private_message on private_message;
|
||||
drop function refresh_private_message();
|
||||
|
||||
-- Drop the view and table
|
||||
drop view private_message_view cascade;
|
||||
drop table private_message;
|
||||
|
||||
-- Rebuild the old views
|
||||
drop view user_view cascade;
|
||||
create view user_view as
|
||||
select
|
||||
u.id,
|
||||
u.name,
|
||||
u.avatar,
|
||||
u.email,
|
||||
u.fedi_name,
|
||||
u.admin,
|
||||
u.banned,
|
||||
u.show_avatars,
|
||||
u.send_notifications_to_email,
|
||||
u.published,
|
||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||
from user_ u;
|
||||
|
||||
create materialized view user_mview as select * from user_view;
|
||||
|
||||
create unique index idx_user_mview_id on user_mview (id);
|
||||
|
||||
-- Drop the columns
|
||||
alter table user_ drop column matrix_user_id;
|
90
server/migrations/2020-01-21-001001_create_private_message/up.sql
vendored
Normal file
90
server/migrations/2020-01-21-001001_create_private_message/up.sql
vendored
Normal file
|
@ -0,0 +1,90 @@
|
|||
-- Creating private message
|
||||
create table private_message (
|
||||
id serial primary key,
|
||||
creator_id int references user_ on update cascade on delete cascade not null,
|
||||
recipient_id int references user_ on update cascade on delete cascade not null,
|
||||
content text not null,
|
||||
deleted boolean default false not null,
|
||||
read boolean default false not null,
|
||||
published timestamp not null default now(),
|
||||
updated timestamp
|
||||
);
|
||||
|
||||
-- Create the view and materialized view which has the avatar and creator name
|
||||
create view private_message_view as
|
||||
select
|
||||
pm.*,
|
||||
u.name as creator_name,
|
||||
u.avatar as creator_avatar,
|
||||
u2.name as recipient_name,
|
||||
u2.avatar as recipient_avatar
|
||||
from private_message pm
|
||||
inner join user_ u on u.id = pm.creator_id
|
||||
inner join user_ u2 on u2.id = pm.recipient_id;
|
||||
|
||||
create materialized view private_message_mview as select * from private_message_view;
|
||||
|
||||
create unique index idx_private_message_mview_id on private_message_mview (id);
|
||||
|
||||
-- Create the triggers
|
||||
create or replace function refresh_private_message()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently private_message_mview;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_private_message
|
||||
after insert or update or delete or truncate
|
||||
on private_message
|
||||
for each statement
|
||||
execute procedure refresh_private_message();
|
||||
|
||||
-- Update user to include matrix id
|
||||
alter table user_ add column matrix_user_id text unique;
|
||||
|
||||
drop view user_view cascade;
|
||||
create view user_view as
|
||||
select
|
||||
u.id,
|
||||
u.name,
|
||||
u.avatar,
|
||||
u.email,
|
||||
u.matrix_user_id,
|
||||
u.fedi_name,
|
||||
u.admin,
|
||||
u.banned,
|
||||
u.show_avatars,
|
||||
u.send_notifications_to_email,
|
||||
u.published,
|
||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||
from user_ u;
|
||||
|
||||
create materialized view user_mview as select * from user_view;
|
||||
|
||||
create unique index idx_user_mview_id on user_mview (id);
|
||||
|
||||
-- This is what a group pm table would look like
|
||||
-- Not going to do it now because of the complications
|
||||
--
|
||||
-- create table private_message (
|
||||
-- id serial primary key,
|
||||
-- creator_id int references user_ on update cascade on delete cascade not null,
|
||||
-- content text not null,
|
||||
-- deleted boolean default false not null,
|
||||
-- published timestamp not null default now(),
|
||||
-- updated timestamp
|
||||
-- );
|
||||
--
|
||||
-- create table private_message_recipient (
|
||||
-- id serial primary key,
|
||||
-- private_message_id int references private_message on update cascade on delete cascade not null,
|
||||
-- recipient_id int references user_ on update cascade on delete cascade not null,
|
||||
-- read boolean default false not null,
|
||||
-- published timestamp not null default now(),
|
||||
-- unique(private_message_id, recipient_id)
|
||||
-- )
|
|
@ -7,7 +7,7 @@ use diesel::PgConnection;
|
|||
pub struct CreateComment {
|
||||
content: String,
|
||||
parent_id: Option<i32>,
|
||||
edit_id: Option<i32>,
|
||||
edit_id: Option<i32>, // TODO this isn't used
|
||||
pub post_id: i32,
|
||||
auth: String,
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ pub struct CreateComment {
|
|||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EditComment {
|
||||
content: String,
|
||||
parent_id: Option<i32>,
|
||||
parent_id: Option<i32>, // TODO why are the parent_id, creator_id, post_id, etc fields required? They aren't going to change
|
||||
edit_id: i32,
|
||||
creator_id: i32,
|
||||
pub post_id: i32,
|
||||
|
|
|
@ -8,6 +8,8 @@ use crate::db::moderator_views::*;
|
|||
use crate::db::password_reset_request::*;
|
||||
use crate::db::post::*;
|
||||
use crate::db::post_view::*;
|
||||
use crate::db::private_message::*;
|
||||
use crate::db::private_message_view::*;
|
||||
use crate::db::site::*;
|
||||
use crate::db::site_view::*;
|
||||
use crate::db::user::*;
|
||||
|
@ -67,6 +69,9 @@ pub enum UserOperation {
|
|||
DeleteAccount,
|
||||
PasswordReset,
|
||||
PasswordChange,
|
||||
CreatePrivateMessage,
|
||||
EditPrivateMessage,
|
||||
GetPrivateMessages,
|
||||
}
|
||||
|
||||
#[derive(Fail, Debug)]
|
||||
|
|
|
@ -30,6 +30,7 @@ pub struct SaveUserSettings {
|
|||
lang: String,
|
||||
avatar: Option<String>,
|
||||
email: Option<String>,
|
||||
matrix_user_id: Option<String>,
|
||||
new_password: Option<String>,
|
||||
new_password_verify: Option<String>,
|
||||
old_password: Option<String>,
|
||||
|
@ -167,6 +168,42 @@ pub struct PasswordChange {
|
|||
password_verify: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreatePrivateMessage {
|
||||
content: String,
|
||||
recipient_id: i32,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EditPrivateMessage {
|
||||
edit_id: i32,
|
||||
content: Option<String>,
|
||||
deleted: Option<bool>,
|
||||
read: Option<bool>,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetPrivateMessages {
|
||||
unread_only: bool,
|
||||
page: Option<i64>,
|
||||
limit: Option<i64>,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct PrivateMessagesResponse {
|
||||
op: String,
|
||||
messages: Vec<PrivateMessageView>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct PrivateMessageResponse {
|
||||
op: String,
|
||||
message: PrivateMessageView,
|
||||
}
|
||||
|
||||
impl Perform<LoginResponse> for Oper<Login> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
|
||||
let data: &Login = &self.data;
|
||||
|
@ -221,6 +258,7 @@ impl Perform<LoginResponse> for Oper<Register> {
|
|||
name: data.username.to_owned(),
|
||||
fedi_name: Settings::get().hostname.to_owned(),
|
||||
email: data.email.to_owned(),
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
password_encrypted: data.password.to_owned(),
|
||||
preferred_username: None,
|
||||
|
@ -357,6 +395,7 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
|||
name: read_user.name,
|
||||
fedi_name: read_user.fedi_name,
|
||||
email,
|
||||
matrix_user_id: data.matrix_user_id.to_owned(),
|
||||
avatar: data.avatar.to_owned(),
|
||||
password_encrypted,
|
||||
preferred_username: read_user.preferred_username,
|
||||
|
@ -504,10 +543,12 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
|||
|
||||
let read_user = User_::read(&conn, data.user_id)?;
|
||||
|
||||
// TODO make addadmin easier
|
||||
let user_form = UserForm {
|
||||
name: read_user.name,
|
||||
fedi_name: read_user.fedi_name,
|
||||
email: read_user.email,
|
||||
matrix_user_id: read_user.matrix_user_id,
|
||||
avatar: read_user.avatar,
|
||||
password_encrypted: read_user.password_encrypted,
|
||||
preferred_username: read_user.preferred_username,
|
||||
|
@ -568,10 +609,12 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
|||
|
||||
let read_user = User_::read(&conn, data.user_id)?;
|
||||
|
||||
// TODO make bans and addadmins easier
|
||||
let user_form = UserForm {
|
||||
name: read_user.name,
|
||||
fedi_name: read_user.fedi_name,
|
||||
email: read_user.email,
|
||||
matrix_user_id: read_user.matrix_user_id,
|
||||
avatar: read_user.avatar,
|
||||
password_encrypted: read_user.password_encrypted,
|
||||
preferred_username: read_user.preferred_username,
|
||||
|
@ -762,6 +805,30 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
|||
};
|
||||
}
|
||||
|
||||
// messages
|
||||
let messages = PrivateMessageQueryBuilder::create(&conn, user_id)
|
||||
.page(1)
|
||||
.limit(999)
|
||||
.unread_only(true)
|
||||
.list()?;
|
||||
|
||||
for message in &messages {
|
||||
let private_message_form = PrivateMessageForm {
|
||||
content: None,
|
||||
creator_id: message.to_owned().creator_id,
|
||||
recipient_id: message.to_owned().recipient_id,
|
||||
deleted: None,
|
||||
read: Some(true),
|
||||
updated: None,
|
||||
};
|
||||
|
||||
let _updated_message = match PrivateMessage::update(&conn, message.id, &private_message_form)
|
||||
{
|
||||
Ok(message) => message,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_private_message").into()),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(GetRepliesResponse {
|
||||
op: self.op.to_string(),
|
||||
replies: vec![],
|
||||
|
@ -905,3 +972,150 @@ impl Perform<LoginResponse> for Oper<PasswordChange> {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<PrivateMessageResponse> for Oper<CreatePrivateMessage> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
|
||||
let data: &CreatePrivateMessage = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
let hostname = &format!("https://{}", Settings::get().hostname);
|
||||
|
||||
// Check for a site ban
|
||||
if UserView::read(&conn, user_id)?.banned {
|
||||
return Err(APIError::err(&self.op, "site_ban").into());
|
||||
}
|
||||
|
||||
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
||||
|
||||
let private_message_form = PrivateMessageForm {
|
||||
content: Some(content_slurs_removed.to_owned()),
|
||||
creator_id: user_id,
|
||||
recipient_id: data.recipient_id,
|
||||
deleted: None,
|
||||
read: None,
|
||||
updated: None,
|
||||
};
|
||||
|
||||
let inserted_private_message = match PrivateMessage::create(&conn, &private_message_form) {
|
||||
Ok(private_message) => private_message,
|
||||
Err(_e) => {
|
||||
return Err(APIError::err(&self.op, "couldnt_create_private_message").into());
|
||||
}
|
||||
};
|
||||
|
||||
// Send notifications to the recipient
|
||||
let recipient_user = User_::read(&conn, data.recipient_id)?;
|
||||
if recipient_user.send_notifications_to_email {
|
||||
if let Some(email) = recipient_user.email {
|
||||
let subject = &format!(
|
||||
"{} - Private Message from {}",
|
||||
Settings::get().hostname,
|
||||
claims.username
|
||||
);
|
||||
let html = &format!(
|
||||
"<h1>Private Message</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||
claims.username, &content_slurs_removed, hostname
|
||||
);
|
||||
match send_email(subject, &email, &recipient_user.name, html) {
|
||||
Ok(_o) => _o,
|
||||
Err(e) => eprintln!("{}", e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let private_message_view = PrivateMessageView::read(&conn, inserted_private_message.id)?;
|
||||
|
||||
Ok(PrivateMessageResponse {
|
||||
op: self.op.to_string(),
|
||||
message: private_message_view,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<PrivateMessageResponse> for Oper<EditPrivateMessage> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
|
||||
let data: &EditPrivateMessage = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
let orig_private_message = PrivateMessage::read(&conn, data.edit_id)?;
|
||||
|
||||
// Check for a site ban
|
||||
if UserView::read(&conn, user_id)?.banned {
|
||||
return Err(APIError::err(&self.op, "site_ban").into());
|
||||
}
|
||||
|
||||
// Check to make sure they are the creator (or the recipient marking as read
|
||||
if !(data.read.is_some() && orig_private_message.recipient_id.eq(&user_id)
|
||||
|| orig_private_message.creator_id.eq(&user_id))
|
||||
{
|
||||
return Err(APIError::err(&self.op, "no_private_message_edit_allowed").into());
|
||||
}
|
||||
|
||||
let content_slurs_removed = match &data.content {
|
||||
Some(content) => Some(remove_slurs(content)),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let private_message_form = PrivateMessageForm {
|
||||
content: content_slurs_removed,
|
||||
creator_id: orig_private_message.creator_id,
|
||||
recipient_id: orig_private_message.recipient_id,
|
||||
deleted: data.deleted.to_owned(),
|
||||
read: data.read.to_owned(),
|
||||
updated: if data.read.is_some() {
|
||||
orig_private_message.updated
|
||||
} else {
|
||||
Some(naive_now())
|
||||
},
|
||||
};
|
||||
|
||||
let _updated_private_message =
|
||||
match PrivateMessage::update(&conn, data.edit_id, &private_message_form) {
|
||||
Ok(private_message) => private_message,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_private_message").into()),
|
||||
};
|
||||
|
||||
let private_message_view = PrivateMessageView::read(&conn, data.edit_id)?;
|
||||
|
||||
Ok(PrivateMessageResponse {
|
||||
op: self.op.to_string(),
|
||||
message: private_message_view,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<PrivateMessagesResponse> for Oper<GetPrivateMessages> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PrivateMessagesResponse, Error> {
|
||||
let data: &GetPrivateMessages = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
let messages = PrivateMessageQueryBuilder::create(&conn, user_id)
|
||||
.page(data.page)
|
||||
.limit(data.limit)
|
||||
.unread_only(data.unread_only)
|
||||
.list()?;
|
||||
|
||||
Ok(PrivateMessagesResponse {
|
||||
op: self.op.to_string(),
|
||||
messages,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "here".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
published: naive_now(),
|
||||
admin: false,
|
||||
|
|
|
@ -174,6 +174,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
|
|
@ -398,6 +398,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
|
|
@ -220,6 +220,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
|
|
@ -15,6 +15,8 @@ pub mod moderator_views;
|
|||
pub mod password_reset_request;
|
||||
pub mod post;
|
||||
pub mod post_view;
|
||||
pub mod private_message;
|
||||
pub mod private_message_view;
|
||||
pub mod site;
|
||||
pub mod site_view;
|
||||
pub mod user;
|
||||
|
|
|
@ -442,6 +442,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
@ -463,6 +464,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
|
|
@ -92,6 +92,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
|
|
@ -187,6 +187,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
|
|
@ -339,6 +339,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
updated: None,
|
||||
admin: false,
|
||||
|
|
144
server/src/db/private_message.rs
Normal file
144
server/src/db/private_message.rs
Normal file
|
@ -0,0 +1,144 @@
|
|||
use super::*;
|
||||
use crate::schema::private_message;
|
||||
|
||||
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||
#[table_name = "private_message"]
|
||||
pub struct PrivateMessage {
|
||||
pub id: i32,
|
||||
pub creator_id: i32,
|
||||
pub recipient_id: i32,
|
||||
pub content: String,
|
||||
pub deleted: bool,
|
||||
pub read: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Clone)]
|
||||
#[table_name = "private_message"]
|
||||
pub struct PrivateMessageForm {
|
||||
pub creator_id: i32,
|
||||
pub recipient_id: i32,
|
||||
pub content: Option<String>,
|
||||
pub deleted: Option<bool>,
|
||||
pub read: Option<bool>,
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
}
|
||||
|
||||
impl Crud<PrivateMessageForm> for PrivateMessage {
|
||||
fn read(conn: &PgConnection, private_message_id: i32) -> Result<Self, Error> {
|
||||
use crate::schema::private_message::dsl::*;
|
||||
private_message.find(private_message_id).first::<Self>(conn)
|
||||
}
|
||||
|
||||
fn delete(conn: &PgConnection, private_message_id: i32) -> Result<usize, Error> {
|
||||
use crate::schema::private_message::dsl::*;
|
||||
diesel::delete(private_message.find(private_message_id)).execute(conn)
|
||||
}
|
||||
|
||||
fn create(conn: &PgConnection, private_message_form: &PrivateMessageForm) -> Result<Self, Error> {
|
||||
use crate::schema::private_message::dsl::*;
|
||||
insert_into(private_message)
|
||||
.values(private_message_form)
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
fn update(
|
||||
conn: &PgConnection,
|
||||
private_message_id: i32,
|
||||
private_message_form: &PrivateMessageForm,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::private_message::dsl::*;
|
||||
diesel::update(private_message.find(private_message_id))
|
||||
.set(private_message_form)
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::user::*;
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
let conn = establish_unpooled_connection();
|
||||
|
||||
let creator_form = UserForm {
|
||||
name: "creator_pm".into(),
|
||||
fedi_name: "rrf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
show_nsfw: false,
|
||||
theme: "darkly".into(),
|
||||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_creator = User_::create(&conn, &creator_form).unwrap();
|
||||
|
||||
let recipient_form = UserForm {
|
||||
name: "recipient_pm".into(),
|
||||
fedi_name: "rrf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
show_nsfw: false,
|
||||
theme: "darkly".into(),
|
||||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
|
||||
|
||||
let private_message_form = PrivateMessageForm {
|
||||
content: Some("A test private message".into()),
|
||||
creator_id: inserted_creator.id,
|
||||
recipient_id: inserted_recipient.id,
|
||||
deleted: None,
|
||||
read: None,
|
||||
updated: None,
|
||||
};
|
||||
|
||||
let inserted_private_message = PrivateMessage::create(&conn, &private_message_form).unwrap();
|
||||
|
||||
let expected_private_message = PrivateMessage {
|
||||
id: inserted_private_message.id,
|
||||
content: "A test private message".into(),
|
||||
creator_id: inserted_creator.id,
|
||||
recipient_id: inserted_recipient.id,
|
||||
deleted: false,
|
||||
read: false,
|
||||
updated: None,
|
||||
published: inserted_private_message.published,
|
||||
};
|
||||
|
||||
let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();
|
||||
let updated_private_message =
|
||||
PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap();
|
||||
let num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap();
|
||||
User_::delete(&conn, inserted_creator.id).unwrap();
|
||||
User_::delete(&conn, inserted_recipient.id).unwrap();
|
||||
|
||||
assert_eq!(expected_private_message, read_private_message);
|
||||
assert_eq!(expected_private_message, updated_private_message);
|
||||
assert_eq!(expected_private_message, inserted_private_message);
|
||||
assert_eq!(1, num_deleted);
|
||||
}
|
||||
}
|
140
server/src/db/private_message_view.rs
Normal file
140
server/src/db/private_message_view.rs
Normal file
|
@ -0,0 +1,140 @@
|
|||
use super::*;
|
||||
use diesel::pg::Pg;
|
||||
|
||||
// The faked schema since diesel doesn't do views
|
||||
table! {
|
||||
private_message_view (id) {
|
||||
id -> Int4,
|
||||
creator_id -> Int4,
|
||||
recipient_id -> Int4,
|
||||
content -> Text,
|
||||
deleted -> Bool,
|
||||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
recipient_name -> Varchar,
|
||||
recipient_avatar -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
private_message_mview (id) {
|
||||
id -> Int4,
|
||||
creator_id -> Int4,
|
||||
recipient_id -> Int4,
|
||||
content -> Text,
|
||||
deleted -> Bool,
|
||||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
recipient_name -> Varchar,
|
||||
recipient_avatar -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
|
||||
)]
|
||||
#[table_name = "private_message_view"]
|
||||
pub struct PrivateMessageView {
|
||||
pub id: i32,
|
||||
pub creator_id: i32,
|
||||
pub recipient_id: i32,
|
||||
pub content: String,
|
||||
pub deleted: bool,
|
||||
pub read: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub recipient_name: String,
|
||||
pub recipient_avatar: Option<String>,
|
||||
}
|
||||
|
||||
pub struct PrivateMessageQueryBuilder<'a> {
|
||||
conn: &'a PgConnection,
|
||||
query: super::private_message_view::private_message_mview::BoxedQuery<'a, Pg>,
|
||||
for_recipient_id: i32,
|
||||
unread_only: bool,
|
||||
page: Option<i64>,
|
||||
limit: Option<i64>,
|
||||
}
|
||||
|
||||
impl<'a> PrivateMessageQueryBuilder<'a> {
|
||||
pub fn create(conn: &'a PgConnection, for_recipient_id: i32) -> Self {
|
||||
use super::private_message_view::private_message_mview::dsl::*;
|
||||
|
||||
let query = private_message_mview.into_boxed();
|
||||
|
||||
PrivateMessageQueryBuilder {
|
||||
conn,
|
||||
query,
|
||||
for_recipient_id,
|
||||
unread_only: false,
|
||||
page: None,
|
||||
limit: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unread_only(mut self, unread_only: bool) -> Self {
|
||||
self.unread_only = unread_only;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
|
||||
self.page = page.get_optional();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
|
||||
self.limit = limit.get_optional();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn list(self) -> Result<Vec<PrivateMessageView>, Error> {
|
||||
use super::private_message_view::private_message_mview::dsl::*;
|
||||
|
||||
let mut query = self.query;
|
||||
|
||||
// If its unread, I only want the ones to me
|
||||
if self.unread_only {
|
||||
query = query
|
||||
.filter(read.eq(false))
|
||||
.filter(recipient_id.eq(self.for_recipient_id));
|
||||
}
|
||||
// Otherwise, I want the ALL view to show both sent and received
|
||||
else {
|
||||
query = query.filter(
|
||||
recipient_id
|
||||
.eq(self.for_recipient_id)
|
||||
.or(creator_id.eq(self.for_recipient_id)),
|
||||
)
|
||||
}
|
||||
|
||||
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
||||
|
||||
query
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.order_by(published.desc())
|
||||
.load::<PrivateMessageView>(self.conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivateMessageView {
|
||||
pub fn read(conn: &PgConnection, from_private_message_id: i32) -> Result<Self, Error> {
|
||||
use super::private_message_view::private_message_view::dsl::*;
|
||||
|
||||
let mut query = private_message_view.into_boxed();
|
||||
|
||||
query = query
|
||||
.filter(id.eq(from_private_message_id))
|
||||
.order_by(published.desc());
|
||||
|
||||
query.first::<Self>(conn)
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ pub struct User_ {
|
|||
pub lang: String,
|
||||
pub show_avatars: bool,
|
||||
pub send_notifications_to_email: bool,
|
||||
pub matrix_user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Clone)]
|
||||
|
@ -47,6 +48,7 @@ pub struct UserForm {
|
|||
pub lang: String,
|
||||
pub show_avatars: bool,
|
||||
pub send_notifications_to_email: bool,
|
||||
pub matrix_user_id: Option<String>,
|
||||
}
|
||||
|
||||
impl Crud<UserForm> for User_ {
|
||||
|
@ -184,6 +186,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
@ -206,6 +209,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
|
|
@ -68,6 +68,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
@ -89,6 +90,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
|
|
@ -8,6 +8,7 @@ table! {
|
|||
name -> Varchar,
|
||||
avatar -> Nullable<Text>,
|
||||
email -> Nullable<Text>,
|
||||
matrix_user_id -> Nullable<Text>,
|
||||
fedi_name -> Varchar,
|
||||
admin -> Bool,
|
||||
banned -> Bool,
|
||||
|
@ -27,6 +28,7 @@ table! {
|
|||
name -> Varchar,
|
||||
avatar -> Nullable<Text>,
|
||||
email -> Nullable<Text>,
|
||||
matrix_user_id -> Nullable<Text>,
|
||||
fedi_name -> Varchar,
|
||||
admin -> Bool,
|
||||
banned -> Bool,
|
||||
|
@ -49,6 +51,7 @@ pub struct UserView {
|
|||
pub name: String,
|
||||
pub avatar: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub matrix_user_id: Option<String>,
|
||||
pub fedi_name: String,
|
||||
pub admin: bool,
|
||||
pub banned: bool,
|
||||
|
|
|
@ -12,6 +12,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||
.route("/login", web::get().to(index))
|
||||
.route("/create_post", web::get().to(index))
|
||||
.route("/create_community", web::get().to(index))
|
||||
.route("/create_private_message", web::get().to(index))
|
||||
.route("/communities/page/{page}", web::get().to(index))
|
||||
.route("/communities", web::get().to(index))
|
||||
.route("/post/{id}/comment/{id2}", web::get().to(index))
|
||||
|
|
|
@ -238,6 +238,19 @@ table! {
|
|||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
private_message (id) {
|
||||
id -> Int4,
|
||||
creator_id -> Int4,
|
||||
recipient_id -> Int4,
|
||||
content -> Text,
|
||||
deleted -> Bool,
|
||||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
site (id) {
|
||||
id -> Int4,
|
||||
|
@ -272,6 +285,7 @@ table! {
|
|||
lang -> Varchar,
|
||||
show_avatars -> Bool,
|
||||
send_notifications_to_email -> Bool,
|
||||
matrix_user_id -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -357,6 +371,7 @@ allow_tables_to_appear_in_same_query!(
|
|||
post_like,
|
||||
post_read,
|
||||
post_saved,
|
||||
private_message,
|
||||
site,
|
||||
user_,
|
||||
user_ban,
|
||||
|
|
|
@ -547,5 +547,21 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
|||
let res = Oper::new(user_operation, password_change).perform(&conn)?;
|
||||
Ok(serde_json::to_string(&res)?)
|
||||
}
|
||||
UserOperation::CreatePrivateMessage => {
|
||||
chat.check_rate_limit_message(msg.id)?;
|
||||
let create_private_message: CreatePrivateMessage = serde_json::from_str(data)?;
|
||||
let res = Oper::new(user_operation, create_private_message).perform(&conn)?;
|
||||
Ok(serde_json::to_string(&res)?)
|
||||
}
|
||||
UserOperation::EditPrivateMessage => {
|
||||
let edit_private_message: EditPrivateMessage = serde_json::from_str(data)?;
|
||||
let res = Oper::new(user_operation, edit_private_message).perform(&conn)?;
|
||||
Ok(serde_json::to_string(&res)?)
|
||||
}
|
||||
UserOperation::GetPrivateMessages => {
|
||||
let messages: GetPrivateMessages = serde_json::from_str(data)?;
|
||||
let res = Oper::new(user_operation, messages).perform(&conn)?;
|
||||
Ok(serde_json::to_string(&res)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
52
ui/src/components/create-private-message.tsx
vendored
Normal file
52
ui/src/components/create-private-message.tsx
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { Component } from 'inferno';
|
||||
import { PrivateMessageForm } from './private-message-form';
|
||||
import { WebSocketService } from '../services';
|
||||
import { PrivateMessageFormParams } from '../interfaces';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
export class CreatePrivateMessage extends Component<any, any> {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = `${i18n.t('create_private_message')} - ${
|
||||
WebSocketService.Instance.site.name
|
||||
}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||
<h5>{i18n.t('create_private_message')}</h5>
|
||||
<PrivateMessageForm
|
||||
onCreate={this.handlePrivateMessageCreate}
|
||||
params={this.params}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
get params(): PrivateMessageFormParams {
|
||||
let urlParams = new URLSearchParams(this.props.location.search);
|
||||
let params: PrivateMessageFormParams = {
|
||||
recipient_id: Number(urlParams.get('recipient_id')),
|
||||
};
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
handlePrivateMessageCreate() {
|
||||
alert(i18n.t('message_sent'));
|
||||
|
||||
// Navigate to the front
|
||||
this.props.history.push(`/`);
|
||||
}
|
||||
}
|
117
ui/src/components/inbox.tsx
vendored
117
ui/src/components/inbox.tsx
vendored
|
@ -12,10 +12,15 @@ import {
|
|||
GetUserMentionsResponse,
|
||||
UserMentionResponse,
|
||||
CommentResponse,
|
||||
PrivateMessage as PrivateMessageI,
|
||||
GetPrivateMessagesForm,
|
||||
PrivateMessagesResponse,
|
||||
PrivateMessageResponse,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import { msgOp, fetchLimit } from '../utils';
|
||||
import { msgOp, fetchLimit, isCommentType } from '../utils';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
import { PrivateMessage } from './private-message';
|
||||
import { SortSelect } from './sort-select';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
@ -26,9 +31,10 @@ enum UnreadOrAll {
|
|||
}
|
||||
|
||||
enum UnreadType {
|
||||
Both,
|
||||
All,
|
||||
Replies,
|
||||
Mentions,
|
||||
Messages,
|
||||
}
|
||||
|
||||
interface InboxState {
|
||||
|
@ -36,6 +42,7 @@ interface InboxState {
|
|||
unreadType: UnreadType;
|
||||
replies: Array<Comment>;
|
||||
mentions: Array<Comment>;
|
||||
messages: Array<PrivateMessageI>;
|
||||
sort: SortType;
|
||||
page: number;
|
||||
}
|
||||
|
@ -44,9 +51,10 @@ export class Inbox extends Component<any, InboxState> {
|
|||
private subscription: Subscription;
|
||||
private emptyState: InboxState = {
|
||||
unreadOrAll: UnreadOrAll.Unread,
|
||||
unreadType: UnreadType.Both,
|
||||
unreadType: UnreadType.All,
|
||||
replies: [],
|
||||
mentions: [],
|
||||
messages: [],
|
||||
sort: SortType.New,
|
||||
page: 1,
|
||||
};
|
||||
|
@ -103,7 +111,10 @@ export class Inbox extends Component<any, InboxState> {
|
|||
</a>
|
||||
</small>
|
||||
</h5>
|
||||
{this.state.replies.length + this.state.mentions.length > 0 &&
|
||||
{this.state.replies.length +
|
||||
this.state.mentions.length +
|
||||
this.state.messages.length >
|
||||
0 &&
|
||||
this.state.unreadOrAll == UnreadOrAll.Unread && (
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
<li className="list-inline-item">
|
||||
|
@ -114,9 +125,10 @@ export class Inbox extends Component<any, InboxState> {
|
|||
</ul>
|
||||
)}
|
||||
{this.selects()}
|
||||
{this.state.unreadType == UnreadType.Both && this.both()}
|
||||
{this.state.unreadType == UnreadType.All && this.all()}
|
||||
{this.state.unreadType == UnreadType.Replies && this.replies()}
|
||||
{this.state.unreadType == UnreadType.Mentions && this.mentions()}
|
||||
{this.state.unreadType == UnreadType.Messages && this.messages()}
|
||||
{this.paginator()}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -150,8 +162,8 @@ export class Inbox extends Component<any, InboxState> {
|
|||
<option disabled>
|
||||
<T i18nKey="type">#</T>
|
||||
</option>
|
||||
<option value={UnreadType.Both}>
|
||||
<T i18nKey="both">#</T>
|
||||
<option value={UnreadType.All}>
|
||||
<T i18nKey="all">#</T>
|
||||
</option>
|
||||
<option value={UnreadType.Replies}>
|
||||
<T i18nKey="replies">#</T>
|
||||
|
@ -159,6 +171,9 @@ export class Inbox extends Component<any, InboxState> {
|
|||
<option value={UnreadType.Mentions}>
|
||||
<T i18nKey="mentions">#</T>
|
||||
</option>
|
||||
<option value={UnreadType.Messages}>
|
||||
<T i18nKey="messages">#</T>
|
||||
</option>
|
||||
</select>
|
||||
<SortSelect
|
||||
sort={this.state.sort}
|
||||
|
@ -169,33 +184,29 @@ 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 };
|
||||
});
|
||||
all() {
|
||||
let combined: Array<Comment | PrivateMessageI> = [];
|
||||
|
||||
combined.push(...replies);
|
||||
combined.push(...mentions);
|
||||
combined.push(...this.state.replies);
|
||||
combined.push(...this.state.mentions);
|
||||
combined.push(...this.state.messages);
|
||||
|
||||
// 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);
|
||||
}
|
||||
combined.sort((a, b) => b.published.localeCompare(a.published));
|
||||
|
||||
return (
|
||||
<div>
|
||||
{combined.map(i => (
|
||||
<CommentNodes nodes={[{ comment: i.data }]} noIndent markable />
|
||||
))}
|
||||
{combined.map(i =>
|
||||
isCommentType(i) ? (
|
||||
<CommentNodes
|
||||
nodes={[{ comment: i }]}
|
||||
noIndent
|
||||
markable
|
||||
/>
|
||||
) : (
|
||||
<PrivateMessage privateMessage={i} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -220,6 +231,16 @@ export class Inbox extends Component<any, InboxState> {
|
|||
);
|
||||
}
|
||||
|
||||
messages() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.messages.map(message => (
|
||||
<PrivateMessage privateMessage={message} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
paginator() {
|
||||
return (
|
||||
<div class="mt-2">
|
||||
|
@ -283,6 +304,13 @@ export class Inbox extends Component<any, InboxState> {
|
|||
limit: fetchLimit,
|
||||
};
|
||||
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
||||
|
||||
let privateMessagesForm: GetPrivateMessagesForm = {
|
||||
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
|
||||
page: this.state.page,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
|
||||
}
|
||||
|
||||
handleSortChange(val: SortType) {
|
||||
|
@ -314,9 +342,37 @@ export class Inbox extends Component<any, InboxState> {
|
|||
this.sendUnreadCount();
|
||||
window.scrollTo(0, 0);
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.GetPrivateMessages) {
|
||||
let res: PrivateMessagesResponse = msg;
|
||||
this.state.messages = res.messages;
|
||||
this.sendUnreadCount();
|
||||
window.scrollTo(0, 0);
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.EditPrivateMessage) {
|
||||
let res: PrivateMessageResponse = msg;
|
||||
let found: PrivateMessageI = this.state.messages.find(
|
||||
m => m.id === res.message.id
|
||||
);
|
||||
found.content = res.message.content;
|
||||
found.updated = res.message.updated;
|
||||
found.deleted = res.message.deleted;
|
||||
// If youre in the unread view, just remove it from the list
|
||||
if (this.state.unreadOrAll == UnreadOrAll.Unread && res.message.read) {
|
||||
this.state.messages = this.state.messages.filter(
|
||||
r => r.id !== res.message.id
|
||||
);
|
||||
} else {
|
||||
let found = this.state.messages.find(c => c.id == res.message.id);
|
||||
found.read = res.message.read;
|
||||
}
|
||||
this.sendUnreadCount();
|
||||
window.scrollTo(0, 0);
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.MarkAllAsRead) {
|
||||
this.state.replies = [];
|
||||
this.state.mentions = [];
|
||||
this.state.messages = [];
|
||||
this.sendUnreadCount();
|
||||
window.scrollTo(0, 0);
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.EditComment) {
|
||||
|
@ -391,7 +447,10 @@ export class Inbox extends Component<any, InboxState> {
|
|||
sendUnreadCount() {
|
||||
let count =
|
||||
this.state.replies.filter(r => !r.read).length +
|
||||
this.state.mentions.filter(r => !r.read).length;
|
||||
this.state.mentions.filter(r => !r.read).length +
|
||||
this.state.messages.filter(
|
||||
r => !r.read && r.creator_id !== UserService.Instance.user.id
|
||||
).length;
|
||||
UserService.Instance.sub.next({
|
||||
user: UserService.Instance.user,
|
||||
unreadCount: count,
|
||||
|
|
41
ui/src/components/navbar.tsx
vendored
41
ui/src/components/navbar.tsx
vendored
|
@ -9,15 +9,19 @@ import {
|
|||
GetRepliesResponse,
|
||||
GetUserMentionsForm,
|
||||
GetUserMentionsResponse,
|
||||
GetPrivateMessagesForm,
|
||||
PrivateMessagesResponse,
|
||||
SortType,
|
||||
GetSiteResponse,
|
||||
Comment,
|
||||
PrivateMessage,
|
||||
} from '../interfaces';
|
||||
import {
|
||||
msgOp,
|
||||
pictshareAvatarThumbnail,
|
||||
showAvatars,
|
||||
fetchLimit,
|
||||
isCommentType,
|
||||
} from '../utils';
|
||||
import { version } from '../version';
|
||||
import { i18n } from '../i18next';
|
||||
|
@ -28,6 +32,7 @@ interface NavbarState {
|
|||
expanded: boolean;
|
||||
replies: Array<Comment>;
|
||||
mentions: Array<Comment>;
|
||||
messages: Array<PrivateMessage>;
|
||||
fetchCount: number;
|
||||
unreadCount: number;
|
||||
siteName: string;
|
||||
|
@ -42,6 +47,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
fetchCount: 0,
|
||||
replies: [],
|
||||
mentions: [],
|
||||
messages: [],
|
||||
expanded: false,
|
||||
siteName: undefined,
|
||||
};
|
||||
|
@ -228,6 +234,20 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
this.state.mentions = unreadMentions;
|
||||
this.setState(this.state);
|
||||
this.sendUnreadCount();
|
||||
} else if (op == UserOperation.GetPrivateMessages) {
|
||||
let res: PrivateMessagesResponse = msg;
|
||||
let unreadMessages = res.messages.filter(r => !r.read);
|
||||
if (
|
||||
unreadMessages.length > 0 &&
|
||||
this.state.fetchCount > 1 &&
|
||||
JSON.stringify(this.state.messages) !== JSON.stringify(unreadMessages)
|
||||
) {
|
||||
this.notify(unreadMessages);
|
||||
}
|
||||
|
||||
this.state.messages = unreadMessages;
|
||||
this.setState(this.state);
|
||||
this.sendUnreadCount();
|
||||
} else if (op == UserOperation.GetSite) {
|
||||
let res: GetSiteResponse = msg;
|
||||
|
||||
|
@ -259,9 +279,17 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
page: 1,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
|
||||
let privateMessagesForm: GetPrivateMessagesForm = {
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
|
||||
if (this.currentLocation !== '/inbox') {
|
||||
WebSocketService.Instance.getReplies(repliesForm);
|
||||
WebSocketService.Instance.getUserMentions(userMentionsForm);
|
||||
WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
|
||||
this.state.fetchCount++;
|
||||
}
|
||||
}
|
||||
|
@ -281,7 +309,8 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
get unreadCount() {
|
||||
return (
|
||||
this.state.replies.filter(r => !r.read).length +
|
||||
this.state.mentions.filter(r => !r.read).length
|
||||
this.state.mentions.filter(r => !r.read).length +
|
||||
this.state.messages.filter(r => !r.read).length
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -299,21 +328,25 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
}
|
||||
}
|
||||
|
||||
notify(replies: Array<Comment>) {
|
||||
notify(replies: Array<Comment | PrivateMessage>) {
|
||||
let recentReply = replies[0];
|
||||
if (Notification.permission !== 'granted') Notification.requestPermission();
|
||||
else {
|
||||
var notification = new Notification(
|
||||
`${replies.length} ${i18n.t('unread_messages')}`,
|
||||
{
|
||||
icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
|
||||
icon: recentReply.creator_avatar
|
||||
? recentReply.creator_avatar
|
||||
: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
|
||||
body: `${recentReply.creator_name}: ${recentReply.content}`,
|
||||
}
|
||||
);
|
||||
|
||||
notification.onclick = () => {
|
||||
this.context.router.history.push(
|
||||
`/post/${recentReply.post_id}/comment/${recentReply.id}`
|
||||
isCommentType(recentReply)
|
||||
? `/post/${recentReply.post_id}/comment/${recentReply.id}`
|
||||
: `/inbox`
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
291
ui/src/components/private-message-form.tsx
vendored
Normal file
291
ui/src/components/private-message-form.tsx
vendored
Normal file
|
@ -0,0 +1,291 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import {
|
||||
PrivateMessageForm as PrivateMessageFormI,
|
||||
EditPrivateMessageForm,
|
||||
PrivateMessageFormParams,
|
||||
PrivateMessage,
|
||||
PrivateMessageResponse,
|
||||
UserView,
|
||||
UserOperation,
|
||||
UserDetailsResponse,
|
||||
GetUserDetailsForm,
|
||||
SortType,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import {
|
||||
msgOp,
|
||||
capitalizeFirstLetter,
|
||||
markdownHelpUrl,
|
||||
mdToHtml,
|
||||
showAvatars,
|
||||
pictshareAvatarThumbnail,
|
||||
} from '../utils';
|
||||
import autosize from 'autosize';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
interface PrivateMessageFormProps {
|
||||
privateMessage?: PrivateMessage; // If a pm is given, that means this is an edit
|
||||
params?: PrivateMessageFormParams;
|
||||
onCancel?(): any;
|
||||
onCreate?(message: PrivateMessage): any;
|
||||
onEdit?(message: PrivateMessage): any;
|
||||
}
|
||||
|
||||
interface PrivateMessageFormState {
|
||||
privateMessageForm: PrivateMessageFormI;
|
||||
recipient: UserView;
|
||||
loading: boolean;
|
||||
previewMode: boolean;
|
||||
showDisclaimer: boolean;
|
||||
}
|
||||
|
||||
export class PrivateMessageForm extends Component<
|
||||
PrivateMessageFormProps,
|
||||
PrivateMessageFormState
|
||||
> {
|
||||
private subscription: Subscription;
|
||||
private emptyState: PrivateMessageFormState = {
|
||||
privateMessageForm: {
|
||||
content: null,
|
||||
recipient_id: null,
|
||||
},
|
||||
recipient: null,
|
||||
loading: false,
|
||||
previewMode: false,
|
||||
showDisclaimer: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
if (this.props.privateMessage) {
|
||||
this.state.privateMessageForm = {
|
||||
content: this.props.privateMessage.content,
|
||||
recipient_id: this.props.privateMessage.recipient_id,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.props.params) {
|
||||
this.state.privateMessageForm.recipient_id = this.props.params.recipient_id;
|
||||
let form: GetUserDetailsForm = {
|
||||
user_id: this.state.privateMessageForm.recipient_id,
|
||||
sort: SortType[SortType.New],
|
||||
saved_only: false,
|
||||
};
|
||||
WebSocketService.Instance.getUserDetails(form);
|
||||
}
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
autosize(document.querySelectorAll('textarea'));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
|
||||
{!this.props.privateMessage && (
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">
|
||||
{capitalizeFirstLetter(i18n.t('to'))}
|
||||
</label>
|
||||
|
||||
{this.state.recipient && (
|
||||
<div class="col-sm-10 form-control-plaintext">
|
||||
<Link
|
||||
className="text-info"
|
||||
to={`/u/${this.state.recipient.name}`}
|
||||
>
|
||||
{this.state.recipient.avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictshareAvatarThumbnail(
|
||||
this.state.recipient.avatar
|
||||
)}
|
||||
class="rounded-circle mr-1"
|
||||
/>
|
||||
)}
|
||||
<span>{this.state.recipient.name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">{i18n.t('message')}</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea
|
||||
value={this.state.privateMessageForm.content}
|
||||
onInput={linkEvent(this, this.handleContentChange)}
|
||||
className={`form-control ${this.state.previewMode && 'd-none'}`}
|
||||
rows={4}
|
||||
maxLength={10000}
|
||||
/>
|
||||
{this.state.previewMode && (
|
||||
<div
|
||||
className="md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(
|
||||
this.state.privateMessageForm.content
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.state.privateMessageForm.content && (
|
||||
<button
|
||||
className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
|
||||
.previewMode && 'active'}`}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
>
|
||||
{i18n.t('preview')}
|
||||
</button>
|
||||
)}
|
||||
<ul class="float-right list-inline mb-1 text-muted small font-weight-bold">
|
||||
<li class="list-inline-item">
|
||||
<span
|
||||
onClick={linkEvent(this, this.handleShowDisclaimer)}
|
||||
class="pointer"
|
||||
>
|
||||
{i18n.t('disclaimer')}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a href={markdownHelpUrl} target="_blank" class="text-muted">
|
||||
{i18n.t('formatting_help')}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.showDisclaimer && (
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<T i18nKey="private_message_disclaimer">
|
||||
#
|
||||
<a
|
||||
class="alert-link"
|
||||
target="_blank"
|
||||
href="https://about.riot.im/"
|
||||
>
|
||||
#
|
||||
</a>
|
||||
</T>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-secondary mr-2">
|
||||
{this.state.loading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
) : this.props.privateMessage ? (
|
||||
capitalizeFirstLetter(i18n.t('save'))
|
||||
) : (
|
||||
capitalizeFirstLetter(i18n.t('send_message'))
|
||||
)}
|
||||
</button>
|
||||
{this.props.privateMessage && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
onClick={linkEvent(this, this.handleCancel)}
|
||||
>
|
||||
{i18n.t('cancel')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
|
||||
event.preventDefault();
|
||||
if (i.props.privateMessage) {
|
||||
let editForm: EditPrivateMessageForm = {
|
||||
edit_id: i.props.privateMessage.id,
|
||||
content: i.state.privateMessageForm.content,
|
||||
};
|
||||
WebSocketService.Instance.editPrivateMessage(editForm);
|
||||
} else {
|
||||
WebSocketService.Instance.createPrivateMessage(
|
||||
i.state.privateMessageForm
|
||||
);
|
||||
}
|
||||
i.state.loading = true;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleRecipientChange(i: PrivateMessageForm, event: any) {
|
||||
i.state.recipient = event.target.value;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleContentChange(i: PrivateMessageForm, event: any) {
|
||||
i.state.privateMessageForm.content = event.target.value;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleCancel(i: PrivateMessageForm) {
|
||||
i.props.onCancel();
|
||||
}
|
||||
|
||||
handlePreviewToggle(i: PrivateMessageForm, event: any) {
|
||||
event.preventDefault();
|
||||
i.state.previewMode = !i.state.previewMode;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleShowDisclaimer(i: PrivateMessageForm) {
|
||||
i.state.showDisclaimer = !i.state.showDisclaimer;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
parseMessage(msg: any) {
|
||||
let op: UserOperation = msgOp(msg);
|
||||
if (msg.error) {
|
||||
alert(i18n.t(msg.error));
|
||||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
return;
|
||||
} else if (op == UserOperation.EditPrivateMessage) {
|
||||
this.state.loading = false;
|
||||
let res: PrivateMessageResponse = msg;
|
||||
this.props.onEdit(res.message);
|
||||
} else if (op == UserOperation.GetUserDetails) {
|
||||
let res: UserDetailsResponse = msg;
|
||||
this.state.recipient = res.user;
|
||||
this.state.privateMessageForm.recipient_id = res.user.id;
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.CreatePrivateMessage) {
|
||||
this.state.loading = false;
|
||||
let res: PrivateMessageResponse = msg;
|
||||
this.props.onCreate(res.message);
|
||||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
}
|
249
ui/src/components/private-message.tsx
vendored
Normal file
249
ui/src/components/private-message.tsx
vendored
Normal file
|
@ -0,0 +1,249 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import {
|
||||
PrivateMessage as PrivateMessageI,
|
||||
EditPrivateMessageForm,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import { mdToHtml, pictshareAvatarThumbnail, showAvatars } from '../utils';
|
||||
import { MomentTime } from './moment-time';
|
||||
import { PrivateMessageForm } from './private-message-form';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
interface PrivateMessageState {
|
||||
showReply: boolean;
|
||||
showEdit: boolean;
|
||||
collapsed: boolean;
|
||||
viewSource: boolean;
|
||||
}
|
||||
|
||||
interface PrivateMessageProps {
|
||||
privateMessage: PrivateMessageI;
|
||||
}
|
||||
|
||||
export class PrivateMessage extends Component<
|
||||
PrivateMessageProps,
|
||||
PrivateMessageState
|
||||
> {
|
||||
private emptyState: PrivateMessageState = {
|
||||
showReply: false,
|
||||
showEdit: false,
|
||||
collapsed: false,
|
||||
viewSource: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
this.handleReplyCancel = this.handleReplyCancel.bind(this);
|
||||
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
|
||||
this
|
||||
);
|
||||
this.handlePrivateMessageEdit = this.handlePrivateMessageEdit.bind(this);
|
||||
}
|
||||
|
||||
get mine(): boolean {
|
||||
return UserService.Instance.user.id == this.props.privateMessage.creator_id;
|
||||
}
|
||||
|
||||
render() {
|
||||
let message = this.props.privateMessage;
|
||||
return (
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<ul class="list-inline mb-0 text-muted small">
|
||||
<li className="list-inline-item">
|
||||
{this.mine ? i18n.t('to') : i18n.t('from')}
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<Link
|
||||
className="text-info"
|
||||
to={
|
||||
this.mine
|
||||
? `/u/${message.recipient_name}`
|
||||
: `/u/${message.creator_name}`
|
||||
}
|
||||
>
|
||||
{(this.mine
|
||||
? message.recipient_avatar
|
||||
: message.creator_avatar) &&
|
||||
showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictshareAvatarThumbnail(
|
||||
this.mine
|
||||
? message.recipient_avatar
|
||||
: message.creator_avatar
|
||||
)}
|
||||
class="rounded-circle mr-1"
|
||||
/>
|
||||
)}
|
||||
<span>
|
||||
{this.mine ? message.recipient_name : message.creator_name}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<span>
|
||||
<MomentTime data={message} />
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<div
|
||||
className="pointer text-monospace"
|
||||
onClick={linkEvent(this, this.handleMessageCollapse)}
|
||||
>
|
||||
{this.state.collapsed ? '[+]' : '[-]'}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{this.state.showEdit && (
|
||||
<PrivateMessageForm
|
||||
privateMessage={message}
|
||||
onEdit={this.handlePrivateMessageEdit}
|
||||
onCancel={this.handleReplyCancel}
|
||||
/>
|
||||
)}
|
||||
{!this.state.showEdit && !this.state.collapsed && (
|
||||
<div>
|
||||
{this.state.viewSource ? (
|
||||
<pre>{this.messageUnlessRemoved}</pre>
|
||||
) : (
|
||||
<div
|
||||
className="md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(this.messageUnlessRemoved)}
|
||||
/>
|
||||
)}
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
{!this.mine && (
|
||||
<>
|
||||
<li className="list-inline-item">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleMarkRead)}
|
||||
>
|
||||
{message.read
|
||||
? i18n.t('mark_as_unread')
|
||||
: i18n.t('mark_as_read')}
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleReplyClick)}
|
||||
>
|
||||
<T i18nKey="reply">#</T>
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{this.mine && (
|
||||
<>
|
||||
<li className="list-inline-item">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleEditClick)}
|
||||
>
|
||||
<T i18nKey="edit">#</T>
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||
>
|
||||
{!message.deleted
|
||||
? i18n.t('delete')
|
||||
: i18n.t('restore')}
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
<li className="list-inline-item">•</li>
|
||||
<li className="list-inline-item">
|
||||
<span
|
||||
className="pointer"
|
||||
onClick={linkEvent(this, this.handleViewSource)}
|
||||
>
|
||||
<T i18nKey="view_source">#</T>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{this.state.showReply && (
|
||||
<PrivateMessageForm
|
||||
params={{
|
||||
recipient_id: this.props.privateMessage.creator_id,
|
||||
}}
|
||||
onCreate={this.handlePrivateMessageCreate}
|
||||
/>
|
||||
)}
|
||||
{/* A collapsed clearfix */}
|
||||
{this.state.collapsed && <div class="row col-12"></div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
get messageUnlessRemoved(): string {
|
||||
let message = this.props.privateMessage;
|
||||
return message.deleted ? `*${i18n.t('deleted')}*` : message.content;
|
||||
}
|
||||
|
||||
handleReplyClick(i: PrivateMessage) {
|
||||
i.state.showReply = true;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleEditClick(i: PrivateMessage) {
|
||||
i.state.showEdit = true;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleDeleteClick(i: PrivateMessage) {
|
||||
let form: EditPrivateMessageForm = {
|
||||
edit_id: i.props.privateMessage.id,
|
||||
deleted: !i.props.privateMessage.deleted,
|
||||
};
|
||||
WebSocketService.Instance.editPrivateMessage(form);
|
||||
}
|
||||
|
||||
handleReplyCancel() {
|
||||
this.state.showReply = false;
|
||||
this.state.showEdit = false;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleMarkRead(i: PrivateMessage) {
|
||||
let form: EditPrivateMessageForm = {
|
||||
edit_id: i.props.privateMessage.id,
|
||||
read: !i.props.privateMessage.read,
|
||||
};
|
||||
WebSocketService.Instance.editPrivateMessage(form);
|
||||
}
|
||||
|
||||
handleMessageCollapse(i: PrivateMessage) {
|
||||
i.state.collapsed = !i.state.collapsed;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleViewSource(i: PrivateMessage) {
|
||||
i.state.viewSource = !i.state.viewSource;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handlePrivateMessageEdit() {
|
||||
this.state.showEdit = false;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handlePrivateMessageCreate() {
|
||||
this.state.showReply = false;
|
||||
this.setState(this.state);
|
||||
alert(i18n.t('message_sent'));
|
||||
}
|
||||
}
|
51
ui/src/components/user.tsx
vendored
51
ui/src/components/user.tsx
vendored
|
@ -405,13 +405,30 @@ export class User extends Component<any, UserState> {
|
|||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{this.isCurrentUser && (
|
||||
{this.isCurrentUser ? (
|
||||
<button
|
||||
class="btn btn-block btn-secondary mt-3"
|
||||
onClick={linkEvent(this, this.handleLogoutClick)}
|
||||
>
|
||||
<T i18nKey="logout">#</T>
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<a
|
||||
className={`btn btn-block btn-secondary mt-3 ${!this.state
|
||||
.user.matrix_user_id && 'disabled'}`}
|
||||
target="_blank"
|
||||
href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
|
||||
>
|
||||
{i18n.t('send_secure_message')}
|
||||
</a>
|
||||
<Link
|
||||
class="btn btn-block btn-secondary mt-3"
|
||||
to={`/create_private_message?recipient_id=${this.state.user.id}`}
|
||||
>
|
||||
{i18n.t('send_message')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -539,6 +556,26 @@ export class User extends Component<any, UserState> {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-5 col-form-label">
|
||||
<a href="https://about.riot.im/" target="_blank">
|
||||
{i18n.t('matrix_user_id')}
|
||||
</a>
|
||||
</label>
|
||||
<div class="col-lg-7">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="@user:example.com"
|
||||
value={this.state.userSettingsForm.matrix_user_id}
|
||||
onInput={linkEvent(
|
||||
this,
|
||||
this.handleUserSettingsMatrixUserIdChange
|
||||
)}
|
||||
minLength={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-5 col-form-label">
|
||||
<T i18nKey="new_password">#</T>
|
||||
|
@ -875,6 +912,17 @@ export class User extends Component<any, UserState> {
|
|||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleUserSettingsMatrixUserIdChange(i: User, event: any) {
|
||||
i.state.userSettingsForm.matrix_user_id = event.target.value;
|
||||
if (
|
||||
i.state.userSettingsForm.matrix_user_id == '' &&
|
||||
!i.state.user.matrix_user_id
|
||||
) {
|
||||
i.state.userSettingsForm.matrix_user_id = undefined;
|
||||
}
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleUserSettingsNewPasswordChange(i: User, event: any) {
|
||||
i.state.userSettingsForm.new_password = event.target.value;
|
||||
if (i.state.userSettingsForm.new_password == '') {
|
||||
|
@ -1001,6 +1049,7 @@ export class User extends Component<any, UserState> {
|
|||
this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
|
||||
this.state.userSettingsForm.show_avatars =
|
||||
UserService.Instance.user.show_avatars;
|
||||
this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
|
||||
}
|
||||
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
|
||||
window.scrollTo(0, 0);
|
||||
|
|
5
ui/src/index.tsx
vendored
5
ui/src/index.tsx
vendored
|
@ -7,6 +7,7 @@ import { Footer } from './components/footer';
|
|||
import { Login } from './components/login';
|
||||
import { CreatePost } from './components/create-post';
|
||||
import { CreateCommunity } from './components/create-community';
|
||||
import { CreatePrivateMessage } from './components/create-private-message';
|
||||
import { PasswordChange } from './components/password_change';
|
||||
import { Post } from './components/post';
|
||||
import { Community } from './components/community';
|
||||
|
@ -46,6 +47,10 @@ class Index extends Component<any, any> {
|
|||
<Route path={`/login`} component={Login} />
|
||||
<Route path={`/create_post`} component={CreatePost} />
|
||||
<Route path={`/create_community`} component={CreateCommunity} />
|
||||
<Route
|
||||
path={`/create_private_message`}
|
||||
component={CreatePrivateMessage}
|
||||
/>
|
||||
<Route path={`/communities/page/:page`} component={Communities} />
|
||||
<Route path={`/communities`} component={Communities} />
|
||||
<Route path={`/post/:id/comment/:comment_id`} component={Post} />
|
||||
|
|
55
ui/src/interfaces.ts
vendored
55
ui/src/interfaces.ts
vendored
|
@ -38,6 +38,9 @@ export enum UserOperation {
|
|||
DeleteAccount,
|
||||
PasswordReset,
|
||||
PasswordChange,
|
||||
CreatePrivateMessage,
|
||||
EditPrivateMessage,
|
||||
GetPrivateMessages,
|
||||
}
|
||||
|
||||
export enum CommentSortType {
|
||||
|
@ -89,6 +92,7 @@ export interface UserView {
|
|||
name: string;
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
matrix_user_id?: string;
|
||||
fedi_name: string;
|
||||
published: string;
|
||||
number_of_posts: number;
|
||||
|
@ -218,6 +222,21 @@ export interface Site {
|
|||
enable_nsfw: boolean;
|
||||
}
|
||||
|
||||
export interface PrivateMessage {
|
||||
id: number;
|
||||
creator_id: number;
|
||||
recipient_id: number;
|
||||
content: string;
|
||||
deleted: boolean;
|
||||
read: boolean;
|
||||
published: string;
|
||||
updated?: string;
|
||||
creator_name: string;
|
||||
creator_avatar?: string;
|
||||
recipient_name: string;
|
||||
recipient_avatar?: string;
|
||||
}
|
||||
|
||||
export enum BanType {
|
||||
Community,
|
||||
Site,
|
||||
|
@ -490,6 +509,7 @@ export interface UserSettingsForm {
|
|||
lang: string;
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
matrix_user_id?: string;
|
||||
new_password?: string;
|
||||
new_password_verify?: string;
|
||||
old_password?: string;
|
||||
|
@ -729,3 +749,38 @@ export interface PasswordChangeForm {
|
|||
password: string;
|
||||
password_verify: string;
|
||||
}
|
||||
|
||||
export interface PrivateMessageForm {
|
||||
content: string;
|
||||
recipient_id: number;
|
||||
auth?: string;
|
||||
}
|
||||
|
||||
export interface PrivateMessageFormParams {
|
||||
recipient_id: number;
|
||||
}
|
||||
|
||||
export interface EditPrivateMessageForm {
|
||||
edit_id: number;
|
||||
content?: string;
|
||||
deleted?: boolean;
|
||||
read?: boolean;
|
||||
auth?: string;
|
||||
}
|
||||
|
||||
export interface GetPrivateMessagesForm {
|
||||
unread_only: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
auth?: string;
|
||||
}
|
||||
|
||||
export interface PrivateMessagesResponse {
|
||||
op: string;
|
||||
messages: Array<PrivateMessage>;
|
||||
}
|
||||
|
||||
export interface PrivateMessageResponse {
|
||||
op: string;
|
||||
message: PrivateMessage;
|
||||
}
|
||||
|
|
26
ui/src/services/WebSocketService.ts
vendored
26
ui/src/services/WebSocketService.ts
vendored
|
@ -32,10 +32,13 @@ import {
|
|||
DeleteAccountForm,
|
||||
PasswordResetForm,
|
||||
PasswordChangeForm,
|
||||
PrivateMessageForm,
|
||||
EditPrivateMessageForm,
|
||||
GetPrivateMessagesForm,
|
||||
} from '../interfaces';
|
||||
import { webSocket } from 'rxjs/webSocket';
|
||||
import { Subject } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import { retryWhen, delay } from 'rxjs/operators';
|
||||
import { UserService } from './';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
|
@ -285,6 +288,27 @@ export class WebSocketService {
|
|||
this.subject.next(this.wsSendWrapper(UserOperation.PasswordChange, form));
|
||||
}
|
||||
|
||||
public createPrivateMessage(form: PrivateMessageForm) {
|
||||
this.setAuth(form);
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.CreatePrivateMessage, form)
|
||||
);
|
||||
}
|
||||
|
||||
public editPrivateMessage(form: EditPrivateMessageForm) {
|
||||
this.setAuth(form);
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.EditPrivateMessage, form)
|
||||
);
|
||||
}
|
||||
|
||||
public getPrivateMessages(form: GetPrivateMessagesForm) {
|
||||
this.setAuth(form);
|
||||
this.subject.next(
|
||||
this.wsSendWrapper(UserOperation.GetPrivateMessages, form)
|
||||
);
|
||||
}
|
||||
|
||||
private wsSendWrapper(op: UserOperation, data: any) {
|
||||
let send = { op: UserOperation[op], data: data };
|
||||
console.log(send);
|
||||
|
|
13
ui/src/translations/en.ts
vendored
13
ui/src/translations/en.ts
vendored
|
@ -23,6 +23,10 @@ export const en = {
|
|||
list_of_communities: 'List of communities',
|
||||
number_of_communities: '{{count}} Communities',
|
||||
community_reqs: 'lowercase, underscores, and no spaces.',
|
||||
create_private_message: 'Create Private Message',
|
||||
send_secure_message: 'Send Secure Message',
|
||||
send_message: 'Send Message',
|
||||
message: 'Message',
|
||||
edit: 'edit',
|
||||
reply: 'reply',
|
||||
cancel: 'Cancel',
|
||||
|
@ -109,6 +113,7 @@ export const en = {
|
|||
replies: 'Replies',
|
||||
mentions: 'Mentions',
|
||||
reply_sent: 'Reply sent',
|
||||
message_sent: 'Message sent',
|
||||
search: 'Search',
|
||||
overview: 'Overview',
|
||||
view: 'View',
|
||||
|
@ -119,6 +124,7 @@ export const en = {
|
|||
notifications_error:
|
||||
'Desktop notifications not available in your browser. Try Firefox or Chrome.',
|
||||
unread_messages: 'Unread Messages',
|
||||
messages: 'Messages',
|
||||
password: 'Password',
|
||||
verify_password: 'Verify Password',
|
||||
old_password: 'Old Password',
|
||||
|
@ -128,6 +134,9 @@ export const en = {
|
|||
new_password: 'New Password',
|
||||
no_email_setup: "This server hasn't correctly set up email.",
|
||||
email: 'Email',
|
||||
matrix_user_id: 'Matrix User',
|
||||
private_message_disclaimer:
|
||||
'Warning: Private messages in Lemmy are not secure. Please create an account on <1>Riot.im</1> for secure messaging.',
|
||||
send_notifications_to_email: 'Send notifications to Email',
|
||||
optional: 'Optional',
|
||||
expires: 'Expires',
|
||||
|
@ -172,6 +181,7 @@ export const en = {
|
|||
joined: 'Joined',
|
||||
by: 'by',
|
||||
to: 'to',
|
||||
from: 'from',
|
||||
transfer_community: 'transfer community',
|
||||
transfer_site: 'transfer site',
|
||||
are_you_sure: 'are you sure?',
|
||||
|
@ -215,5 +225,8 @@ export const en = {
|
|||
email_already_exists: 'Email already exists.',
|
||||
couldnt_update_user: "Couldn't update user.",
|
||||
system_err_login: 'System error. Try logging out and back in.',
|
||||
couldnt_create_private_message: "Couldn't create private message.",
|
||||
no_private_message_edit_allowed: 'Not allowed to edit private message.',
|
||||
couldnt_update_private_message: "Couldn't update private message.",
|
||||
},
|
||||
};
|
||||
|
|
5
ui/src/utils.ts
vendored
5
ui/src/utils.ts
vendored
|
@ -11,6 +11,7 @@ import 'moment/locale/it';
|
|||
import {
|
||||
UserOperation,
|
||||
Comment,
|
||||
PrivateMessage,
|
||||
User,
|
||||
SortType,
|
||||
ListingType,
|
||||
|
@ -361,3 +362,7 @@ export function imageThumbnailer(url: string): string {
|
|||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
export function isCommentType(item: Comment | PrivateMessage): item is Comment {
|
||||
return (item as Comment).community_id !== undefined;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue