diff --git a/jirs-client/js/css/sidebar.css b/jirs-client/js/css/sidebar.css index 17795aca..730d2bc4 100644 --- a/jirs-client/js/css/sidebar.css +++ b/jirs-client/js/css/sidebar.css @@ -113,13 +113,13 @@ nav#sidebar .linkItem > a > .linkText { .styledTooltip.messages > .messagesList > .message { padding: 15px; - max-height: 90px; - overflow: hidden; + /*max-height: 90px;*/ + /*overflow: hidden;*/ } -.styledTooltip.messages > .messagesList > .message:hover { - max-height: 100%; -} +/*.styledTooltip.messages > .messagesList > .message:hover {*/ +/* max-height: 100%;*/ +/*}*/ .styledTooltip.messages > .messagesList > .message > .top { display: flex; diff --git a/jirs-client/src/ws/mod.rs b/jirs-client/src/ws/mod.rs index 316c4e54..a2bc96dc 100644 --- a/jirs-client/src/ws/mod.rs +++ b/jirs-client/src/ws/mod.rs @@ -213,8 +213,21 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders) { } } // messages + WsMsg::Message(received) => { + let mut old = vec![]; + std::mem::swap(&mut old, &mut model.messages); + for m in old { + if m.id != received.id { + model.messages.push(m); + } else { + model.messages.push(received.clone()); + } + } + model.messages.sort_by(|a, b| a.id.cmp(&b.id)); + } WsMsg::MessagesResponse(v) => { model.messages = v.clone(); + model.messages.sort_by(|a, b| a.id.cmp(&b.id)); } WsMsg::MessageMarkedSeen(id) => { let mut old = vec![]; @@ -224,6 +237,7 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders) { model.messages.push(m); } } + model.messages.sort_by(|a, b| a.id.cmp(&b.id)); } _ => (), }; diff --git a/jirs-server/src/db/invitations.rs b/jirs-server/src/db/invitations.rs index b18b8107..8aed5738 100644 --- a/jirs-server/src/db/invitations.rs +++ b/jirs-server/src/db/invitations.rs @@ -9,7 +9,7 @@ use jirs_data::{ }; use crate::db::tokens::CreateBindToken; -use crate::db::users::{FindUser, Register}; +use crate::db::users::{LookupUser, Register}; use crate::db::DbExecutor; use crate::errors::ServiceErrors; @@ -236,7 +236,7 @@ impl Handler for DbExecutor { }; let user: User = self.handle( - FindUser { + LookupUser { name: invitation.name.clone(), email: invitation.email.clone(), }, diff --git a/jirs-server/src/db/messages.rs b/jirs-server/src/db/messages.rs index 1a8e058a..c662bf20 100644 --- a/jirs-server/src/db/messages.rs +++ b/jirs-server/src/db/messages.rs @@ -1,11 +1,13 @@ use actix::Handler; use diesel::prelude::*; -use jirs_data::{Message, MessageId, UserId}; +use jirs_data::{BindToken, Message, MessageId, MessageType, User, UserId}; +use crate::db::users::{FindUser, LookupUser}; use crate::db::DbExecutor; use crate::errors::ServiceErrors; +#[derive(Debug)] pub struct LoadMessages { pub user_id: UserId, } @@ -36,6 +38,7 @@ impl Handler for DbExecutor { } } +#[derive(Debug)] pub struct MarkMessageSeen { pub user_id: UserId, pub message_id: MessageId, @@ -61,13 +64,119 @@ impl Handler for DbExecutor { .find(msg.message_id) .filter(receiver_id.eq(msg.user_id)), ); + debug!("{}", diesel::debug_query::(&query)); + let size = query + .execute(conn) + .map_err(|_| ServiceErrors::DatabaseQueryFailed("load user messages".to_string()))?; + + if size > 0 { + Ok(msg.message_id) + } else { + Err(ServiceErrors::DatabaseQueryFailed(format!( + "failed to delete message for {:?}", + msg + ))) + } + } +} + +#[derive(Debug)] +pub enum CreateMessageReceiver { + Reference(UserId), + Lookup { name: String, email: String }, +} + +#[derive(Debug)] +pub struct CreateMessage { + pub receiver: CreateMessageReceiver, + pub sender_id: UserId, + pub summary: String, + pub description: String, + pub message_type: MessageType, + pub hyper_link: String, +} + +impl actix::Message for CreateMessage { + type Result = Result; +} + +impl Handler for DbExecutor { + type Result = Result; + + fn handle(&mut self, msg: CreateMessage, ctx: &mut Self::Context) -> Self::Result { + use crate::schema::messages::dsl::*; + + let conn = &self + .pool + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + let user: User = match { + match msg.receiver { + CreateMessageReceiver::Lookup { name, email } => { + self.handle(LookupUser { name, email }, ctx) + } + CreateMessageReceiver::Reference(user_id) => self.handle(FindUser { user_id }, ctx), + } + } { + Ok(user) => user, + _ => { + return Err(ServiceErrors::RecordNotFound( + "No matching user found".to_string(), + )) + } + }; + + let query = diesel::insert_into(messages).values(( + receiver_id.eq(user.id), + sender_id.eq(msg.sender_id), + summary.eq(msg.summary), + description.eq(msg.description), + message_type.eq(msg.message_type), + hyper_link.eq(msg.hyper_link), + )); debug!( "{}", diesel::debug_query::(&query).to_string() ); query - .execute(conn) - .map_err(|_| ServiceErrors::DatabaseQueryFailed("load user messages".to_string()))?; - Ok(msg.message_id) + .get_result(conn) + .map_err(|_| ServiceErrors::DatabaseQueryFailed("create message failed".to_string())) + } +} + +#[derive(Debug)] +pub struct LookupMessagesByToken { + pub token: BindToken, + pub user_id: UserId, +} + +impl actix::Message for LookupMessagesByToken { + type Result = Result, ServiceErrors>; +} + +impl Handler for DbExecutor { + type Result = Result, ServiceErrors>; + + fn handle(&mut self, msg: LookupMessagesByToken, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::messages::dsl::*; + + let conn = &self + .pool + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + let query = messages.filter( + hyper_link + .eq(format!("#{}", msg.token)) + .and(receiver_id.eq(msg.user_id)), + ); + debug!( + "{}", + diesel::debug_query::(&query).to_string() + ); + query + .load(conn) + .map_err(|_| ServiceErrors::DatabaseQueryFailed("create message failed".to_string())) } } diff --git a/jirs-server/src/db/users.rs b/jirs-server/src/db/users.rs index b863c0b7..c09fd302 100644 --- a/jirs-server/src/db/users.rs +++ b/jirs-server/src/db/users.rs @@ -1,4 +1,5 @@ use actix::{Handler, Message}; +use diesel::connection::TransactionManager; use diesel::pg::Pg; use diesel::prelude::*; use serde::{Deserialize, Serialize}; @@ -9,12 +10,10 @@ use crate::db::projects::CreateProject; use crate::db::{DbExecutor, DbPooledConn}; use crate::errors::ServiceErrors; use crate::schema::users::all_columns; -use diesel::connection::TransactionManager; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Debug)] pub struct FindUser { - pub name: String, - pub email: String, + pub user_id: UserId, } impl Message for FindUser { @@ -27,6 +26,35 @@ impl Handler for DbExecutor { fn handle(&mut self, msg: FindUser, _ctx: &mut Self::Context) -> Self::Result { use crate::schema::users::dsl::*; + let conn = &self + .pool + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + let query = users.find(msg.user_id); + debug!("{}", diesel::debug_query::(&query)); + query + .first(conn) + .map_err(|_| ServiceErrors::RecordNotFound(format!("user with id = {}", msg.user_id))) + } +} + +#[derive(Debug)] +pub struct LookupUser { + pub name: String, + pub email: String, +} + +impl Message for LookupUser { + type Result = Result; +} + +impl Handler for DbExecutor { + type Result = Result; + + fn handle(&mut self, msg: LookupUser, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::users::dsl::*; + let conn = &self .pool .get() @@ -43,7 +71,7 @@ impl Handler for DbExecutor { } } -#[derive(Serialize, Deserialize)] +#[derive(Debug)] pub struct LoadProjectUsers { pub project_id: i32, } @@ -76,7 +104,7 @@ impl Handler for DbExecutor { } } -#[derive(Serialize, Deserialize)] +#[derive(Debug)] pub struct LoadIssueAssignees { pub issue_id: i32, } @@ -109,7 +137,7 @@ impl Handler for DbExecutor { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Debug)] pub struct Register { pub name: String, pub email: String, @@ -309,11 +337,13 @@ impl Handler for DbExecutor { #[cfg(test)] mod tests { + use diesel::connection::TransactionManager; + + use jirs_data::{Project, ProjectCategory}; + use crate::db::build_pool; use super::*; - use diesel::connection::TransactionManager; - use jirs_data::{Project, ProjectCategory}; #[test] fn check_collision() { diff --git a/jirs-server/src/ws/auth.rs b/jirs-server/src/ws/auth.rs index d47033f4..79a6e1da 100644 --- a/jirs-server/src/ws/auth.rs +++ b/jirs-server/src/ws/auth.rs @@ -5,7 +5,7 @@ use jirs_data::{Token, WsMsg}; use crate::db::authorize_user::AuthorizeUser; use crate::db::tokens::{CreateBindToken, FindBindToken}; -use crate::db::users::FindUser; +use crate::db::users::LookupUser; use crate::mail::welcome::Welcome; use crate::ws::{WebSocketActor, WsHandler, WsResult}; @@ -18,7 +18,7 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: Authenticate, _ctx: &mut Self::Context) -> WsResult { let Authenticate { name, email } = msg; // TODO check attempt number, allow only 5 times per day - let user = match block_on(self.db.send(FindUser { name, email })) { + let user = match block_on(self.db.send(LookupUser { name, email })) { Ok(Ok(user)) => user, Ok(Err(e)) => { error!("{:?}", e); diff --git a/jirs-server/src/ws/invitations.rs b/jirs-server/src/ws/invitations.rs index c8ca55ef..9fa481f3 100644 --- a/jirs-server/src/ws/invitations.rs +++ b/jirs-server/src/ws/invitations.rs @@ -1,9 +1,12 @@ use futures::executor::block_on; -use jirs_data::{EmailString, InvitationId, InvitationToken, UserRole, UsernameString, WsMsg}; +use jirs_data::{ + EmailString, InvitationId, InvitationToken, MessageType, UserRole, UsernameString, WsMsg, +}; use crate::db::invitations; -use crate::ws::{WebSocketActor, WsHandler, WsResult}; +use crate::db::messages::CreateMessageReceiver; +use crate::ws::{InnerMsg, WebSocketActor, WsHandler, WsMessageSender, WsResult}; pub struct ListInvitation; @@ -40,18 +43,14 @@ impl WsHandler for WebSocketActor { Some(up) => up.project_id, _ => return Ok(None), }; - let (user_id, inviter_name) = - match self.current_user.as_ref().map(|u| (u.id, u.name.clone())) { - Some(id) => id, - _ => return Ok(None), - }; + let (user_id, inviter_name) = self.require_user().map(|u| (u.id, u.name.clone()))?; let CreateInvitation { email, name, role } = msg; - let invitation = match block_on(self.db.send(invitations::CreateInvitation { + let invitation = match block_on(self.db.send(crate::db::invitations::CreateInvitation { user_id, project_id, - email, - name, + email: email.clone(), + name: name.clone(), role, })) { Ok(Ok(invitation)) => invitation, @@ -80,6 +79,24 @@ impl WsHandler for WebSocketActor { } } + // If user exists then send message to him + match block_on(self.db.send(crate::db::messages::CreateMessage { + receiver: CreateMessageReceiver::Lookup { name, email }, + sender_id: user_id, + summary: "You have been invited to project".to_string(), + description: "You have been invited to project".to_string(), + message_type: MessageType::ReceivedInvitation, + hyper_link: format!("#{}", invitation.bind_token), + })) { + Ok(Ok(message)) => { + self.addr.do_send(InnerMsg::SendToUser( + message.receiver_id, + WsMsg::Message(message), + )); + } + _ => {} + } + Ok(Some(WsMsg::InvitationSendSuccess)) } } @@ -135,21 +152,45 @@ pub struct AcceptInvitation { } impl WsHandler for WebSocketActor { - fn handle_msg(&mut self, msg: AcceptInvitation, _ctx: &mut Self::Context) -> WsResult { + fn handle_msg(&mut self, msg: AcceptInvitation, ctx: &mut Self::Context) -> WsResult { let AcceptInvitation { invitation_token } = msg; - let res = match block_on(self.db.send(invitations::AcceptInvitation { + let token = match block_on(self.db.send(invitations::AcceptInvitation { invitation_token: invitation_token.clone(), })) { - Ok(Ok(token)) => Some(WsMsg::InvitationAcceptSuccess(token.access_token)), + Ok(Ok(token)) => token, Ok(Err(e)) => { error!("{:?}", e); - Some(WsMsg::InvitationAcceptFailure(invitation_token)) + return Ok(Some(WsMsg::InvitationAcceptFailure(invitation_token))); } Err(e) => { error!("{}", e); - Some(WsMsg::InvitationAcceptFailure(invitation_token)) + return Ok(Some(WsMsg::InvitationAcceptFailure(invitation_token))); } }; - Ok(res) + + for message in block_on(self.db.send(crate::db::messages::LookupMessagesByToken { + token: invitation_token, + user_id: token.user_id, + })) + .unwrap_or(Ok(vec![])) + .unwrap_or_default() + { + match block_on(self.db.send(crate::db::messages::MarkMessageSeen { + user_id: token.user_id, + message_id: message.id, + })) { + Ok(Ok(id)) => { + ctx.send_msg(&WsMsg::MessageMarkedSeen(id)); + } + Ok(Err(e)) => { + error!("{:?}", e); + } + Err(e) => { + error!("{}", e); + } + } + } + + Ok(Some(WsMsg::InvitationAcceptSuccess(token.access_token))) } } diff --git a/jirs-server/src/ws/mod.rs b/jirs-server/src/ws/mod.rs index cbe89b7d..5953e605 100644 --- a/jirs-server/src/ws/mod.rs +++ b/jirs-server/src/ws/mod.rs @@ -325,6 +325,7 @@ pub enum InnerMsg { Join(ProjectId, UserId, Recipient), Leave(ProjectId, UserId, Recipient), BroadcastToChannel(ProjectId, WsMsg), + SendToUser(UserId, WsMsg), Transfer(WsMsg), } @@ -381,13 +382,17 @@ impl Handler for WsServer { v.remove_item(&recipient); } } + InnerMsg::SendToUser(user_id, msg) => { + if let Some(v) = self.sessions.get(&user_id) { + self.send_to_recipients(v, &msg); + } + } InnerMsg::BroadcastToChannel(project_id, msg) => { debug!("Begin broadcast to channel {} msg {:?}", project_id, msg); let set = match self.rooms.get(&project_id) { Some(s) => s, _ => return debug!(" channel not found, aborting..."), }; - let _s = set.len(); for r in set.keys() { let v = match self.sessions.get(r) { Some(v) => v, @@ -396,12 +401,7 @@ impl Handler for WsServer { continue; } }; - for recipient in v.iter() { - match recipient.do_send(InnerMsg::Transfer(msg.clone())) { - Ok(_) => debug!("msg sent"), - Err(e) => error!("{}", e), - }; - } + self.send_to_recipients(v, &msg); } } _ => (), @@ -413,6 +413,15 @@ impl WsServer { pub fn ensure_room(&mut self, room: i32) { self.rooms.entry(room).or_insert_with(HashMap::new); } + + fn send_to_recipients(&self, recipients: &Vec>, msg: &WsMsg) { + for recipient in recipients.iter() { + match recipient.do_send(InnerMsg::Transfer(msg.clone())) { + Ok(_) => debug!("msg sent"), + Err(e) => error!("{}", e), + }; + } + } } #[get("/ws/")]