From d3b2fbce32df7244d8fe16020672a08c81f25d35 Mon Sep 17 00:00:00 2001 From: Adrian Wozniak Date: Fri, 29 May 2020 21:14:07 +0200 Subject: [PATCH] Handle mark seen --- jirs-client/js/css/sidebar.css | 13 +++- jirs-client/src/shared/navbar_left.rs | 27 ++++++-- jirs-client/src/ws/mod.rs | 9 +++ jirs-server/src/db/invitations.rs | 96 ++++++++++++++++++++------- jirs-server/src/db/messages.rs | 44 +++++++++++- jirs-server/src/ws/messages.rs | 29 +++++++- jirs-server/src/ws/mod.rs | 8 +-- 7 files changed, 185 insertions(+), 41 deletions(-) diff --git a/jirs-client/js/css/sidebar.css b/jirs-client/js/css/sidebar.css index e43d23c2..17795aca 100644 --- a/jirs-client/js/css/sidebar.css +++ b/jirs-client/js/css/sidebar.css @@ -121,8 +121,19 @@ nav#sidebar .linkItem > a > .linkText { max-height: 100%; } -.styledTooltip.messages > .messagesList > .message > .summary { +.styledTooltip.messages > .messagesList > .message > .top { + display: flex; + justify-content: space-between; +} + +.styledTooltip.messages > .messagesList > .message > .top > .summary { font-family: var(--font-bold); + font-size: 20px; + line-height: 32px; +} + +.styledTooltip.messages > .messagesList > .message > .top > .action { + width: 32px; } .styledTooltip.messages > .messagesList > .message > .description { diff --git a/jirs-client/src/shared/navbar_left.rs b/jirs-client/src/shared/navbar_left.rs index b7fbbcbe..970ee89c 100644 --- a/jirs-client/src/shared/navbar_left.rs +++ b/jirs-client/src/shared/navbar_left.rs @@ -42,6 +42,9 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { orders, ); } + Msg::MessageSeen(id) => { + send_ws_msg(WsMsg::MessageMarkSeen(*id), model.ws.as_ref(), orders); + } _ => (), } } @@ -154,7 +157,7 @@ fn messages_tooltip_popup(model: &Model) -> Node { fn message_ui(model: &Model, message: &Message) -> Option> { let Message { - id: _, + id, receiver_id: _, sender_id: _, summary, @@ -164,6 +167,7 @@ fn message_ui(model: &Model, message: &Message) -> Option> { created_at: _, updated_at: _, } = message; + let message_id = *id; let hyperlink = if hyper_link.is_empty() && !hyper_link.starts_with("#") { empty![] @@ -181,6 +185,21 @@ fn message_ui(model: &Model, message: &Message) -> Option> { }; let message_description = parse_description(model, description.as_str()); + let close_button = StyledButton::build() + .icon(Icon::Close) + .empty() + .on_click(mouse_ev(Ev::Click, move |ev| { + ev.stop_propagation(); + ev.prevent_default(); + Some(Msg::MessageSeen(message_id)) + })) + .build() + .into_node(); + let top = div![ + class!["top"], + div![class!["summary"], summary], + div![class!["action"], close_button], + ]; let node = match message_type { MessageType::ReceivedInvitation => { @@ -215,20 +234,20 @@ fn message_ui(model: &Model, message: &Message) -> Option> { div![ class!["message"], attrs![At::Class => format!("{}", message_type)], - div![class!["summary"], summary], + top, div![class!["description"], message_description], div![class!["actions"], accept, reject], ] } MessageType::AssignedToIssue => div![ class!["message assignedToIssue"], - div![class!["summary"], summary], + top, div![class!["description"], message_description], hyperlink, ], MessageType::Mention => div![ class!["message mention"], - div![class!["summary"], summary], + top, div![class!["description"], message_description], hyperlink, ], diff --git a/jirs-client/src/ws/mod.rs b/jirs-client/src/ws/mod.rs index e29f0544..316c4e54 100644 --- a/jirs-client/src/ws/mod.rs +++ b/jirs-client/src/ws/mod.rs @@ -216,6 +216,15 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders) { WsMsg::MessagesResponse(v) => { model.messages = v.clone(); } + WsMsg::MessageMarkedSeen(id) => { + let mut old = vec![]; + std::mem::swap(&mut old, &mut model.messages); + for m in old { + if m.id != *id { + model.messages.push(m); + } + } + } _ => (), }; orders.render(); diff --git a/jirs-server/src/db/invitations.rs b/jirs-server/src/db/invitations.rs index a23858ba..b18b8107 100644 --- a/jirs-server/src/db/invitations.rs +++ b/jirs-server/src/db/invitations.rs @@ -1,4 +1,5 @@ use actix::{Handler, Message}; +use diesel::connection::TransactionManager; use diesel::pg::Pg; use diesel::prelude::*; @@ -108,6 +109,40 @@ impl Handler for DbExecutor { } } +struct UpdateInvitationState { + pub id: InvitationId, + pub state: InvitationState, +} + +impl Message for UpdateInvitationState { + type Result = Result<(), ServiceErrors>; +} + +impl Handler for DbExecutor { + type Result = Result<(), ServiceErrors>; + + fn handle(&mut self, msg: UpdateInvitationState, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::invitations::dsl::*; + + let conn = &self + .pool + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + let query = diesel::update(invitations) + .set(( + state.eq(msg.state), + updated_at.eq(chrono::Utc::now().naive_utc()), + )) + .filter(id.eq(msg.id)); + debug!("{}", diesel::debug_query::(&query).to_string()); + query + .execute(conn) + .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?; + Ok(()) + } +} + pub struct RevokeInvitation { pub id: InvitationId, } @@ -119,24 +154,14 @@ impl Message for RevokeInvitation { impl Handler for DbExecutor { type Result = Result<(), ServiceErrors>; - fn handle(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::invitations::dsl::*; - - let conn = &self - .pool - .get() - .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; - let query = diesel::update(invitations) - .set(( - state.eq(InvitationState::Revoked), - updated_at.eq(chrono::Utc::now().naive_utc()), - )) - .filter(id.eq(msg.id)); - debug!("{}", diesel::debug_query::(&query).to_string()); - query - .execute(conn) - .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?; - Ok(()) + fn handle(&mut self, msg: RevokeInvitation, ctx: &mut Self::Context) -> Self::Result { + self.handle( + UpdateInvitationState { + id: msg.id, + state: InvitationState::Revoked, + }, + ctx, + ) } } @@ -159,13 +184,24 @@ impl Handler for DbExecutor { .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + let tm = conn.transaction_manager(); + + tm.begin_transaction(conn) + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + let query = invitations.filter(bind_token.eq(msg.invitation_token)); debug!("{}", diesel::debug_query::(&query).to_string()); - let invitation: Invitation = query - .first(conn) - .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?; + let invitation: Invitation = query.first(conn).map_err(|e| { + if tm.rollback_transaction(conn).is_err() { + return ServiceErrors::DatabaseConnectionLost; + } + ServiceErrors::DatabaseQueryFailed(format!("{}", e)) + })?; if invitation.state == InvitationState::Revoked { + if tm.rollback_transaction(conn).is_err() { + return Err(ServiceErrors::DatabaseConnectionLost); + } return Err(ServiceErrors::DatabaseQueryFailed( "This invitation is no longer valid".to_string(), )); @@ -180,6 +216,9 @@ impl Handler for DbExecutor { .filter(state.eq(InvitationState::Sent)); debug!("{}", diesel::debug_query::(&query).to_string()); query.execute(conn).map_err(|e| { + if tm.rollback_transaction(conn).is_err() { + return ServiceErrors::DatabaseConnectionLost; + } ServiceErrors::DatabaseQueryFailed(format!("update invitation {} {}", invitation.id, e)) })?; @@ -214,9 +253,12 @@ impl Handler for DbExecutor { role.eq(invitation.role), )); debug!("{}", diesel::debug_query::(&query)); - query - .execute(conn) - .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?; + query.execute(conn).map_err(|e| { + if tm.rollback_transaction(conn).is_err() { + return ServiceErrors::DatabaseConnectionLost; + } + ServiceErrors::DatabaseQueryFailed(format!("{}", e)) + })?; }; let token = { @@ -225,10 +267,16 @@ impl Handler for DbExecutor { let query = tokens.filter(user_id.eq(user.id)).order_by(id.desc()); debug!("{}", diesel::debug_query::(&query)); query.first(conn).map_err(|e| { + if tm.rollback_transaction(conn).is_err() { + return ServiceErrors::DatabaseConnectionLost; + } ServiceErrors::DatabaseQueryFailed(format!("token for user {} {}", user.id, e)) })? }; + tm.commit_transaction(conn) + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + Ok(token) } } diff --git a/jirs-server/src/db/messages.rs b/jirs-server/src/db/messages.rs index 93c4bd29..1a8e058a 100644 --- a/jirs-server/src/db/messages.rs +++ b/jirs-server/src/db/messages.rs @@ -1,8 +1,10 @@ -use crate::db::DbExecutor; -use crate::errors::ServiceErrors; use actix::Handler; use diesel::prelude::*; -use jirs_data::{Message, UserId}; + +use jirs_data::{Message, MessageId, UserId}; + +use crate::db::DbExecutor; +use crate::errors::ServiceErrors; pub struct LoadMessages { pub user_id: UserId, @@ -33,3 +35,39 @@ impl Handler for DbExecutor { .map_err(|_| ServiceErrors::DatabaseQueryFailed("load user messages".to_string())) } } + +pub struct MarkMessageSeen { + pub user_id: UserId, + pub message_id: MessageId, +} + +impl actix::Message for MarkMessageSeen { + type Result = Result; +} + +impl Handler for DbExecutor { + type Result = Result; + + fn handle(&mut self, msg: MarkMessageSeen, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::messages::dsl::*; + + let conn = &self + .pool + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + let query = diesel::delete( + messages + .find(msg.message_id) + .filter(receiver_id.eq(msg.user_id)), + ); + debug!( + "{}", + diesel::debug_query::(&query).to_string() + ); + query + .execute(conn) + .map_err(|_| ServiceErrors::DatabaseQueryFailed("load user messages".to_string()))?; + Ok(msg.message_id) + } +} diff --git a/jirs-server/src/ws/messages.rs b/jirs-server/src/ws/messages.rs index 28f4ca1a..f0003738 100644 --- a/jirs-server/src/ws/messages.rs +++ b/jirs-server/src/ws/messages.rs @@ -1,8 +1,9 @@ -use crate::ws::{WebSocketActor, WsHandler, WsResult}; use futures::executor::block_on; +use jirs_data::{MessageId, WsMsg}; + use crate::db::messages; -use jirs_data::WsMsg; +use crate::ws::{WebSocketActor, WsHandler, WsResult}; pub struct LoadMessages; @@ -22,3 +23,27 @@ impl WsHandler for WebSocketActor { } } } + +pub struct MarkMessageSeen { + pub id: MessageId, +} + +impl WsHandler for WebSocketActor { + fn handle_msg(&mut self, msg: MarkMessageSeen, _ctx: &mut Self::Context) -> WsResult { + let user_id = self.require_user()?.id; + match block_on(self.db.send(messages::MarkMessageSeen { + message_id: msg.id, + user_id, + })) { + Ok(Ok(id)) => Ok(Some(WsMsg::MessageMarkedSeen(id))), + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } + } + } +} diff --git a/jirs-server/src/ws/mod.rs b/jirs-server/src/ws/mod.rs index 4f0c8ed1..cbe89b7d 100644 --- a/jirs-server/src/ws/mod.rs +++ b/jirs-server/src/ws/mod.rs @@ -189,6 +189,7 @@ impl WebSocketActor { // messages WsMsg::MessagesRequest => self.handle_msg(LoadMessages, ctx)?, + WsMsg::MessageMarkSeen(id) => self.handle_msg(MarkMessageSeen { id }, ctx)?, // else fail _ => { @@ -241,13 +242,6 @@ impl WebSocketActor { }) } - // fn require_project(&self) -> Result<&Project, WsMsg> { - // self.current_project - // .as_ref() - // .map(|u| u) - // .ok_or_else(|| WsMsg::AuthorizeExpired) - // } - fn load_user_project(&self) -> Result { let user_id = self.require_user()?.id; match block_on(self.db.send(CurrentUserProject { user_id })) {