Handle mark seen

This commit is contained in:
Adrian Wozniak 2020-05-29 21:14:07 +02:00
parent e7ade80fe3
commit d3b2fbce32
7 changed files with 185 additions and 41 deletions

View File

@ -121,8 +121,19 @@ nav#sidebar .linkItem > a > .linkText {
max-height: 100%; 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-family: var(--font-bold);
font-size: 20px;
line-height: 32px;
}
.styledTooltip.messages > .messagesList > .message > .top > .action {
width: 32px;
} }
.styledTooltip.messages > .messagesList > .message > .description { .styledTooltip.messages > .messagesList > .message > .description {

View File

@ -42,6 +42,9 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
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<Msg> {
fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> { fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
let Message { let Message {
id: _, id,
receiver_id: _, receiver_id: _,
sender_id: _, sender_id: _,
summary, summary,
@ -164,6 +167,7 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
created_at: _, created_at: _,
updated_at: _, updated_at: _,
} = message; } = message;
let message_id = *id;
let hyperlink = if hyper_link.is_empty() && !hyper_link.starts_with("#") { let hyperlink = if hyper_link.is_empty() && !hyper_link.starts_with("#") {
empty![] empty![]
@ -181,6 +185,21 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
}; };
let message_description = parse_description(model, description.as_str()); 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 { let node = match message_type {
MessageType::ReceivedInvitation => { MessageType::ReceivedInvitation => {
@ -215,20 +234,20 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
div![ div![
class!["message"], class!["message"],
attrs![At::Class => format!("{}", message_type)], attrs![At::Class => format!("{}", message_type)],
div![class!["summary"], summary], top,
div![class!["description"], message_description], div![class!["description"], message_description],
div![class!["actions"], accept, reject], div![class!["actions"], accept, reject],
] ]
} }
MessageType::AssignedToIssue => div![ MessageType::AssignedToIssue => div![
class!["message assignedToIssue"], class!["message assignedToIssue"],
div![class!["summary"], summary], top,
div![class!["description"], message_description], div![class!["description"], message_description],
hyperlink, hyperlink,
], ],
MessageType::Mention => div![ MessageType::Mention => div![
class!["message mention"], class!["message mention"],
div![class!["summary"], summary], top,
div![class!["description"], message_description], div![class!["description"], message_description],
hyperlink, hyperlink,
], ],

View File

@ -216,6 +216,15 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
WsMsg::MessagesResponse(v) => { WsMsg::MessagesResponse(v) => {
model.messages = v.clone(); 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(); orders.render();

View File

@ -1,4 +1,5 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use diesel::connection::TransactionManager;
use diesel::pg::Pg; use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
@ -108,6 +109,40 @@ impl Handler<DeleteInvitation> for DbExecutor {
} }
} }
struct UpdateInvitationState {
pub id: InvitationId,
pub state: InvitationState,
}
impl Message for UpdateInvitationState {
type Result = Result<(), ServiceErrors>;
}
impl Handler<UpdateInvitationState> 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::<Pg, _>(&query).to_string());
query
.execute(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
Ok(())
}
}
pub struct RevokeInvitation { pub struct RevokeInvitation {
pub id: InvitationId, pub id: InvitationId,
} }
@ -119,24 +154,14 @@ impl Message for RevokeInvitation {
impl Handler<RevokeInvitation> for DbExecutor { impl Handler<RevokeInvitation> for DbExecutor {
type Result = Result<(), ServiceErrors>; type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: RevokeInvitation, ctx: &mut Self::Context) -> Self::Result {
use crate::schema::invitations::dsl::*; self.handle(
UpdateInvitationState {
let conn = &self id: msg.id,
.pool state: InvitationState::Revoked,
.get() },
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; ctx,
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::<Pg, _>(&query).to_string());
query
.execute(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
Ok(())
} }
} }
@ -159,13 +184,24 @@ impl Handler<AcceptInvitation> for DbExecutor {
.get() .get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .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)); let query = invitations.filter(bind_token.eq(msg.invitation_token));
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string()); debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
let invitation: Invitation = query let invitation: Invitation = query.first(conn).map_err(|e| {
.first(conn) if tm.rollback_transaction(conn).is_err() {
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?; return ServiceErrors::DatabaseConnectionLost;
}
ServiceErrors::DatabaseQueryFailed(format!("{}", e))
})?;
if invitation.state == InvitationState::Revoked { if invitation.state == InvitationState::Revoked {
if tm.rollback_transaction(conn).is_err() {
return Err(ServiceErrors::DatabaseConnectionLost);
}
return Err(ServiceErrors::DatabaseQueryFailed( return Err(ServiceErrors::DatabaseQueryFailed(
"This invitation is no longer valid".to_string(), "This invitation is no longer valid".to_string(),
)); ));
@ -180,6 +216,9 @@ impl Handler<AcceptInvitation> for DbExecutor {
.filter(state.eq(InvitationState::Sent)); .filter(state.eq(InvitationState::Sent));
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string()); debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
query.execute(conn).map_err(|e| { query.execute(conn).map_err(|e| {
if tm.rollback_transaction(conn).is_err() {
return ServiceErrors::DatabaseConnectionLost;
}
ServiceErrors::DatabaseQueryFailed(format!("update invitation {} {}", invitation.id, e)) ServiceErrors::DatabaseQueryFailed(format!("update invitation {} {}", invitation.id, e))
})?; })?;
@ -214,9 +253,12 @@ impl Handler<AcceptInvitation> for DbExecutor {
role.eq(invitation.role), role.eq(invitation.role),
)); ));
debug!("{}", diesel::debug_query::<Pg, _>(&query)); debug!("{}", diesel::debug_query::<Pg, _>(&query));
query query.execute(conn).map_err(|e| {
.execute(conn) if tm.rollback_transaction(conn).is_err() {
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?; return ServiceErrors::DatabaseConnectionLost;
}
ServiceErrors::DatabaseQueryFailed(format!("{}", e))
})?;
}; };
let token = { let token = {
@ -225,10 +267,16 @@ impl Handler<AcceptInvitation> for DbExecutor {
let query = tokens.filter(user_id.eq(user.id)).order_by(id.desc()); let query = tokens.filter(user_id.eq(user.id)).order_by(id.desc());
debug!("{}", diesel::debug_query::<Pg, _>(&query)); debug!("{}", diesel::debug_query::<Pg, _>(&query));
query.first(conn).map_err(|e| { 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)) ServiceErrors::DatabaseQueryFailed(format!("token for user {} {}", user.id, e))
})? })?
}; };
tm.commit_transaction(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
Ok(token) Ok(token)
} }
} }

View File

@ -1,8 +1,10 @@
use crate::db::DbExecutor;
use crate::errors::ServiceErrors;
use actix::Handler; use actix::Handler;
use diesel::prelude::*; 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 struct LoadMessages {
pub user_id: UserId, pub user_id: UserId,
@ -33,3 +35,39 @@ impl Handler<LoadMessages> for DbExecutor {
.map_err(|_| ServiceErrors::DatabaseQueryFailed("load user messages".to_string())) .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<MessageId, ServiceErrors>;
}
impl Handler<MarkMessageSeen> for DbExecutor {
type Result = Result<MessageId, ServiceErrors>;
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::<diesel::pg::Pg, _>(&query).to_string()
);
query
.execute(conn)
.map_err(|_| ServiceErrors::DatabaseQueryFailed("load user messages".to_string()))?;
Ok(msg.message_id)
}
}

View File

@ -1,8 +1,9 @@
use crate::ws::{WebSocketActor, WsHandler, WsResult};
use futures::executor::block_on; use futures::executor::block_on;
use jirs_data::{MessageId, WsMsg};
use crate::db::messages; use crate::db::messages;
use jirs_data::WsMsg; use crate::ws::{WebSocketActor, WsHandler, WsResult};
pub struct LoadMessages; pub struct LoadMessages;
@ -22,3 +23,27 @@ impl WsHandler<LoadMessages> for WebSocketActor {
} }
} }
} }
pub struct MarkMessageSeen {
pub id: MessageId,
}
impl WsHandler<MarkMessageSeen> 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);
}
}
}
}

View File

@ -189,6 +189,7 @@ impl WebSocketActor {
// messages // messages
WsMsg::MessagesRequest => self.handle_msg(LoadMessages, ctx)?, WsMsg::MessagesRequest => self.handle_msg(LoadMessages, ctx)?,
WsMsg::MessageMarkSeen(id) => self.handle_msg(MarkMessageSeen { id }, ctx)?,
// else fail // 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<UserProject, WsMsg> { fn load_user_project(&self) -> Result<UserProject, WsMsg> {
let user_id = self.require_user()?.id; let user_id = self.require_user()?.id;
match block_on(self.db.send(CurrentUserProject { user_id })) { match block_on(self.db.send(CurrentUserProject { user_id })) {