Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions bindings/matrix-sdk-ffi/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ All notable changes to this project will be documented in this file.

### Features

- Add new high-level search helpers `RoomSearchIterator` and `GlobalSearchIterator` to perform
searches for messages in a room or across all rooms.
([6394](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
- Expose `event_type_raw` and `latest_content_raw()` on `EventTimelineItem`,
allowing clients to access the raw event type string and content JSON for
custom event handling without pattern-matching through nested enums.
Expand Down
2 changes: 1 addition & 1 deletion bindings/matrix-sdk-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ matrix-sdk = { workspace = true, features = [
matrix-sdk-base.workspace = true
matrix-sdk-common.workspace = true
matrix-sdk-ffi-macros.workspace = true
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
matrix-sdk-ui = { workspace = true, features = ["uniffi", "experimental-search"] }
mime = { version = "0.3.17", default-features = false }
ruma = { workspace = true, features = [
"html",
Expand Down
39 changes: 38 additions & 1 deletion bindings/matrix-sdk-ffi/src/client_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// Allow UniFFI to use methods marked as `#[deprecated]`.
#![allow(deprecated)]

use std::{num::NonZeroUsize, sync::Arc, time::Duration};
use std::{fs, num::NonZeroUsize, path::PathBuf, sync::Arc, time::Duration};

#[cfg(not(any(target_family = "wasm", target_os = "android")))]
use matrix_sdk::reqwest::Certificate;
Expand All @@ -24,6 +24,7 @@ use matrix_sdk::{
encryption::{BackupDownloadStrategy, EncryptionSettings},
event_cache::EventCacheError,
ruma::{ServerName, UserId},
search_index::SearchIndexStoreKind,
sliding_sync::{
Error as MatrixSlidingSyncError, VersionBuilder as MatrixSlidingSyncVersionBuilder,
VersionBuilderError,
Expand Down Expand Up @@ -137,6 +138,7 @@ pub struct ClientBuilder {
decryption_settings: DecryptionSettings,
enable_share_history_on_invite: bool,
request_config: Option<RequestConfig>,
search_index_store: Option<SearchIndexStoreKind>,

#[cfg(not(target_family = "wasm"))]
user_agent: Option<String>,
Expand Down Expand Up @@ -193,6 +195,7 @@ impl ClientBuilder {
enable_share_history_on_invite: false,
request_config: Default::default(),
threading_support: ThreadingSupport::Disabled,
search_index_store: None,
})
}

Expand Down Expand Up @@ -357,6 +360,26 @@ impl ClientBuilder {
Arc::new(builder)
}

pub fn with_search_index_store(
self: Arc<Self>,
path: String,
password: Option<String>,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);

// Note: creation of the path is deferred to later.
let path = PathBuf::from(path);

let kind = if let Some(password) = password {
SearchIndexStoreKind::EncryptedDirectory(path, password)
} else {
SearchIndexStoreKind::UnencryptedDirectory(path)
};

builder.search_index_store = Some(kind);
Arc::new(builder)
}

pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = MatrixClient::builder()
Expand Down Expand Up @@ -385,6 +408,20 @@ impl ClientBuilder {
None
};

if let Some(search_index_store) = builder.search_index_store {
// Create the search index directory.
match search_index_store {
SearchIndexStoreKind::UnencryptedDirectory(ref path)
| SearchIndexStoreKind::EncryptedDirectory(ref path, _) => {
fs::create_dir_all(path)?;
}
_ => {}
}

// Configure the inner builder to use the search index store.
inner_builder = inner_builder.search_index_store(search_index_store);
}

// Determine server either from URL, server name or user ID.
inner_builder = match builder.homeserver_cfg {
Some(HomeserverConfig::Url(url)) => inner_builder.homeserver_url(url),
Expand Down
11 changes: 10 additions & 1 deletion bindings/matrix-sdk-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
///
Expand Down
1 change: 1 addition & 0 deletions bindings/matrix-sdk-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ mod room_member;
mod room_preview;
mod ruma;
mod runtime;
mod search;
mod session_verification;
mod spaces;
mod store;
Expand Down
179 changes: 179 additions & 0 deletions bindings/matrix-sdk-ffi/src/search.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// 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::{
GlobalSearchIterator as SdkGlobalSearchIterator,
RoomSearchIterator as SdkRoomSearchIterator,
},
timeline::TimelineDetails,
};
use tokio::sync::Mutex;

use crate::{
client::Client,
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(SdkRoomSearchIterator::new(self.inner.clone(), query)),
}
}
}

#[derive(uniffi::Object)]
pub struct RoomSearchIterator {
sdk_room: matrix_sdk::room::Room,
inner: Mutex<SdkRoomSearchIterator>,
}

#[matrix_sdk_ffi_macros::export]
impl RoomSearchIterator {
/// 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(20).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> {
let sender = event.sender()?;

let event_id = event.event_id().unwrap().to_string();
let timestamp =
event.timestamp().unwrap_or_else(ruma::MilliSecondsSinceUnixEpoch::now).into();

let content = matrix_sdk_ui::timeline::TimelineItemContent::from_event(room, event).await?;
let profile = TimelineDetails::from_initial_value(
matrix_sdk_ui::timeline::Profile::load(room, &sender).await,
);

Some(Self {
event_id,
sender: sender.to_string(),
sender_profile: ProfileDetails::from(profile),
content: TimelineItemContent::from(content),
timestamp,
})
}
}

#[derive(Clone, uniffi::Enum)]
pub enum SearchRoomFilter {
/// All the joined rooms (= DMs + groups).
Rooms,
/// Only joined DM rooms.
Dms,
/// Only joined non-DM (group) rooms.
Groups,
}

#[matrix_sdk_ffi_macros::export]
impl Client {
pub async fn search(
&self,
query: String,
filter: SearchRoomFilter,
) -> Result<GlobalSearchIterator, ClientError> {
let sdk_client = (*self.inner).clone();
let mut search = SdkGlobalSearchIterator::builder(sdk_client.clone(), query);

match filter {
SearchRoomFilter::Rooms => {}
SearchRoomFilter::Dms => search = search.only_dm_rooms().await?,
SearchRoomFilter::Groups => search = search.only_groups().await?,
}

Ok(GlobalSearchIterator { sdk_client, inner: Mutex::new(search.build()) })
}
}

#[derive(uniffi::Record)]
pub struct BasicGlobalSearchResult {
room_id: String,
event_id: String,
}

#[derive(uniffi::Record)]
pub struct GlobalSearchResult {
room_id: String,
result: RoomSearchResult,
}

#[derive(uniffi::Object)]
pub struct GlobalSearchIterator {
sdk_client: matrix_sdk::Client,
inner: Mutex<SdkGlobalSearchIterator>,
}

#[matrix_sdk_ffi_macros::export]
impl GlobalSearchIterator {
/// 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,
num_results: u64,
) -> Result<Option<Vec<GlobalSearchResult>>, ClientError> {
let Some(events) = self.inner.lock().await.next_events(num_results as usize).await? else {
return Ok(None);
};

let mut results = Vec::with_capacity(events.len());

for (room_id, event) in events {
let Some(room) = self.sdk_client.get_room(&room_id) else {
continue;
};
if let Some(result) = RoomSearchResult::from(&room, event).await {
results.push(GlobalSearchResult { room_id: room_id.to_string(), result });
}
}

results.shrink_to_fit();

Ok(Some(results))
}
}
3 changes: 3 additions & 0 deletions crates/matrix-sdk-ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ All notable changes to this project will be documented in this file.

### Features

- Add new high-level search helpers in `matrix_sdk_ui::search` to perform searches for messages in
a room or across all rooms.
([6394](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
- Introduce a `ThreadListService` which offers reactive interfaces for rendering
and managing the list of threads from a particular room.
([6311](https://github.com/matrix-org/matrix-rust-sdk/pull/6311))
Expand Down
3 changes: 3 additions & 0 deletions crates/matrix-sdk-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ experimental-encrypted-state-events = [
"ruma/unstable-msc4362"
]

experimental-search = ["matrix-sdk/experimental-search", "matrix-sdk-search"]

[dependencies]
as_variant.workspace = true
async-rx = { workspace = true, features = ["alloc"] }
Expand All @@ -53,6 +55,7 @@ itertools.workspace = true
matrix-sdk = { workspace = true, features = ["e2e-encryption"] }
matrix-sdk-base.workspace = true
matrix-sdk-common.workspace = true
matrix-sdk-search = { workspace = true, optional = true }
mime.workspace = true
pin-project-lite.workspace = true
ruma = { workspace = true, features = ["html", "unstable-msc3381"] }
Expand Down
2 changes: 2 additions & 0 deletions crates/matrix-sdk-ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use ruma::html::HtmlSanitizerMode;
pub mod encryption_sync_service;
pub mod notification_client;
pub mod room_list_service;
#[cfg(feature = "experimental-search")]
pub mod search;
pub mod spaces;
pub mod sync_service;
pub mod timeline;
Expand Down
Loading
Loading