val request = QueryChannelsRequest(
filter = Filters.and(
Filters.eq("type", "messaging"),
Filters.`in`("members", listOf("thierry")),
),
offset = 0,
limit = 10,
querySort = QuerySortByField.descByName("lastMessageAt")
).apply {
watch = true
state = true
}
client.queryChannels(request).enqueue { result ->
if (result.isSuccess) {
val channels: List<Channel> = result.data()
} else {
// Handle result.error()
}
}
Querying Channels
If you’re building a similar application to Facebook Messenger or Intercom, you’ll want to show a list of Channels. The Chat API supports MongoDB style queries to make this easy to implement.
You can query channels based on built-in fields as well as any custom field you add to channels. You can find the complete list of supported operators in the query syntax section of the docs.
Multiple filters can be combined, but more complex filters can increase the latency of this API, particularly with custom fields. Reach out to support if you have any doubts about your query filter or logic.
As an example, let’s say that you want to query the last conversations I participated in sorted by last_message_at
.
Stream Chat does not run MongoDB on the backend, only a subset of the query options are available.
Here’s an example of how you can query the list of channels:
const filter = { type: 'messaging', members: { $in: ['thierry'] } };
const sort = [{ last_message_at: -1 }];
const channels = await chatClient.queryChannels(filter, sort, {
watch: true, // this is the default
state: true,
});
channels.map((channel) => {
console.log(channel.data.name, channel.cid)
})
}
final filter = Filter.in_('members',['thierry']);
final sort = [SortOption("last_message_at", direction: SortOption.DESC)];
final channels = await client.queryChannels(
filter: filter,
sort: sort,
watch: true,
state: true,
).last;
channels.forEach((Channel c) {
print("${c.extraData['name']} ${c.cid}");
});
$filter = ['members' => ['$in' => ['elon', 'jack', 'jessie'] ]];
$sort = ['last_message_at' => 1]; // sorting direction (1 or -1)
$options = ['limit' => 10];
$channels = $client->queryChannels($filter,$sort,$options);
let controller = chatClient.channelListController(
query: .init(
filter: .and([.equal(.type, to: .messaging), .containMembers(userIds: ["thierry"])]),
sort: [.init(key: .lastMessageAt, isAscending: false)],
pageSize: 10
)
)
controller.synchronize { error in
if let error = error {
// handle error
print(error)
} else {
// access channels
print(controller.channels)
// load more if needed
controller.loadNextChannels(limit: 10) { error in
// handle error / access channels
}
}
}
const FFilter Filter = FFilter::And({
FFilter::In(TEXT("members"), {TEXT("thierry")}),
FFilter::Equal(TEXT("type"), TEXT("messaging")),
});
const TArray<FChannelSortOption> SortOptions{{EChannelSortField::LastMessageAt, ESortDirection::Descending}};
Client->QueryChannels(
Filter,
SortOptions,
EChannelFlags::State,
{}, // Pagination options
[](const TArray<UChatChannel*> ReceivedChannels)
{
// Do something with ReceivedChannels
});
resp, err := c.QueryChannels(ctx, &QueryOption{
Filter: map[string]interface{}{
"members": map[string]interface{}{
"$in": []string{ "elon", "jack", "jessie" },
},
},
Sort: []*SortOption{{Field: "last_message_at", Direction: 1}}, // sorting direction (1 or -1)
Limit: 10,
})
client.query_channels(
{"members": {"$in": ["elon", "jack", "jessie"]}},
{"last_message_at": 1},
limit=10,
)
client.query_channels({
'members' => { '$in' => ['elon', 'jack', 'jessie'] } },
sort: { 'last_message_at' => 1 },
limit: 10
)
await channelClient.QueryChannelsAsync(QueryChannelsOptions.Default
.WithFilter(new Dictionary<string, object>
{
{ "members", new Dictionary<string, object> { { "$in", new[] {"elon", "jack", "jessie"} } } },
})
.WithSortBy(new SortParameter { Field = "last_message_at", Direction = SortDirection.Ascending })
.WithLimit(10));
var filters = new List<IFieldFilterRule>
{
// Return only channels where local user is a member
ChannelFilter.Members.In(Client.LocalUserData.UserId),
};
var channels = await Client.QueryChannelsAsync(filters);
// Android SDK
FilterObject filter = Filters.and(
Filters.eq("type", "messaging"),
Filters.in("members", Arrays.asList("thierry"))
);
int offset = 0;
int limit = 10;
QuerySortByField<Channel> sort = QuerySortByField.descByName("last_message_at");
int messageLimit = 0;
int memberLimit = 0;
QueryChannelsRequest request = new QueryChannelsRequest(filter, offset, limit, sort, messageLimit, memberLimit)
.withWatch()
.withState();
client.queryChannels(request).enqueue(result -> {
if (result.isSuccess()) {
List<Channel> channels = result.data();
} else {
// Handle result.error()
}
});
// Backend SDK
Channel.list()
.user(user)
.filterCondition("type", "messaging")
.filterConditions(FilterCondition.in("members", "thierry"))
.sort(Sort.builder().field("last_message_at").direction(Direction.DESC).build())
.watch(true)
.state(true)
.request()
Query Parameters
name | type | description | default | optional |
---|---|---|---|---|
filters | object | The query filters to use. You can query on any of the custom fields you’ve defined on the Channel. You can also filter other built-in channel fields, see next section for reference. | {} | |
sort | object or array of objects | The sorting used for the channels matching the filters. Sorting is based on field and direction, multiple sorting options can be provided. You can sort based on last_updated, last_message_at,updated_at,created_at,member_count, unread_count or has_unread (unread status). Direction can be ascending (1) or descending (-1) | [{last_updated: -1}] | |
options | object | Query options. See below. | {} |
Some languages does not guarantee field order in objects or maps. That’s why using array of single key objects is necessary
By default when query channels does not have any filter and it will match all channels on your application. While this might be OK during development, you most likely want to have at least some basic filtering.
At a minimum, the filter
should include members: { $in: [userID] }
.
The query channels endpoint will only return channels that the user can read, you should make sure that the query uses a filter that includes such logic. For example: messaging channels are readable only to their members, such requirement can be included in the query filter (see below).
Common filters by use-case
Messaging and Team
On messaging and team applications you normally have users added to channels as a member. A good starting point is to use this filter to show the channels the user is participating.
val filter = Filters.`in`("members", listOf("thierry"))
const filter = { members: { $in: ['thierry'] } };
final filter = Filter.and([
Filter.equal('type', 'messaging'),
Filter.in_('members', ['thierry']),
]);
$filter = ['members' => ['$in' => ['thierry'] ]];
let currentUserChannels = chatClient.channelListController(
query: .init(
filter: .containMembers(userIds: [chatClient.currentUserId!])
)
)
currentUserChannels.synchronize()
// or
let thierryChannels = chatClient.channelListController(
query: .init(
filter: .containMembers(userIds: ["thierry"])
)
)
thierryChannels.synchronize()
const FFilter Filter = FFilter::And({
FFilter::In(TEXT("members"), {TEXT("thierry")}),
FFilter::Equal(TEXT("type"), TEXT("messaging")),
});
filter := map[string]interface{}{
"members": map[string]interface{}{
"$in": []string{ "thierry" },
},
}
filter = {"members": {"$in": ["thierry"]}}
filter = {"members" => {"$in" => ["thierry"]}}
var filter = new Dictionary<string, object>
{
{ "members", new Dictionary<string, object> { { "$in", new[] {"thierry"} } } },
}
// Android SDK
FilterObject filter = Filters.in("members", Arrays.asList("thierry"));
// Backend SDK
Map<String, Object> filter = FilterCondition.in("members", "thierry")
var filters = new List<IFieldFilterRule>
{
// Return only channels where local user is a member
ChannelFilter.Members.In(Client.LocalUserData.UserId),
// You can define multiple filters that will all have to be satisfied
};
var channels = await Client.QueryChannelsAsync(filters);
Support
On a support chat, you probably want to attach additional information to channels such as the support agent handling the case and other information regarding the status of the support case (ie. open, pending, solved).
val filter = Filters.and(
Filters.eq("agent_id", user.id),
Filters.`in`("status", listOf("pending", "open", "new")),
)
const filter = { agent_id: `${user.Id}`, status: { $in: ['pending', 'open', 'new'] }};
final filter = Filter.and([
Filter.equal('agent_id', user.id),
Filter.in_('status', ['pending', 'open', 'new']),
]);
final filter = Filter.and([
Filter.equal('agent_id', '${user.id}'),
Filter.in_('status', ['pending', 'open', 'new']),
]);
$filter = ['agent_id' => 'agent-id', 'status' => ['$in' => ['pending', 'open', 'new'] ]];
let channels = chatClient.channelListController(
query: .init(
filter: .and([.equal("agent_id", to: chatClient.currentUserId!), .in("status", values: ["pending", "open", "new"])])
)
)
channels.synchronize()
const FFilter Filter = FFilter::And({
FFilter::Equal(TEXT("agent_id"), User->Id),
FFilter::In(TEXT("status"), {TEXT("pending"), TEXT("open"), TEXT("new")}),
});
filter := map[string]interface{}{
"agent_id": user.Id,
"status": map[string]interface{}{
"$in": []string{ "pending", "open", "new" },
},
}
filter = {
"agent_id": user["id"],
"status": {"$in": ["pending", "new", "open"]},
}
filter = {
"agent_id" => user["id"],
"status" => {"$in" => ["pending", "new", "open"]},
}
var filter = new Dictionary<string, object>
{
{ "agent_id", user.Id },
{ "status", new Dictionary<string, object> { { "$in", new[] {"pending", "new", "open"} } } },
}
// Android SDK
FilterObject filter = Filters.and(
Filters.eq("agent_id", user.getId()),
Filters.in("status", Arrays.asList("pending", "open", "new"))
);
// Backend SDK
Map<String, Object> filter = FilterCondition.and(
FilterCondition.eq("agent_id", userID),
FilterCondition.in("status", "pending", "open", "new")
);
var filters = new List<IFieldFilterRule>
{
// Return only channels where local user is a member
ChannelFilter.Custom("agent_id").EqualsTo(Client.LocalUserData.UserId),
ChannelFilter.Custom("status").In("pending", "open", "new")
};
var channels = await Client.QueryChannelsAsync(filters);
Channel Queryable Built-In Fields
To learn more about the supported query operators themselves, please see: Query Syntax Operators.
The following channel fields can be used to filter your query results.
Name | Type | Description | Supported Operators | EXAMPLE |
---|---|---|---|---|
frozen | boolean | channel frozen status | $eq | false |
type | string or list of string | the type of channel | $in, $eq | messaging |
id | string or list of string | the ID of the channel | $in, $eq | general |
cid | string or list of string | the full channel ID | $in, $eq | messaging:general |
members | string or list of string | the list of user IDs of the channel members | $in, $eq | marcelo or [thierry, marcelo] |
invite | string, must be one of these values: (pending, accepted, rejected) | the status of the invite | $eq | pending |
joined | boolean | whether current user is joined the channel or not (through invite or directly) | $eq | true |
muted | boolean | whether the current user has muted the channel | $eq | true |
member.user.name | string | the ‘name’ property of a user who is a member of the channel | $autocomplete, $eq | marc |
created_by_id | string | the id of the user that created the channel | $eq | marcelo |
hidden | boolean | whether the current user has hidden the channel | $eq | false |
last_message_at | string, must be formatted as an RFC3339 timestamp | the time of the last message in the channel | $eq, $gt, $lt, $gte, $lte, $exists | 2021-01-15T09:30:20.45Z |
member_count | integer | the number of members in the channel | $eq, $gt, $lt, $gte, $lte | 5 |
created_at | string, must be formatted as an RFC3339 timestamp | the time the channel was created | $eq, $gt, $lt, $gte, $lte $exists | 2021-01-15T09:30:20.45Z |
updated_at | string, must be formatted as an RFC3339 timestamp | the time the channel was updated | $eq, $gt, $lt, $gte, $lte | 2021-01-15T09:30:20.45Z |
team | string | the team associated with the channel | $eq | stream |
last_updated | string, must be formatted as an RFC3339 timestamp | the time of the last message in the channel. If the channel has no messages, then the time the channel was created | $eq, $gt, $lt, $gte, $lte | 2021-01-15T09:30:20.45Z |
Disabled | boolean | Whether the channel is disabled or not. | $eq | false |
has_unread | boolean | Retrieve channels where the user has an unread message. Only “true” is supported, up to 100 channels | true | true |
Querying by the channel Identifier should be done using the cid
field as far as possible to optimize API performance. As the full channel ID, cid
is indexed everywhere in Stream database where id
is not.
Query Options
name | type | description | default | optional |
---|---|---|---|---|
state | boolean | if true returns the Channel state | true | ✓ |
watch | boolean | if true listen to changes to this Channel in real time. | true | ✓ |
limit | integer | The number of channels to return (max is 30) | 10 | ✓ |
offset | integer | The offset (max is 1000) | 0 | ✓ |
message_limit | integer | How many messages should be included to each channel (Max 300) | 25 | ✓ |
member_limit | integer | How many members should be included for each channel (Max 100) | 100 | ✓ |
Query channels allows you to retrieve channels and start watching them at same time using the watch parameter set to true.
Response
The result of querying a channel is a list of ChannelState
objects which include all the required information to render them without any additional API call.
ChannelState Response
Field Name | Description |
---|---|
channel | The data for this channel |
messages | The most recent messages for this channel (see message_limit option) |
watcher_count | How many users are currently watching the channel |
read | The read state for all members of the channel |
members | The list of members, up to 100 ordered by the most recent added |
pinned_messages | Up to 10 most recent pinned messages |
[
{
"id":"f8IOxxbt",
"type":"messaging",
"cid":"messaging:f8IOxxbt",
"last_message_at":"2020-01-10T07:26:46.791232Z",
"created_at":"2020-01-10T07:25:37.63256Z",
"updated_at":"2020-01-10T07:25:37.632561Z",
"created_by":{
"id":"8ce4c6e11118ca103a0a7c633dcf60dd",
"role":"admin",
"created_at":"2019-08-27T17:33:14.442265Z",
"updated_at":"2020-01-10T07:25:36.402819Z",
"last_active":"2020-01-10T07:25:36.395796Z",
"banned":false,
"online":false,
"image":"https://ui-avatars.com/api/?name=mezie&size=192&background=000000&color=6E7FFE&length=1",
"name":"mezie",
"username":"mezie"
},
"frozen":false,
"config":{
"created_at":"2020-01-20T10:23:44.878185331Z",
"updated_at":"2020-01-20T10:23:44.878185458Z",
"name":"messaging",
"typing_events":true,
"read_events":true,
"connect_events":true,
"search":true,
"reactions":true,
"replies":true,
"mutes":true,
"uploads":true,
"url_enrichment":true,
"max_message_length":5000,
"automod":"disabled",
"automod_behavior":"flag",
"commands":[
{
"name":"giphy",
"description":"Post a random gif to the channel",
"args":"[text]",
"set":"fun_set"
}
]
},
"name":"Video Call"
}
]
Pagination
Query channel requests can be paginated similar to how you paginate on other calls. Here’s a short example:
// Get the first 10 channels
val filter = Filters.`in`("members", "thierry")
val offset = 0
val limit = 10
val request = QueryChannelsRequest(filter, offset, limit)
client.queryChannels(request).enqueue { result ->
if (result.isSuccess) {
val channels = result.data()
} else {
// Handle result.error()
}
}
// Get the second 10 channels
val nextRequest = QueryChannelsRequest(
filter = filter,
offset = 10, // Skips first 10
limit = limit
)
client.queryChannels(nextRequest).enqueue { result ->
if (result.isSuccess) {
val channels = result.data()
} else {
// Handle result.error()
}
}
// retrieve 20 channels with Thierry as a member and skip first 10
const filter = { members: { $in: ['thierry'] } };
const sort = { last_message_at: -1 };
const channels = await authClient.queryChannels(filter, sort, {
limit: 20, offset: 10
});
// retrieve 20 channels with Thierry as a member and skip first 10
final filter = Filter.in_('members', ['thierry']);
final sort = [SortOption("last_message_at", direction: SortOption.DESC)];
final response = await client.queryChannels(
filter: filter,
sort: sort,
paginationParams: PaginationParams(
limit: 20,
offset: 10,
),
);
response.first.forEach((ChannelState f) {
print("${f.channel.extraData['name']} ${f.channel.cid}");
});
// retrieve 20 channels with Thierry as a member and skip first 10
$filter = ['members' => ['$in' => ['thierry'] ]];
$sort = ['last_message_at' => -1]; // sorting direction (1 or -1)
$options = ['limit' => 20, 'offset' => 10];
$channels = $client->queryChannels($filter,$sort,$options);
let controller = chatClient.channelListController(
query: .init(
filter: .containMembers(userIds: ["thierry"]),
pageSize: 10
)
)
// Get the first 10 channels
controller.synchronize { error in
if let error = error {
// handle error
print(error)
} else {
// Access channels
print(controller.channels)
// Get the next 10 channels
controller.loadNextChannels { error in
// handle error / access channels
print(error ?? controller.channels)
}
}
}
const FFilter Filter = FFilter::In(TEXT("members"), {TEXT("thierry")});
const TArray<FChannelSortOption> SortOptions{
{EChannelSortField::LastMessageAt, ESortDirection::Descending},
};
const FPaginationOptions PaginationOptions{
20, // Limit
10, // Offset
};
Client->QueryChannels(
Filter,
SortOptions,
EChannelFlags::State,
PaginationOptions,
[](const TArray<UChatChannel*> ReceivedChannels)
{
// Started watching channels
});
// retrieve 20 channels with Thierry as a member and skip first 10
resp, err := c.QueryChannels(ctx, &QueryOption{
Filter: map[string]interface{}{
"members": map[string]interface{}{
"$in": []string{ "thierry" },
},
},
Sort: []*SortOption{{Field: "last_message_at", Direction: -1}}, // sorting direction (1 or -1)
Limit: 20,
Offset: 10,
})
# retrieve 20 channels with Thierry as a member and skip first 10
client.query_channels(
{"members": {"$in": ["thierry"]}},
{"last_message_at": -1},
limit=20,
offset=10,
)
# retrieve 20 channels with Thierry as a member and skip first 10
client.query_channels({
'members' => { '$in' => ['thierry'] } },
sort: { 'last_message_at' => -1 },
limit: 20,
offset: 10,
)
await channelClient.QueryChannelsAsync(QueryChannelsOptions.Default
.WithFilter(new Dictionary<string, object>
{
{ "members", new Dictionary<string, object> { { "$in", new[] {"thierry"} } } },
})
.WithSortBy(new SortParameter { Field = "last_message_at", Direction = SortDirection.Descending })
.WithLimit(20)
.WithOffset(10);
// Android SDK
// Get the first 10 channels
FilterObject filter = Filters.in("members", "thierry");
int offset = 0;
int limit = 10;
QuerySorter<Channel> sort = new QuerySortByField<>();
int messageLimit = 0;
int memberLimit = 0;
QueryChannelsRequest request = new QueryChannelsRequest(filter, offset, limit, sort, messageLimit, memberLimit);
client.queryChannels(request).enqueue(result -> {
if (result.isSuccess()) {
List<Channel> channels = result.data();
} else {
// Handle result.error()
}
});
// Get the second 10 channels
int nextOffset = 10; // Skips first 10
QueryChannelsRequest nextRequest = new QueryChannelsRequest(filter, nextOffset, limit, sort, messageLimit, memberLimit);
client.queryChannels(nextRequest).enqueue(result -> {
if (result.isSuccess()) {
List<Channel> channels = result.data();
} else {
// Handle result.error()
}
});
// Backend SDK
// Get first 10 channels
Map<String, Object> filter = FilterCondition.in("members", "thierry");
Integer offset = 0;
Integer limit = 10;
Integer messageLimit = 0;
Integer memberLimit = 0;
ChannelListRequest req = Channel.list()
.user(testUserRequestObject)
.filterConditions(filter)
.watch(true)
.state(true)
.messageLimit(0)
.memberLimit(0)
ChannelListResponse channels = req.limit(limit).offset(offset).request();
// Get the second 10 channels
Integer nextOffset = 10;
ChannelListResponse nextChannels = req.limit(limit).offset(nextOffset).request();
var filters = new List<IFieldFilterRule>
{
// Return only channels where local user is a member
ChannelFilter.Members.In(Client.LocalUserData.UserId),
// You can define multiple filters that will all have to be satisfied
};
// Pass limit and offset to control the page or results returned
// Limit - how many records per page
// Offset - how many records to skip
var channels = await Client.QueryChannelsAsync(filters, limit: 30, offset: 60);
It is important to note that your filter
should include, at the very least {members: {$in: [userID]}
or pagination could break.