support marking chat messages as read

This commit is contained in:
DSeeLP 2025-04-03 20:24:27 +02:00
parent a6cdd5377b
commit ec40de6068
5 changed files with 123 additions and 2 deletions

View File

@ -12,6 +12,7 @@ use crate::{
pub struct Chat { pub struct Chat {
pub id: Uuid, pub id: Uuid,
pub created: DateTime<Utc>, pub created: DateTime<Utc>,
pub read_until: Option<Uuid>,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]

View File

@ -537,6 +537,38 @@ paths:
$ref: '#/components/responses/InvalidBody' $ref: '#/components/responses/InvalidBody'
default: default:
$ref: '#/components/responses/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: /api/socket:
get: get:
operationId: websocket-events operationId: websocket-events
@ -586,6 +618,13 @@ components:
schema: schema:
type: string type: string
format: uuid format: uuid
MessageId:
name: messageId
in: path
required: true
schema:
type: string
format: uuid
PaginationLimit: PaginationLimit:
name: limit name: limit
in: query in: query

View File

@ -1,6 +1,10 @@
use std::sync::Arc; use std::sync::Arc;
use axum::{Router, extract::Path, routing::get}; use axum::{
Router,
extract::Path,
routing::{get, post},
};
use bank_core::{ use bank_core::{
ApiError, ApiError,
chat::{Chat, ChatInfo, ChatMessage, SendMessage, StartChat}, chat::{Chat, ChatInfo, ChatMessage, SendMessage, StartChat},
@ -23,6 +27,7 @@ pub(super) fn router() -> Router<Arc<AppState>> {
.route("/", get(list_chats).post(create_chat)) .route("/", get(list_chats).post(create_chat))
.route("/{id}", get(get_chat)) .route("/{id}", get(get_chat))
.route("/{id}/messages", get(get_messages).post(send_message)) .route("/{id}/messages", get(get_messages).post(send_message))
.route("/{id}/messages/{messageId}/read", post(mark_read))
} }
#[instrument(skip(state))] #[instrument(skip(state))]
@ -108,3 +113,14 @@ pub async fn send_message(
} }
Ok(Json(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(())
}

View File

@ -14,6 +14,7 @@ fn chat_from_row(row: Row) -> Chat {
Chat { Chat {
id: row.get("id"), id: row.get("id"),
created: row.get("created"), created: row.get("created"),
read_until: row.get("read_until"),
} }
} }
fn chat_info_from_row(row: Row) -> ChatInfo { fn chat_info_from_row(row: Row) -> ChatInfo {
@ -138,6 +139,7 @@ impl Chats {
Ok(Chat { Ok(Chat {
id, id,
created: result.get(0), created: result.get(0),
read_until: None,
}) })
} }
@ -147,7 +149,7 @@ impl Chats {
user: Uuid, user: Uuid,
pagination: RequestPagination, pagination: RequestPagination,
) -> Result<Pagination<Chat>, tokio_postgres::Error> { ) -> Result<Pagination<Chat>, 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 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 let result = client
.query(&stmt, &[&user, &pagination.limit(), &pagination.offset()]) .query(&stmt, &[&user, &pagination.limit(), &pagination.offset()])
@ -207,4 +209,19 @@ impl Chats {
extra, extra,
}) })
} }
pub async fn mark_read(
client: &mut impl GenericClient,
user: Uuid,
chat: Uuid,
message: Uuid,
) -> Result<bool, tokio_postgres::Error> {
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)
}
} }

View File

@ -119,6 +119,54 @@ jsonpath "$.extra" == null
[Captures] [Captures]
message2: jsonpath "$.id" 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 # Verify list messages endpoint
GET {{host}}/api/chats/{{chat}}/messages?limit=50 GET {{host}}/api/chats/{{chat}}/messages?limit=50