ffi bindings for room search

This commit is contained in:
Benjamin Bouvier
2026-03-26 16:52:47 +01:00
parent 55e84a6b71
commit 2fdb16cd1d
4 changed files with 188 additions and 4 deletions

View File

@@ -24,7 +24,10 @@ use matrix_sdk::{
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
QueueWedgeError as SdkQueueWedgeError, StoreError,
};
use matrix_sdk_ui::{encryption_sync_service, notification_client, spaces, sync_service, timeline};
use matrix_sdk_ui::{
encryption_sync_service, notification_client, search::SearchError, spaces, sync_service,
timeline,
};
use ruma::{
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter, StandardErrorBody},
MilliSecondsSinceUnixEpoch,
@@ -239,6 +242,12 @@ impl From<spaces::Error> for ClientError {
}
}
impl From<SearchError> for ClientError {
fn from(e: SearchError) -> Self {
Self::from_err(e)
}
}
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
/// String.
///

View File

@@ -24,6 +24,7 @@ mod room_member;
mod room_preview;
mod ruma;
mod runtime;
mod search;
mod session_verification;
mod spaces;
mod store;

View File

@@ -0,0 +1,108 @@
// Copyright 2026 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use matrix_sdk::deserialized_responses::TimelineEvent;
use matrix_sdk_ui::search::RoomSearch;
use ruma::OwnedUserId;
use tokio::sync::Mutex;
use crate::{
error::ClientError,
room::Room,
timeline::{ProfileDetails, TimelineItemContent},
utils::Timestamp,
};
#[matrix_sdk_ffi_macros::export]
impl Room {
pub fn search(&self, query: String) -> RoomSearchIterator {
RoomSearchIterator {
sdk_room: self.inner.clone(),
inner: Mutex::new(RoomSearch::new(self.inner.clone(), query)),
}
}
}
#[derive(uniffi::Object)]
pub struct RoomSearchIterator {
sdk_room: matrix_sdk::room::Room,
inner: Mutex<RoomSearch>,
}
#[matrix_sdk_ffi_macros::export]
impl RoomSearchIterator {
/// Return a list of event ids for the next batch of search results, or
/// `None` if there are no more results.
pub async fn next(&self) -> Option<Vec<String>> {
match self.inner.lock().await.next().await {
Ok(Some(event_ids)) => Some(event_ids.into_iter().map(|id| id.to_string()).collect()),
Ok(None) => None,
Err(e) => {
eprintln!("Error during search: {e}");
None
}
}
}
/// Return a list of events for the next batch of search results, or `None`
/// if there are no more results.
pub async fn next_events(&self) -> Result<Option<Vec<RoomSearchResult>>, ClientError> {
let Some(events) = self.inner.lock().await.next_events().await? else {
return Ok(None);
};
let mut results = Vec::with_capacity(events.len());
for event in events {
if let Some(result) = RoomSearchResult::from(&self.sdk_room, event).await {
results.push(result);
}
}
results.shrink_to_fit();
Ok(Some(results))
}
}
#[derive(Clone, uniffi::Record)]
pub struct RoomSearchResult {
event_id: String,
sender: String,
sender_profile: ProfileDetails,
content: TimelineItemContent,
timestamp: Timestamp,
}
impl RoomSearchResult {
async fn from(room: &matrix_sdk::room::Room, event: TimelineEvent) -> Option<Self> {
// TODO: i did make an helper for this, on some branch on my machine
let sender = event.raw().get_field::<OwnedUserId>("sender").ok().flatten()?;
let event_id = event.event_id().unwrap().to_string();
let timestamp =
event.timestamp().unwrap_or_else(ruma::MilliSecondsSinceUnixEpoch::now).into();
let (content, profile) =
matrix_sdk_ui::timeline::TimelineItemContent::from_raw_event(room, event).await?;
Some(Self {
event_id,
sender: sender.to_string(),
sender_profile: ProfileDetails::from(profile),
content: TimelineItemContent::from(content),
timestamp,
})
}
}

View File

@@ -15,16 +15,18 @@
use std::sync::Arc;
use as_variant::as_variant;
use matrix_sdk::{Room, deserialized_responses::TimelineEvent};
use matrix_sdk_base::crypto::types::events::UtdCause;
use ruma::{
OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedUserId, UserId,
events::{
AnyStateEventContentChange, Mentions, MessageLikeEventType, StateEventContentChange,
StateEventType,
AnyMessageLikeEventContent, AnyStateEventContentChange, Mentions, MessageLikeEventType,
StateEventContentChange, StateEventType,
policy::rule::{
room::PolicyRuleRoomEventContent, server::PolicyRuleServerEventContent,
user::PolicyRuleUserEventContent,
},
relation::Replacement,
room::{
aliases::RoomAliasesEventContent,
avatar::RoomAvatarEventContent,
@@ -36,7 +38,7 @@ use ruma::{
history_visibility::RoomHistoryVisibilityEventContent,
join_rules::RoomJoinRulesEventContent,
member::{Change, RoomMemberEventContent},
message::MessageType,
message::{MessageType, RoomMessageEventContent},
name::RoomNameEventContent,
pinned_events::RoomPinnedEventsEventContent,
power_levels::RoomPowerLevelsEventContent,
@@ -74,6 +76,11 @@ pub use self::{
reply::{EmbeddedEvent, InReplyToDetails},
};
use super::ReactionsByKeyBySender;
use crate::timeline::{
Profile, TimelineDetails,
event_handler::{HandleAggregationKind, TimelineAction},
traits::RoomDataProvider as _,
};
/// The content of an [`EventTimelineItem`][super::EventTimelineItem].
#[allow(clippy::large_enum_variant)]
@@ -119,6 +126,65 @@ pub enum TimelineItemContent {
}
impl TimelineItemContent {
// TODO: commonize with the latest event implementation, likely?
pub async fn from_raw_event(
room: &Room,
timeline_event: TimelineEvent,
) -> Option<(Self, TimelineDetails<Profile>)> {
let raw_any_sync_timeline_event = timeline_event.into_raw();
let any_sync_timeline_event = raw_any_sync_timeline_event.deserialize().ok()?;
let sender = any_sync_timeline_event.sender().to_owned();
let profile = room
.profile_from_user_id(&sender)
.await
.map(TimelineDetails::Ready)
.unwrap_or(TimelineDetails::Unavailable);
match TimelineAction::from_event(
any_sync_timeline_event,
&raw_any_sync_timeline_event,
room,
None,
None,
None,
None,
)
.await
{
// Easy path: no aggregation, direct event.
Some(TimelineAction::AddItem { content }) => Some((content, profile)),
// Aggregated event.
//
// Only edits are supported for the moment.
Some(TimelineAction::HandleAggregation {
kind: HandleAggregationKind::Edit { replacement: Replacement { new_content, .. } },
..
}) => {
// Let's map the edit into a regular message.
match TimelineAction::from_content(
AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::new(
new_content.msgtype,
)),
// We don't care about the `InReplyToDetails` in the context of a
// `LatestEventValue`.
None,
// We don't care about the thread information in the context of a
// `LatestEventValue`.
None,
None,
) {
TimelineAction::AddItem { content } => Some((content, profile)),
_ => None,
}
}
_ => None,
}
}
pub fn as_msglike(&self) -> Option<&MsgLikeContent> {
as_variant!(self, TimelineItemContent::MsgLike)
}