diff --git a/bindings/matrix-sdk-ffi/src/error.rs b/bindings/matrix-sdk-ffi/src/error.rs index 361b2ff72..203c156aa 100644 --- a/bindings/matrix-sdk-ffi/src/error.rs +++ b/bindings/matrix-sdk-ffi/src/error.rs @@ -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 for ClientError { } } +impl From for ClientError { + fn from(e: SearchError) -> Self { + Self::from_err(e) + } +} + /// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple /// String. /// diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index c55b165ae..7facb65f2 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -24,6 +24,7 @@ mod room_member; mod room_preview; mod ruma; mod runtime; +mod search; mod session_verification; mod spaces; mod store; diff --git a/bindings/matrix-sdk-ffi/src/search.rs b/bindings/matrix-sdk-ffi/src/search.rs new file mode 100644 index 000000000..2bf6baf8d --- /dev/null +++ b/bindings/matrix-sdk-ffi/src/search.rs @@ -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, +} + +#[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> { + 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>, 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 { + // TODO: i did make an helper for this, on some branch on my machine + let sender = event.raw().get_field::("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, + }) + } +} diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index 3ad6016ec..620ea6e74 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -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)> { + 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) }