diff --git a/bank_core/src/chat.rs b/bank_core/src/chat.rs index b55e98d..471251c 100644 --- a/bank_core/src/chat.rs +++ b/bank_core/src/chat.rs @@ -12,6 +12,7 @@ use crate::{ pub struct Chat { pub id: Uuid, pub created: DateTime, + pub read_until: Option, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] diff --git a/openapi-def.yaml b/openapi-def.yaml index 73159bd..c0cd38d 100644 --- a/openapi-def.yaml +++ b/openapi-def.yaml @@ -537,6 +537,38 @@ paths: $ref: '#/components/responses/InvalidBody' default: $ref: '#/components/responses/Default' + /api/chats/{chatId}/messages/{messageId}/read: + post: + operationId: mark-chat-message-read + summary: Mark message as read/unread + tags: + - Chats + security: + - bearer: [] + parameters: + - $ref: '#/components/parameters/ChatId' + - $ref: '#/components/parameters/MessageId' + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/ChatMessage' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + 404: + $ref: '#/components/responses/ResourceNotFound' + 422: + $ref: '#/components/responses/InvalidBody' + default: + $ref: '#/components/responses/Default' /api/socket: get: operationId: websocket-events @@ -586,6 +618,13 @@ components: schema: type: string format: uuid + MessageId: + name: messageId + in: path + required: true + schema: + type: string + format: uuid PaginationLimit: name: limit in: query diff --git a/src/api/chats.rs b/src/api/chats.rs index 35b864d..737a430 100644 --- a/src/api/chats.rs +++ b/src/api/chats.rs @@ -1,6 +1,10 @@ use std::sync::Arc; -use axum::{Router, extract::Path, routing::get}; +use axum::{ + Router, + extract::Path, + routing::{get, post}, +}; use bank_core::{ ApiError, chat::{Chat, ChatInfo, ChatMessage, SendMessage, StartChat}, @@ -23,6 +27,7 @@ pub(super) fn router() -> Router> { .route("/", get(list_chats).post(create_chat)) .route("/{id}", get(get_chat)) .route("/{id}/messages", get(get_messages).post(send_message)) + .route("/{id}/messages/{messageId}/read", post(mark_read)) } #[instrument(skip(state))] @@ -108,3 +113,14 @@ pub async fn send_message( } Ok(Json(message)) } + +pub async fn mark_read( + EState(state): State, + auth: Auth, + Path((chat_id, message_id)): Path<(Uuid, Uuid)>, +) -> Result<(), Error> { + let mut client = state.conn().await?; + check_chat(&client, chat_id, auth.user_id()).await?; + Chats::mark_read(&mut client, auth.user_id(), chat_id, message_id).await?; + Ok(()) +} diff --git a/src/model/chats.rs b/src/model/chats.rs index 8f532d5..8936f52 100644 --- a/src/model/chats.rs +++ b/src/model/chats.rs @@ -14,6 +14,7 @@ fn chat_from_row(row: Row) -> Chat { Chat { id: row.get("id"), created: row.get("created"), + read_until: row.get("read_until"), } } fn chat_info_from_row(row: Row) -> ChatInfo { @@ -138,6 +139,7 @@ impl Chats { Ok(Chat { id, created: result.get(0), + read_until: None, }) } @@ -147,7 +149,7 @@ impl Chats { user: Uuid, pagination: RequestPagination, ) -> Result, tokio_postgres::Error> { - let stmt = client.prepare_cached("select c.id as id, c.created as created from chat_members cm join chats c on cm.chat = c.id where cm.\"user\" = $1 order by c.created desc limit $2 offset $3").await?; + let stmt = client.prepare_cached("select c.*, cmru.read_until, array_agg(cm.\"user\") as members from chats c join chat_members cm on cm.chat = c.id join chat_members cmru on cmru.chat = c.id and cmru.\"user\" = $1 where cm.\"user\" = $1 group by c.id, cmru.read_until order by c.created desc limit $2 offset $3").await?; let count_stmt = client.prepare_cached("select count(c.id) from chat_members cm join chats c on cm.chat = c.id where cm.\"user\" = $1").await?; let result = client .query(&stmt, &[&user, &pagination.limit(), &pagination.offset()]) @@ -207,4 +209,19 @@ impl Chats { extra, }) } + + pub async fn mark_read( + client: &mut impl GenericClient, + user: Uuid, + chat: Uuid, + message: Uuid, + ) -> Result { + let stmt = client + .prepare_cached( + "update chat_members set read_until = $3 where chat = $1 and \"user\" = $2", + ) + .await?; + let rows = client.execute(&stmt, &[&chat, &user, &message]).await?; + Ok(rows == 1) + } } diff --git a/tests/integration/chats.hurl b/tests/integration/chats.hurl index e2ee6c4..22361ee 100644 --- a/tests/integration/chats.hurl +++ b/tests/integration/chats.hurl @@ -119,6 +119,54 @@ jsonpath "$.extra" == null [Captures] message2: jsonpath "$.id" +## Test read status + +GET {{host}}/api/chats/{{chat}} +Authorization: Bearer {{user1-token}} +HTTP 200 +[Asserts] +jsonpath "$.id" == {{chat}} +jsonpath "$.read_until" == null + +GET {{host}}/api/chats/{{chat}} +Authorization: Bearer {{user2-token}} +HTTP 200 +[Asserts] +jsonpath "$.id" == {{chat}} +jsonpath "$.read_until" == null + + +POST {{host}}/api/chats/{{chat}}/messages/{{message2}}/read +Authorization: Bearer {{user2-token}} +HTTP 200 + +GET {{host}}/api/chats/{{chat}} +Authorization: Bearer {{user2-token}} +HTTP 200 +[Asserts] +jsonpath "$.id" == {{chat}} +jsonpath "$.read_until" == {{message2}} + + +POST {{host}}/api/chats/{{chat}}/messages/{{message1}}/read +Authorization: Bearer {{user2-token}} +HTTP 200 + +GET {{host}}/api/chats/{{chat}} +Authorization: Bearer {{user2-token}} +HTTP 200 +[Asserts] +jsonpath "$.id" == {{chat}} +jsonpath "$.read_until" == {{message1}} + + +GET {{host}}/api/chats/{{chat}} +Authorization: Bearer {{user1-token}} +HTTP 200 +[Asserts] +jsonpath "$.id" == {{chat}} +jsonpath "$.read_until" == null + # Verify list messages endpoint GET {{host}}/api/chats/{{chat}}/messages?limit=50