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%;
}
.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 {

View File

@ -42,6 +42,9 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
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>> {
let Message {
id: _,
id,
receiver_id: _,
sender_id: _,
summary,
@ -164,6 +167,7 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
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<Node<Msg>> {
};
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<Node<Msg>> {
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,
],

View File

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

View File

@ -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<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 id: InvitationId,
}
@ -119,24 +154,14 @@ impl Message for RevokeInvitation {
impl Handler<RevokeInvitation> 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::<Pg, _>(&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<AcceptInvitation> 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::<Pg, _>(&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<AcceptInvitation> for DbExecutor {
.filter(state.eq(InvitationState::Sent));
debug!("{}", diesel::debug_query::<Pg, _>(&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<AcceptInvitation> for DbExecutor {
role.eq(invitation.role),
));
debug!("{}", diesel::debug_query::<Pg, _>(&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<AcceptInvitation> for DbExecutor {
let query = tokens.filter(user_id.eq(user.id)).order_by(id.desc());
debug!("{}", diesel::debug_query::<Pg, _>(&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)
}
}

View File

@ -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<LoadMessages> 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<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 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<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
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<UserProject, WsMsg> {
let user_id = self.require_user()?.id;
match block_on(self.db.send(CurrentUserProject { user_id })) {