Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
415c62c
Implement StreamChatClient.SearchMessagesAsync
sierpinskid May 20, 2026
2eefbca
Add tests for search feature
sierpinskid May 20, 2026
4f47976
Fix test to not use both Query and MessageFilter (disallowed by API)
sierpinskid May 21, 2026
dde9038
Fix date serialization. The API was returning "Search failed with err…
sierpinskid May 22, 2026
5958cfa
Fix "TearDown : System.ArgumentException : 'async void' methods are n…
sierpinskid May 22, 2026
c95e348
Fix tests deadlock
sierpinskid May 22, 2026
af2a824
Make test more resilient to changes not being immediately available o…
sierpinskid May 22, 2026
6f6d61f
Apply API response to cache when soft delete is used so that the loca…
sierpinskid May 22, 2026
ed22ad0
Fix inconsistent datetime serialization expected by the API
sierpinskid May 27, 2026
60f2bef
rewrite test for clarity + extend timeout
sierpinskid May 27, 2026
cb38489
Fix flaky When_thread_reply_with_show_in_channel_received_expect_adde…
sierpinskid May 28, 2026
5cfa9f6
Don't serialize MessageFilterConditions if null or empty -> we add th…
sierpinskid May 28, 2026
610ef6f
Refactor WatchedChannels to only contain watched channels. Before sea…
sierpinskid May 28, 2026
08ce9cb
watch search results by default + add StreamMessage.IsWatched flag + …
sierpinskid May 29, 2026
1b15bd6
Revise comments + optimize watching search result channels
sierpinskid May 29, 2026
4daa0dd
Fix compiler error
sierpinskid May 29, 2026
c42e115
Fix thread object custom data
sierpinskid May 29, 2026
48b4d85
Add freeing space before the docker image is pulled. Unity image can …
sierpinskid May 29, 2026
1974563
Improve comments on the public interface
sierpinskid May 29, 2026
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
5 changes: 5 additions & 0 deletions .github/workflows/main.ci.cd.workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ jobs:
- name: Install Git
run: git config --global --add safe.directory /github/workspace

- name: Free Disk space
uses: jlumbroso/free-disk-space@v1.2.0
with:
dotnet: false

- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update
Expand Down
37 changes: 35 additions & 2 deletions Assets/Plugins/StreamChat/Core/IStreamChatClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,16 @@ public interface IStreamChatClient : IDisposable, IStreamChatClientEventsListene
IStreamLocalUserData LocalUserData { get; }

/// <summary>
/// Watched channels receive updates on all users activity like new messages, reactions, etc.
/// Use <see cref="GetOrCreateChannelWithIdAsync"/> and <see cref="QueryChannelsAsync"/> to watch channels
/// Channels you are currently watching. Watched channels receive realtime updates
/// (new messages, reactions, member changes, etc.).
///
/// <para>
/// Start watching with <see cref="GetOrCreateChannelWithIdAsync"/>,
/// <see cref="QueryChannelsAsync"/> or <see cref="IStreamChannel.WatchAsync"/>;
/// stop with <see cref="IStreamChannel.StopWatchingAsync"/>. Channels returned by other
/// methods may not be watched - check <see cref="IStreamChannel.IsWatched"/> on a specific
/// channel to know its state.
/// </para>
/// </summary>
IReadOnlyList<IStreamChannel> WatchedChannels { get; }

Expand Down Expand Up @@ -270,6 +278,31 @@ Task<IStreamThread> GetThreadAsync(string parentMessageId,
/// <param name="request">Query request</param>
Task<StreamQueryThreadsResponse> QueryThreadsAsync(StreamQueryThreadsRequest request);

/// <summary>
/// Search messages across the channels the local user can access. Returns the matching
/// <see cref="IStreamMessage"/> results together with their <see cref="IStreamChannel"/>.
/// By default the result channels are watched so the returned messages and channels keep
/// receiving realtime updates; set
/// <see cref="StreamSearchMessagesRequest.WatchResultChannels"/> to <c>false</c> for one-off
/// results that don't need to stay up to date.
///
/// <para>
/// The <paramref name="request"/> requires a channel-level filter (e.g.
/// <c>ChannelFilter.Members.In(localUser)</c>). Additional message-level filters can be
/// expressed with <c>MessageFilter.*</c> builders, and a free-text phrase can be supplied
/// via <see cref="StreamSearchMessagesRequest.Query"/>. See <see cref="StreamSearchMessagesRequest"/>
/// for pagination and sorting options.
/// </para>
/// </summary>
/// <param name="request">Search parameters - channel filter, message filter, query phrase,
/// sort, and pagination.</param>
/// <param name="cancellationToken">[Optional] Cancellation token for the request.</param>
/// <returns>The matching messages and channels plus pagination cursors.</returns>
/// <remarks>https://getstream.io/chat/docs/unity/search/?language=unity</remarks>
Task<StreamSearchMessagesResponse> SearchMessagesAsync(
StreamSearchMessagesRequest request,
CancellationToken cancellationToken = default(CancellationToken));

/// <summary>
/// Upsert users. Upsert means update this user or create if not found
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,8 @@ protected FieldFilterRule InternalAutocomplete(string value)

protected FieldFilterRule InternalContains(string value)
=> new FieldFilterRule(FieldName, QueryOperatorType.Contains, value);

protected FieldFilterRule InternalExists(bool exists)
=> new FieldFilterRule(FieldName, QueryOperatorType.Exists, exists);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using StreamChat.Libs.Utils;

namespace StreamChat.Core.QueryBuilders.Filters
{
Expand All @@ -24,7 +24,7 @@ public FieldFilterRule(string field, QueryOperatorType operatorType, string valu
OperatorType = operatorType;
Value = value;
}

public FieldFilterRule(string field, QueryOperatorType operatorType, int value)
{
Field = field;
Expand All @@ -36,14 +36,17 @@ public FieldFilterRule(string field, QueryOperatorType operatorType, DateTime va
{
Field = field;
OperatorType = operatorType;
Value = ToRfc3339String(value);
// Store the raw DateTime so callers can pick the wire format at serialization time
// (different Stream endpoints accept different RFC 3339 sub-forms - see StreamDateFormat).
Value = value;
}

public FieldFilterRule(string field, QueryOperatorType operatorType, DateTimeOffset value)
{
Field = field;
OperatorType = operatorType;
Value = ToRfc3339String(value);
// See note above about deferred date formatting.
Value = value;
}

public FieldFilterRule(string field, QueryOperatorType operatorType, IEnumerable<string> value)
Expand All @@ -52,37 +55,100 @@ public FieldFilterRule(string field, QueryOperatorType operatorType, IEnumerable
OperatorType = operatorType;
Value = value.ToArray();
}

public FieldFilterRule(string field, QueryOperatorType operatorType, IEnumerable<DateTime> value)
{
Field = field;
OperatorType = operatorType;
// See note above about deferred date formatting.
Value = value.ToArray();
}

public FieldFilterRule(string field, QueryOperatorType operatorType, IEnumerable<DateTimeOffset> value)
{
Field = field;
OperatorType = operatorType;
// See note above about deferred date formatting.
Value = value.ToArray();
}

/// <summary>
/// Returns the filter entry using the default endpoint-portable date form
/// (<see cref="StreamDateFormat.UtcOffset"/>). Callers targeting <c>POST /search</c>'s
/// <c>message_filter_conditions</c> must use the format-aware overload
/// (<see cref="GenerateFilterEntry(StreamDateFormat)"/>) with
/// <see cref="StreamDateFormat.Utc"/>.
/// </summary>
//StreamTodo: research how to reduce allocation here
public KeyValuePair<string, object> GenerateFilterEntry()
=> GenerateFilterEntry(StreamDateFormat.UtcOffset);

/// <summary>
/// Returns the filter entry, formatting any date values using <paramref name="dateFormat"/>.
/// Non-date values are passed through untouched.
/// </summary>
internal KeyValuePair<string, object> GenerateFilterEntry(StreamDateFormat dateFormat)
=> new KeyValuePair<string, object>
(
Field, new Dictionary<string, object>
{
{
OperatorType.ToOperatorKeyword(), Value
OperatorType.ToOperatorKeyword(), FormatValueForWire(Value, dateFormat)
}
}
);

private static string ToRfc3339String(DateTime dateTime)
=> dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo);

private static string ToRfc3339String(DateTimeOffset dateTimeOffset)
=> dateTimeOffset.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo);

private static object FormatValueForWire(object value, StreamDateFormat dateFormat)
{
if (value is DateTime dt)
{
return dt.ToStreamDateString(dateFormat);
}

if (value is DateTimeOffset dto)
{
return dto.ToStreamDateString(dateFormat);
}

if (value is DateTime[] dts)
{
return dts.Select(d => d.ToStreamDateString(dateFormat)).ToArray();
}

if (value is DateTimeOffset[] dtos)
{
return dtos.Select(d => d.ToStreamDateString(dateFormat)).ToArray();
}

return value;
}
}

/// <summary>
/// Internal helpers for serializing <see cref="IFieldFilterRule"/> instances to the wire
/// dictionary with an explicit <see cref="StreamDateFormat"/>.
///
/// <para>
/// The public <see cref="IFieldFilterRule.GenerateFilterEntry"/> contract is intentionally
/// parameterless to avoid breaking external implementations. SDK-internal call sites that
/// need the <see cref="StreamDateFormat.Utc"/> (Z) form - currently only
/// <c>POST /search</c>'s <c>message_filter_conditions</c> / <c>filter_conditions</c> - go
/// through this helper. Anything implementing <see cref="IFieldFilterRule"/> that isn't the
/// SDK's own <see cref="FieldFilterRule"/> transparently falls back to the parameterless
/// path (i.e. <see cref="StreamDateFormat.UtcOffset"/>).
/// </para>
/// </summary>
internal static class FieldFilterRuleExtensions
{
internal static KeyValuePair<string, object> GenerateFilterEntry(this IFieldFilterRule rule,
StreamDateFormat dateFormat)
{
if (rule is FieldFilterRule concrete)
{
return concrete.GenerateFilterEntry(dateFormat);
}

return rule.GenerateFilterEntry();
}
}
}
}

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Collections.Generic;

namespace StreamChat.Core.QueryBuilders.Filters.Messages
{
/// <summary>
/// Filter by the type of an attachment on the message (<c>image</c>, <c>video</c>,
/// <c>file</c>, <c>audio</c>, <c>giphy</c>, <c>location</c>, or any custom type).
/// </summary>
public sealed class MessageFieldAttachmentType : BaseFieldToFilter
{
public override string FieldName => "attachments.type";

public FieldFilterRule EqualsTo(string attachmentType) => InternalEqualsTo(attachmentType);

public FieldFilterRule Contains(string attachmentType) => InternalContains(attachmentType);

public FieldFilterRule In(IEnumerable<string> attachmentTypes) => InternalIn(attachmentTypes);

public FieldFilterRule In(params string[] attachmentTypes) => InternalIn(attachmentTypes);
}
}

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using StreamChat.Core.StatefulModels;

namespace StreamChat.Core.QueryBuilders.Filters.Messages
{
/// <summary>
/// Filter by message <see cref="IStreamMessage.CreatedAt"/> timestamp.
/// </summary>
public sealed class MessageFieldCreatedAt : BaseFieldToFilter
{
public override string FieldName => "created_at";

public FieldFilterRule EqualsTo(DateTime date) => InternalEqualsTo(date);
public FieldFilterRule EqualsTo(DateTimeOffset date) => InternalEqualsTo(date);

public FieldFilterRule GreaterThan(DateTime date) => InternalGreaterThan(date);
public FieldFilterRule GreaterThan(DateTimeOffset date) => InternalGreaterThan(date);

public FieldFilterRule GreaterThanOrEquals(DateTime date) => InternalGreaterThanOrEquals(date);
public FieldFilterRule GreaterThanOrEquals(DateTimeOffset date) => InternalGreaterThanOrEquals(date);

public FieldFilterRule LessThan(DateTime date) => InternalLessThan(date);
public FieldFilterRule LessThan(DateTimeOffset date) => InternalLessThan(date);

public FieldFilterRule LessThanOrEquals(DateTime date) => InternalLessThanOrEquals(date);
public FieldFilterRule LessThanOrEquals(DateTimeOffset date) => InternalLessThanOrEquals(date);
}
}

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using StreamChat.Core.State;

namespace StreamChat.Core.QueryBuilders.Filters.Messages
{
/// <summary>
/// Filter by an arbitrary custom message field (any top-level key the customer attached to the message).
/// </summary>
public sealed class MessageFieldCustom : BaseFieldToFilter
{
public override string FieldName { get; }

public MessageFieldCustom(string customFieldName)
{
StreamAsserts.AssertNotNullOrEmpty(customFieldName, nameof(customFieldName));
FieldName = customFieldName;
}

public FieldFilterRule EqualsTo(string value) => InternalEqualsTo(value);
public FieldFilterRule EqualsTo(bool value) => InternalEqualsTo(value);
public FieldFilterRule EqualsTo(int value) => InternalEqualsTo(value);
public FieldFilterRule EqualsTo(DateTime value) => InternalEqualsTo(value);
public FieldFilterRule EqualsTo(DateTimeOffset value) => InternalEqualsTo(value);

public FieldFilterRule In(IEnumerable<string> values) => InternalIn(values);
public FieldFilterRule In(params string[] values) => InternalIn(values);

public FieldFilterRule GreaterThan(int value) => InternalGreaterThan(value);
public FieldFilterRule GreaterThan(string value) => InternalGreaterThan(value);
public FieldFilterRule GreaterThan(DateTime value) => InternalGreaterThan(value);
public FieldFilterRule GreaterThan(DateTimeOffset value) => InternalGreaterThan(value);

public FieldFilterRule GreaterThanOrEquals(int value) => InternalGreaterThanOrEquals(value);
public FieldFilterRule GreaterThanOrEquals(string value) => InternalGreaterThanOrEquals(value);
public FieldFilterRule GreaterThanOrEquals(DateTime value) => InternalGreaterThanOrEquals(value);
public FieldFilterRule GreaterThanOrEquals(DateTimeOffset value) => InternalGreaterThanOrEquals(value);

public FieldFilterRule LessThan(int value) => InternalLessThan(value);
public FieldFilterRule LessThan(string value) => InternalLessThan(value);
public FieldFilterRule LessThan(DateTime value) => InternalLessThan(value);
public FieldFilterRule LessThan(DateTimeOffset value) => InternalLessThan(value);

public FieldFilterRule LessThanOrEquals(int value) => InternalLessThanOrEquals(value);
public FieldFilterRule LessThanOrEquals(string value) => InternalLessThanOrEquals(value);
public FieldFilterRule LessThanOrEquals(DateTime value) => InternalLessThanOrEquals(value);
public FieldFilterRule LessThanOrEquals(DateTimeOffset value) => InternalLessThanOrEquals(value);

public FieldFilterRule Contains(string value) => InternalContains(value);

public FieldFilterRule Exists(bool exists) => InternalExists(exists);
}
}

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

Loading
Loading