Private message delete and read extracted.

pull/997/head
Dessalines 2020-07-20 00:29:44 -04:00
parent dca38d10eb
commit 0a28ffb9c4
12 changed files with 457 additions and 132 deletions

View File

@ -489,6 +489,137 @@ Only the first user will be able to be the admin.
`PUT /user/mention`
#### Get Private Messages
##### Request
```rust
{
op: "GetPrivateMessages",
data: {
unread_only: bool,
page: Option<i64>,
limit: Option<i64>,
auth: String,
}
}
```
##### Response
```rust
{
op: "GetPrivateMessages",
data: {
messages: Vec<PrivateMessageView>,
}
}
```
##### HTTP
`GET /private_message/list`
#### Create Private Message
##### Request
```rust
{
op: "CreatePrivateMessage",
data: {
content: String,
recipient_id: i32,
auth: String,
}
}
```
##### Response
```rust
{
op: "CreatePrivateMessage",
data: {
message: PrivateMessageView,
}
}
```
##### HTTP
`POST /private_message`
#### Edit Private Message
##### Request
```rust
{
op: "EditPrivateMessage",
data: {
edit_id: i32,
content: String,
auth: String,
}
}
```
##### Response
```rust
{
op: "EditPrivateMessage",
data: {
message: PrivateMessageView,
}
}
```
##### HTTP
`PUT /private_message`
#### Delete Private Message
##### Request
```rust
{
op: "DeletePrivateMessage",
data: {
edit_id: i32,
deleted: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "DeletePrivateMessage",
data: {
message: PrivateMessageView,
}
}
```
##### HTTP
`POST /private_message/delete`
#### Mark Private Message as Read
##### Request
```rust
{
op: "MarkPrivateMessageAsRead",
data: {
edit_id: i32,
read: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "MarkPrivateMessageAsRead",
data: {
message: PrivateMessageView,
}
}
```
##### HTTP
`POST /private_message/mark_as_read`
#### Mark All As Read
Marks all user replies and mentions as read.

View File

@ -80,6 +80,50 @@ impl PrivateMessage {
.filter(ap_id.eq(object_id))
.first::<Self>(conn)
}
pub fn update_content(
conn: &PgConnection,
private_message_id: i32,
new_content: &str,
) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
diesel::update(private_message.find(private_message_id))
.set(content.eq(new_content))
.get_result::<Self>(conn)
}
pub fn update_deleted(
conn: &PgConnection,
private_message_id: i32,
new_deleted: bool,
) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
diesel::update(private_message.find(private_message_id))
.set(deleted.eq(new_deleted))
.get_result::<Self>(conn)
}
pub fn update_read(
conn: &PgConnection,
private_message_id: i32,
new_read: bool,
) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
diesel::update(private_message.find(private_message_id))
.set(read.eq(new_read))
.get_result::<Self>(conn)
}
pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result<Vec<Self>, Error> {
use crate::schema::private_message::dsl::*;
diesel::update(
private_message
.filter(recipient_id.eq(for_recipient_id))
.filter(read.eq(false)),
)
.set(read.eq(true))
.get_results::<Self>(conn)
}
}
#[cfg(test)]
@ -180,6 +224,10 @@ mod tests {
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 deleted_private_message =
PrivateMessage::update_deleted(&conn, inserted_private_message.id, true).unwrap();
let marked_read_private_message =
PrivateMessage::update_read(&conn, inserted_private_message.id, true).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();
@ -187,6 +235,8 @@ mod tests {
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!(deleted_private_message.deleted);
assert!(marked_read_private_message.read);
assert_eq!(1, num_deleted);
}
}

View File

@ -110,7 +110,7 @@ pub struct GetUserDetailsResponse {
moderates: Vec<CommunityModeratorView>,
comments: Vec<CommentView>,
posts: Vec<PostView>,
admins: Vec<UserView>,
admins: Vec<UserView>, // TODO why is this necessary, just use GetSite
}
#[derive(Serialize, Deserialize)]
@ -216,9 +216,21 @@ pub struct CreatePrivateMessage {
#[derive(Serialize, Deserialize)]
pub struct EditPrivateMessage {
edit_id: i32,
content: Option<String>,
deleted: Option<bool>,
read: Option<bool>,
content: String,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct DeletePrivateMessage {
edit_id: i32,
deleted: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct MarkPrivateMessageAsRead {
edit_id: i32,
read: bool,
auth: String,
}
@ -974,36 +986,10 @@ impl Perform for Oper<MarkAllAsRead> {
}
}
// messages
let messages = blocking(pool, move |conn| {
PrivateMessageQueryBuilder::create(conn, user_id)
.page(1)
.limit(999)
.unread_only(true)
.list()
})
.await??;
// TODO: this should probably be a bulk operation
for message in &messages {
let private_message_form = PrivateMessageForm {
content: message.to_owned().content,
creator_id: message.to_owned().creator_id,
recipient_id: message.to_owned().recipient_id,
deleted: None,
read: Some(true),
updated: None,
ap_id: message.to_owned().ap_id,
local: message.local,
published: None,
};
let message_id = message.id;
let update_pm =
move |conn: &'_ _| PrivateMessage::update(conn, message_id, &private_message_form);
if blocking(pool, update_pm).await?.is_err() {
return Err(APIError::err("couldnt_update_private_message").into());
}
// Mark all private_messages as read
let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, user_id);
if blocking(pool, update_pm).await?.is_err() {
return Err(APIError::err("couldnt_update_private_message").into());
}
Ok(GetRepliesResponse { replies: vec![] })
@ -1293,59 +1279,25 @@ impl Perform for Oper<EditPrivateMessage> {
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_private_message =
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("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))
{
// Checking permissions
let edit_id = data.edit_id;
let orig_private_message =
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
if user_id != orig_private_message.creator_id {
return Err(APIError::err("no_private_message_edit_allowed").into());
}
let content_slurs_removed = match &data.content {
Some(content) => remove_slurs(content),
None => orig_private_message.content.clone(),
};
let private_message_form = {
if data.read.is_some() {
PrivateMessageForm {
content: orig_private_message.content.to_owned(),
creator_id: orig_private_message.creator_id,
recipient_id: orig_private_message.recipient_id,
read: data.read.to_owned(),
updated: orig_private_message.updated,
deleted: Some(orig_private_message.deleted),
ap_id: orig_private_message.ap_id,
local: orig_private_message.local,
published: None,
}
} else {
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: Some(orig_private_message.read),
updated: Some(naive_now()),
ap_id: orig_private_message.ap_id,
local: orig_private_message.local,
published: None,
}
}
};
// Doing the update
let content_slurs_removed = remove_slurs(&data.content);
let edit_id = data.edit_id;
let updated_private_message = match blocking(pool, move |conn| {
PrivateMessage::update(conn, edit_id, &private_message_form)
PrivateMessage::update_content(conn, edit_id, &content_slurs_removed)
})
.await?
{
@ -1353,30 +1305,14 @@ impl Perform for Oper<EditPrivateMessage> {
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
};
if data.read.is_none() {
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_private_message
.send_delete(&user, &self.client, pool)
.await?;
} else {
updated_private_message
.send_undo_delete(&user, &self.client, pool)
.await?;
}
} else {
updated_private_message
.send_update(&user, &self.client, pool)
.await?;
}
} else {
updated_private_message
.send_update(&user, &self.client, pool)
.await?;
}
// Send the apub update
updated_private_message
.send_update(&user, &self.client, pool)
.await?;
let edit_id = data.edit_id;
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
let recipient_id = message.recipient_id;
let res = PrivateMessageResponse { message };
@ -1384,7 +1320,146 @@ impl Perform for Oper<EditPrivateMessage> {
ws.chatserver.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res.clone(),
recipient_id: orig_private_message.recipient_id,
recipient_id,
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<DeletePrivateMessage> {
type Response = PrivateMessageResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PrivateMessageResponse, LemmyError> {
let data: &DeletePrivateMessage = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Checking permissions
let edit_id = data.edit_id;
let orig_private_message =
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
if user_id != orig_private_message.creator_id {
return Err(APIError::err("no_private_message_edit_allowed").into());
}
// Doing the update
let edit_id = data.edit_id;
let deleted = data.deleted;
let updated_private_message = match blocking(pool, move |conn| {
PrivateMessage::update_deleted(conn, edit_id, deleted)
})
.await?
{
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
};
// Send the apub update
if data.deleted {
updated_private_message
.send_delete(&user, &self.client, pool)
.await?;
} else {
updated_private_message
.send_undo_delete(&user, &self.client, pool)
.await?;
}
let edit_id = data.edit_id;
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
let recipient_id = message.recipient_id;
let res = PrivateMessageResponse { message };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendUserRoomMessage {
op: UserOperation::DeletePrivateMessage,
response: res.clone(),
recipient_id,
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<MarkPrivateMessageAsRead> {
type Response = PrivateMessageResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PrivateMessageResponse, LemmyError> {
let data: &MarkPrivateMessageAsRead = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Checking permissions
let edit_id = data.edit_id;
let orig_private_message =
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
if user_id != orig_private_message.recipient_id {
return Err(APIError::err("couldnt_update_private_message").into());
}
// Doing the update
let edit_id = data.edit_id;
let read = data.read;
match blocking(pool, move |conn| {
PrivateMessage::update_read(conn, edit_id, read)
})
.await?
{
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
};
// No need to send an apub update
let edit_id = data.edit_id;
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
let recipient_id = message.recipient_id;
let res = PrivateMessageResponse { message };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendUserRoomMessage {
op: UserOperation::MarkPrivateMessageAsRead,
response: res.clone(),
recipient_id,
my_id: ws.id,
});
}

View File

@ -90,7 +90,15 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.wrap(rate_limit.message())
.route("/list", web::get().to(route_get::<GetPrivateMessages>))
.route("", web::post().to(route_post::<CreatePrivateMessage>))
.route("", web::put().to(route_post::<EditPrivateMessage>)),
.route("", web::put().to(route_post::<EditPrivateMessage>))
.route(
"/delete",
web::post().to(route_post::<DeletePrivateMessage>),
)
.route(
"/mark_as_read",
web::post().to(route_post::<MarkPrivateMessageAsRead>),
),
)
// User
.service(

View File

@ -59,6 +59,8 @@ pub enum UserOperation {
PasswordChange,
CreatePrivateMessage,
EditPrivateMessage,
DeletePrivateMessage,
MarkPrivateMessageAsRead,
GetPrivateMessages,
UserJoin,
GetComments,

View File

@ -448,13 +448,21 @@ impl ChatServer {
UserOperation::DeleteAccount => do_user_operation::<DeleteAccount>(args).await,
UserOperation::PasswordReset => do_user_operation::<PasswordReset>(args).await,
UserOperation::PasswordChange => do_user_operation::<PasswordChange>(args).await,
UserOperation::UserJoin => do_user_operation::<UserJoin>(args).await,
UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
// Private Message ops
UserOperation::CreatePrivateMessage => {
do_user_operation::<CreatePrivateMessage>(args).await
}
UserOperation::EditPrivateMessage => do_user_operation::<EditPrivateMessage>(args).await,
UserOperation::DeletePrivateMessage => {
do_user_operation::<DeletePrivateMessage>(args).await
}
UserOperation::MarkPrivateMessageAsRead => {
do_user_operation::<MarkPrivateMessageAsRead>(args).await
}
UserOperation::GetPrivateMessages => do_user_operation::<GetPrivateMessages>(args).await,
UserOperation::UserJoin => do_user_operation::<UserJoin>(args).await,
UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
// Site ops
UserOperation::GetModlog => do_user_operation::<GetModlog>(args).await,

View File

@ -9,17 +9,16 @@ import {
FollowCommunityForm,
CommunityResponse,
GetFollowedCommunitiesResponse,
GetPostForm,
GetPostResponse,
CommentForm,
CommentResponse,
CommunityForm,
GetCommunityForm,
GetCommunityResponse,
CommentLikeForm,
CreatePostLikeForm,
PrivateMessageForm,
EditPrivateMessageForm,
DeletePrivateMessageForm,
PrivateMessageResponse,
PrivateMessagesResponse,
GetUserMentionsResponse,
@ -1149,16 +1148,16 @@ describe('main', () => {
);
// lemmy alpha deletes the private message
let deletePrivateMessageForm: EditPrivateMessageForm = {
let deletePrivateMessageForm: DeletePrivateMessageForm = {
deleted: true,
edit_id: createRes.message.id,
auth: lemmyAlphaAuth,
};
let deleteRes: PrivateMessageResponse = await fetch(
`${lemmyAlphaApiUrl}/private_message`,
`${lemmyAlphaApiUrl}/private_message/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -1182,16 +1181,16 @@ describe('main', () => {
expect(getPrivateMessagesDeletedRes.messages.length).toBe(0);
// lemmy alpha undeletes the private message
let undeletePrivateMessageForm: EditPrivateMessageForm = {
let undeletePrivateMessageForm: DeletePrivateMessageForm = {
deleted: false,
edit_id: createRes.message.id,
auth: lemmyAlphaAuth,
};
let undeleteRes: PrivateMessageResponse = await fetch(
`${lemmyAlphaApiUrl}/private_message`,
`${lemmyAlphaApiUrl}/private_message/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},

View File

@ -446,22 +446,42 @@ export class Inbox extends Component<any, InboxState> {
let found: PrivateMessageI = this.state.messages.find(
m => m.id === data.message.id
);
found.content = data.message.content;
found.updated = data.message.updated;
found.deleted = data.message.deleted;
// If youre in the unread view, just remove it from the list
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
this.state.messages = this.state.messages.filter(
r => r.id !== data.message.id
);
} else {
let found = this.state.messages.find(c => c.id == data.message.id);
found.read = data.message.read;
if (found) {
found.content = data.message.content;
found.updated = data.message.updated;
}
this.setState(this.state);
} else if (res.op == UserOperation.DeletePrivateMessage) {
let data = res.data as PrivateMessageResponse;
let found: PrivateMessageI = this.state.messages.find(
m => m.id === data.message.id
);
if (found) {
found.deleted = data.message.deleted;
found.updated = data.message.updated;
}
this.setState(this.state);
} else if (res.op == UserOperation.MarkPrivateMessageAsRead) {
let data = res.data as PrivateMessageResponse;
let found: PrivateMessageI = this.state.messages.find(
m => m.id === data.message.id
);
if (found) {
found.updated = data.message.updated;
// If youre in the unread view, just remove it from the list
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
this.state.messages = this.state.messages.filter(
r => r.id !== data.message.id
);
} else {
let found = this.state.messages.find(c => c.id == data.message.id);
found.read = data.message.read;
}
}
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
setupTippy();
} else if (res.op == UserOperation.MarkAllAsRead) {
// Moved to be instant
} else if (res.op == UserOperation.EditComment) {

View File

@ -263,7 +263,11 @@ export class PrivateMessageForm extends Component<
this.state.loading = false;
this.setState(this.state);
return;
} else if (res.op == UserOperation.EditPrivateMessage) {
} else if (
res.op == UserOperation.EditPrivateMessage ||
res.op == UserOperation.DeletePrivateMessage ||
res.op == UserOperation.MarkPrivateMessageAsRead
) {
let data = res.data as PrivateMessageResponse;
this.state.loading = false;
this.props.onEdit(data.message);

View File

@ -2,7 +2,8 @@ import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import {
PrivateMessage as PrivateMessageI,
EditPrivateMessageForm,
DeletePrivateMessageForm,
MarkPrivateMessageAsReadForm,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
@ -243,11 +244,11 @@ export class PrivateMessage extends Component<
}
handleDeleteClick(i: PrivateMessage) {
let form: EditPrivateMessageForm = {
let form: DeletePrivateMessageForm = {
edit_id: i.props.privateMessage.id,
deleted: !i.props.privateMessage.deleted,
};
WebSocketService.Instance.editPrivateMessage(form);
WebSocketService.Instance.deletePrivateMessage(form);
}
handleReplyCancel() {
@ -257,11 +258,11 @@ export class PrivateMessage extends Component<
}
handleMarkRead(i: PrivateMessage) {
let form: EditPrivateMessageForm = {
let form: MarkPrivateMessageAsReadForm = {
edit_id: i.props.privateMessage.id,
read: !i.props.privateMessage.read,
};
WebSocketService.Instance.editPrivateMessage(form);
WebSocketService.Instance.markPrivateMessageAsRead(form);
}
handleMessageCollapse(i: PrivateMessage) {

21
ui/src/interfaces.ts vendored
View File

@ -40,6 +40,8 @@ export enum UserOperation {
PasswordChange,
CreatePrivateMessage,
EditPrivateMessage,
DeletePrivateMessage,
MarkPrivateMessageAsRead,
GetPrivateMessages,
UserJoin,
GetComments,
@ -834,9 +836,19 @@ export interface PrivateMessageFormParams {
export interface EditPrivateMessageForm {
edit_id: number;
content?: string;
deleted?: boolean;
read?: boolean;
content: string;
auth?: string;
}
export interface DeletePrivateMessageForm {
edit_id: number;
deleted: boolean;
auth?: string;
}
export interface MarkPrivateMessageAsReadForm {
edit_id: number;
read: boolean;
auth?: string;
}
@ -864,7 +876,6 @@ export interface UserJoinResponse {
}
export type MessageType =
| EditPrivateMessageForm
| LoginForm
| RegisterForm
| CommunityForm
@ -900,6 +911,8 @@ export type MessageType =
| PasswordChangeForm
| PrivateMessageForm
| EditPrivateMessageForm
| DeletePrivateMessageForm
| MarkPrivateMessageAsReadForm
| GetPrivateMessagesForm
| SiteConfigForm;

View File

@ -36,6 +36,8 @@ import {
PasswordChangeForm,
PrivateMessageForm,
EditPrivateMessageForm,
DeletePrivateMessageForm,
MarkPrivateMessageAsReadForm,
GetPrivateMessagesForm,
GetCommentsForm,
UserJoinForm,
@ -315,6 +317,18 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.EditPrivateMessage, form));
}
public deletePrivateMessage(form: DeletePrivateMessageForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.DeletePrivateMessage, form));
}
public markPrivateMessageAsRead(form: MarkPrivateMessageAsReadForm) {
this.setAuth(form);
this.ws.send(
this.wsSendWrapper(UserOperation.MarkPrivateMessageAsRead, form)
);
}
public getPrivateMessages(form: GetPrivateMessagesForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form));