3 Commits

Author SHA1 Message Date
dac20f60e0 Can get read receipts 2025-12-01 11:09:14 +01:00
9359dc5be0 Send read receipts 2025-12-01 10:42:19 +01:00
849aef9343 Update unread messages count only if room is not muted 2025-12-01 10:30:54 +01:00
6 changed files with 123 additions and 7 deletions

View File

@@ -11,7 +11,10 @@ use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind};
use matrix_sdk::media::MediaEventContent; use matrix_sdk::media::MediaEventContent;
use matrix_sdk::room::MessagesOptions; use matrix_sdk::room::MessagesOptions;
use matrix_sdk::room::edit::EditedContent; use matrix_sdk::room::edit::EditedContent;
use matrix_sdk::ruma::api::client::filter::RoomEventFilter;
use matrix_sdk::ruma::api::client::receipt::create_receipt::v3::ReceiptType;
use matrix_sdk::ruma::events::reaction::ReactionEventContent; use matrix_sdk::ruma::events::reaction::ReactionEventContent;
use matrix_sdk::ruma::events::receipt::ReceiptThread;
use matrix_sdk::ruma::events::relation::Annotation; use matrix_sdk::ruma::events::relation::Annotation;
use matrix_sdk::ruma::events::room::message::{ use matrix_sdk::ruma::events::room::message::{
MessageType, RoomMessageEvent, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, MessageType, RoomMessageEvent, RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
@@ -23,7 +26,7 @@ use serde_json::value::RawValue;
#[derive(Serialize)] #[derive(Serialize)]
pub struct APIEvent { pub struct APIEvent {
id: OwnedEventId, pub id: OwnedEventId,
time: MilliSecondsSinceUnixEpoch, time: MilliSecondsSinceUnixEpoch,
sender: OwnedUserId, sender: OwnedUserId,
data: Box<RawValue>, data: Box<RawValue>,
@@ -61,10 +64,14 @@ pub(super) async fn get_events(
room: &Room, room: &Room,
limit: u32, limit: u32,
from: Option<&str>, from: Option<&str>,
filter: Option<RoomEventFilter>,
) -> anyhow::Result<APIEventsList> { ) -> anyhow::Result<APIEventsList> {
let mut msg_opts = MessagesOptions::backward(); let mut msg_opts = MessagesOptions::backward();
msg_opts.from = from.map(str::to_string); msg_opts.from = from.map(str::to_string);
msg_opts.limit = UInt::from(limit); msg_opts.limit = UInt::from(limit);
if let Some(filter) = filter {
msg_opts.filter = filter;
}
let messages = room.messages(msg_opts).await?; let messages = room.messages(msg_opts).await?;
Ok(APIEventsList { Ok(APIEventsList {
@@ -97,8 +104,15 @@ pub async fn get_for_room(
return Ok(HttpResponse::NotFound().json("Room not found!")); return Ok(HttpResponse::NotFound().json("Room not found!"));
}; };
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok().json(
.json(get_events(&room, query.limit.unwrap_or(500), query.from.as_deref()).await?)) get_events(
&room,
query.limit.unwrap_or(500),
query.from.as_deref(),
None,
)
.await?,
))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -266,3 +280,23 @@ pub async fn redact_event(
} }
}) })
} }
/// Send receipt for event
pub async fn receipt(
client: MatrixClientExtractor,
path: web::Path<RoomIdInPath>,
event_path: web::Path<EventIdInPath>,
) -> HttpResult {
let Some(room) = client.client.client.get_room(&path.room_id) else {
return Ok(HttpResponse::NotFound().json("Room not found"));
};
room.send_single_receipt(
ReceiptType::Read,
ReceiptThread::Main,
event_path.event_id.clone(),
)
.await?;
Ok(HttpResponse::Accepted().finish())
}

View File

@@ -8,7 +8,10 @@ use matrix_sdk::notification_settings::{
IsEncrypted, IsOneToOne, NotificationSettings, RoomNotificationMode, IsEncrypted, IsOneToOne, NotificationSettings, RoomNotificationMode,
}; };
use matrix_sdk::room::ParentSpace; use matrix_sdk::room::ParentSpace;
use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId}; use matrix_sdk::ruma::events::receipt::{ReceiptThread, ReceiptType};
use matrix_sdk::ruma::{
MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId,
};
use matrix_sdk::{Room, RoomMemberships}; use matrix_sdk::{Room, RoomMemberships};
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
@@ -71,7 +74,11 @@ impl APIRoomInfo {
parents: parent_spaces, parents: parent_spaces,
number_unread_messages: r.unread_notification_counts().notification_count, number_unread_messages: r.unread_notification_counts().notification_count,
notifications, notifications,
latest_event: get_events(r, 1, None).await?.events.into_iter().next(), latest_event: get_events(r, 1, None, None)
.await?
.events
.into_iter()
.next(),
}) })
} }
} }
@@ -137,3 +144,37 @@ pub async fn room_avatar(
matrix_media_controller::serve_mxc_file(req, uri).await matrix_media_controller::serve_mxc_file(req, uri).await
} }
#[derive(serde::Serialize)]
pub struct UserReceipt {
user: OwnedUserId,
event_id: OwnedEventId,
ts: Option<MilliSecondsSinceUnixEpoch>,
}
/// Get room receipts
pub async fn receipts(client: MatrixClientExtractor, path: web::Path<RoomIdInPath>) -> HttpResult {
let Some(room) = client.client.client.get_room(&path.room_id) else {
return Ok(HttpResponse::NotFound().json("Room not found"));
};
let members = room.members(RoomMemberships::ACTIVE).await?;
let mut receipts = Vec::new();
for m in members {
let Some((event_id, receipt)) = room
.load_user_receipt(ReceiptType::Read, ReceiptThread::Main, m.user_id())
.await?
else {
continue;
};
receipts.push(UserReceipt {
user: m.user_id().to_owned(),
event_id,
ts: receipt.ts,
})
}
Ok(HttpResponse::Ok().json(receipts))
}

View File

@@ -155,6 +155,10 @@ async fn main() -> std::io::Result<()> {
"/api/matrix/room/{room_id}/avatar", "/api/matrix/room/{room_id}/avatar",
web::get().to(matrix_room_controller::room_avatar), web::get().to(matrix_room_controller::room_avatar),
) )
.route(
"/api/matrix/room/{room_id}/receipts",
web::get().to(matrix_room_controller::receipts),
)
// Matrix profile controller // Matrix profile controller
.route( .route(
"/api/matrix/profile/{user_id}", "/api/matrix/profile/{user_id}",
@@ -189,6 +193,10 @@ async fn main() -> std::io::Result<()> {
"/api/matrix/room/{room_id}/event/{event_id}", "/api/matrix/room/{room_id}/event/{event_id}",
web::delete().to(matrix_event_controller::redact_event), web::delete().to(matrix_event_controller::redact_event),
) )
.route(
"/api/matrix/room/{room_id}/event/{event_id}/receipt",
web::post().to(matrix_event_controller::receipt),
)
// Matrix media controller // Matrix media controller
.route( .route(
"/api/matrix/media/{mxc}", "/api/matrix/media/{mxc}",

View File

@@ -140,4 +140,14 @@ export class MatrixApiEvent {
uri: `/matrix/room/${room.id}/event/${event_id}`, uri: `/matrix/room/${room.id}/event/${event_id}`,
}); });
} }
/**
* Send event receipt
*/
static async SendReceipt(room: Room, event_id: string): Promise<void> {
await APIClient.exec({
method: "POST",
uri: `/matrix/room/${room.id}/event/${event_id}/receipt`,
});
}
} }

View File

@@ -110,7 +110,7 @@ function _MainMessageWidget(p: {
p.onRoomsListUpdate((r) => { p.onRoomsListUpdate((r) => {
const n = [...r]; const n = [...r];
const idx = r.findIndex((el) => el.id === m.room_id); const idx = r.findIndex((el) => el.id === m.room_id);
if (idx) if (idx && n[idx].notifications === "AllMessages")
n[idx] = { n[idx] = {
...n[idx], ...n[idx],
number_unread_messages: n[idx].number_unread_messages + 1, number_unread_messages: n[idx].number_unread_messages + 1,

View File

@@ -4,14 +4,37 @@ import type { Room } from "../../api/matrix/MatrixApiRoom";
import { RoomEventsManager } from "../../utils/RoomEventsManager"; import { RoomEventsManager } from "../../utils/RoomEventsManager";
import { RoomMessagesList } from "./RoomMessagesList"; import { RoomMessagesList } from "./RoomMessagesList";
import { SendMessageForm } from "./SendMessageForm"; import { SendMessageForm } from "./SendMessageForm";
import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent";
import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider";
export function RoomWidget(p: { export function RoomWidget(p: {
room: Room; room: Room;
users: UsersMap; users: UsersMap;
manager: RoomEventsManager; manager: RoomEventsManager;
}): React.ReactElement { }): React.ReactElement {
const snackbar = useSnackbar();
const receiptId = React.useRef<string | undefined>(undefined);
const handleRoomClick = async () => {
if (p.manager.messages.length === 0) return;
const latest = p.manager.messages[p.manager.messages.length - 1];
if (latest.event_id === receiptId.current) return;
receiptId.current = latest.event_id;
try {
await MatrixApiEvent.SendReceipt(p.room, latest.event_id);
} catch (e) {
console.error("Failed to send read receipt!", e);
snackbar(`Failed to send read receipt! ${e}`);
}
};
return ( return (
<div style={{ display: "flex", flexDirection: "column", flex: 1 }}> <div
style={{ display: "flex", flexDirection: "column", flex: 1 }}
onClick={handleRoomClick}
>
<RoomMessagesList {...p} /> <RoomMessagesList {...p} />
<SendMessageForm {...p} /> <SendMessageForm {...p} />
</div> </div>