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
2 changes: 1 addition & 1 deletion .cursor/rules/patterns/repository-pattern.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ void main() {
final result = await client.feed(FeedId(group: 'user', id: 'test'));

switch (result) {
case Success(value: final feed):
case Success(data: final feed):
expect(feed.id, equals('feed-1'));
case Failure(error: final error):
fail('Expected success, got error: $error');
Expand Down
2 changes: 1 addition & 1 deletion .cursor/rules/stream-feeds-sdk.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ class FeedStateNotifier extends StateNotifier<FeedState> {
final result = await repository.getOrCreateFeed(state.feedQuery);

switch (result) {
case Success(value: final feedData):
case Success(data: final feedData):
state = state.copyWith(activities: feedData.activities.items);
case Failure(error: final error):
state = state.copyWith(error: error.toString());
Expand Down
73 changes: 73 additions & 0 deletions docs/code_snippets/02_01_querying_users.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:stream_feeds/stream_feeds.dart';

late StreamFeedsClient client;

Future<void> queryUsers() async {
// Search users by name prefix
final userList = client.userList(
UsersQuery(
filter: Filter.autoComplete(UsersFilterField.name, 'Al'),
sort: [UsersSort.asc(UsersSortField.name)],
limit: 25,
),
);

final result = await userList.get();

switch (result) {
case Success(data: final users):
for (final user in users) {
print('${user.id}: ${user.name}');
}
case Failure(error: final error):
print('Failed to query users: $error');
}

// The loaded users are also kept in the observable state of the list
userList.stream.listen((state) => print('${state.users.length} users'));

// Dispose the list when you no longer need it
userList.dispose();
}

Future<void> queryUsersWithFilter() async {
// Query users by exact ID match
final userList = client.userList(
UsersQuery(
filter: Filter.in_(UsersFilterField.id, ['alice', 'bob', 'carol']),
),
);

final result = await userList.get();

switch (result) {
case Success(data: final users):
print('Found ${users.length} users');
case Failure(error: final error):
print('Failed to query users: $error');
}
}

Future<void> queryMoreUsers() async {
// Users are paginated with limit/offset instead of cursors
final userList = client.userList(
UsersQuery(
filter: Filter.contains(UsersFilterField.teams, 'support'),
sort: [UsersSort.desc(UsersSortField.lastActive)],
limit: 10,
),
);

await userList.get();

// Keep loading while more users are available
while (userList.state.canLoadMore) {
final result = await userList.queryMoreUsers();
if (result.isFailure) break;
}

for (final user in userList.state.users) {
final status = user.online ? 'online' : 'offline';
print('${user.name ?? user.id} is $status');
}
}
3 changes: 3 additions & 0 deletions packages/stream_feeds/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Upcoming

### New methods
- Added `userList` to `StreamFeedsClient` for querying users.

### New fields
- Added `isRead` and `isSeen` fields to `ActivityData` and `AggregatedActivityData` for notification-feed read/seen state.
- Added `friendReactionCount` and `friendReactions` fields to `ActivityData` to expose reactions from friends.
Expand Down
13 changes: 13 additions & 0 deletions packages/stream_feeds/lib/src/client/feeds_client_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import '../repository/devices_repository.dart';
import '../repository/feeds_repository.dart';
import '../repository/moderation_repository.dart';
import '../repository/polls_repository.dart';
import '../repository/users_repository.dart';
import '../state/activity.dart';
import '../state/activity_comment_list.dart';
import '../state/activity_list.dart';
Expand Down Expand Up @@ -58,6 +59,8 @@ import '../state/query/members_query.dart';
import '../state/query/moderation_configs_query.dart';
import '../state/query/poll_votes_query.dart';
import '../state/query/polls_query.dart';
import '../state/query/users_query.dart';
import '../state/user_list.dart';
import '../ws/feeds_ws_event.dart';
import 'endpoint_config.dart';

Expand Down Expand Up @@ -170,6 +173,7 @@ class StreamFeedsClientImpl implements StreamFeedsClient {
_moderationRepository = ModerationRepository(feedsApi);
_pollsRepository = PollsRepository(feedsApi);
_capabilitiesRepository = CapabilitiesRepository(feedsApi);
_usersRepository = UsersRepository(feedsApi);

moderation = ModerationClient(_moderationRepository);

Expand Down Expand Up @@ -208,6 +212,7 @@ class StreamFeedsClientImpl implements StreamFeedsClient {
late final ModerationRepository _moderationRepository;
late final PollsRepository _pollsRepository;
late final CapabilitiesRepository _capabilitiesRepository;
late final UsersRepository _usersRepository;

// TODO: Fill this with correct values
late final _systemEnvironmentManager = SystemEnvironmentManager(
Expand Down Expand Up @@ -473,6 +478,14 @@ class StreamFeedsClientImpl implements StreamFeedsClient {
);
}

@override
UserList userList(UsersQuery query) {
return UserList(
query: query,
usersRepository: _usersRepository,
);
}

@override
Future<Result<AppData>> getApp() => _appRepository.getApp();

Expand Down
4 changes: 2 additions & 2 deletions packages/stream_feeds/lib/src/client/moderation_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ import '../state.dart' show ModerationConfigsQuery;
///
/// // Ban a user
/// final banResult = await client.moderation.ban(
/// api.BanRequest(
/// BanRequest(
/// targetUserId: 'user-123',
/// reason: 'Violation of community guidelines',
/// ),
/// );
///
/// // Flag content for review
/// final flagResult = await client.moderation.flag(
/// api.FlagRequest(
/// FlagRequest(
/// targetId: 'activity-456',
/// reason: 'inappropriate content',
/// ),
Expand Down
75 changes: 51 additions & 24 deletions packages/stream_feeds/lib/src/feeds_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import 'state/query/members_query.dart';
import 'state/query/moderation_configs_query.dart';
import 'state/query/poll_votes_query.dart';
import 'state/query/polls_query.dart';
import 'state/query/users_query.dart';
import 'state/user_list.dart';

export 'client/moderation_client.dart';

Expand Down Expand Up @@ -394,7 +396,7 @@ abstract interface class StreamFeedsClient {
/// ),
/// ],
/// );
///```
/// ```
///
/// Returns a [Result] containing the list of upserted [ActivityData] or an error.
Future<Result<List<ActivityData>>> upsertActivities({
Expand All @@ -405,11 +407,13 @@ abstract interface class StreamFeedsClient {
///
/// Deletes the provided [ids] in a single batch operation.
///
///```dart
///await client.deleteActivities(
/// ids: ['123', '456'],
/// hardDelete: false,
///);
/// Example:
/// ```dart
/// await client.deleteActivities(
/// ids: ['123', '456'],
/// hardDelete: false,
/// );
/// ```
///
/// Returns a [Result] containing the list of deleted activity ids or an error.
Future<Result<api.DeleteActivitiesResponse>> deleteActivities({
Expand Down Expand Up @@ -656,7 +660,7 @@ abstract interface class StreamFeedsClient {
/// ```dart
/// final result = await client.getApp();
/// switch (result) {
/// case Success(value: final appData):
/// case Success(data: final appData):
/// print('App name: ${appData.name}');
/// print('File upload size limit: ${appData.fileUploadConfig.sizeLimit}');
/// case Failure(error: final error):
Expand All @@ -678,7 +682,7 @@ abstract interface class StreamFeedsClient {
/// final result = await client.queryDevices();
///
/// switch (result) {
/// case Success(value: final devicesResponse):
/// case Success(data: final devicesResponse):
/// print('Found ${devicesResponse.devices.length} devices');
/// case Failure(error: final error):
/// print('Failed to query devices: $error');
Expand Down Expand Up @@ -789,13 +793,13 @@ abstract interface class StreamFeedsClient {
/// Example:
/// ```dart
/// final result = await client.getOrCreateFollows(
/// api.FollowBatchRequest(
/// FollowBatchRequest(
/// follows: [
/// api.FollowRequest(
/// FollowRequest(
/// source: FeedId.user('john').rawValue,
/// target: FeedId.user('jane').rawValue,
/// ),
/// api.FollowRequest(
/// FollowRequest(
/// source: FeedId.user('john').rawValue,
/// target: FeedId.user('bob').rawValue,
/// ),
Expand All @@ -804,7 +808,7 @@ abstract interface class StreamFeedsClient {
/// );
///
/// switch (result) {
/// case Success(value: final batchFollowData):
/// case Success(data: final batchFollowData):
/// print('Created ${batchFollowData.created.length} new follows');
/// print('Total follows: ${batchFollowData.follows.length}');
/// case Failure(error: final error):
Expand All @@ -825,13 +829,13 @@ abstract interface class StreamFeedsClient {
/// Example:
/// ```dart
/// final result = await client.getOrCreateUnfollows(
/// api.UnfollowBatchRequest(
/// UnfollowBatchRequest(
/// follows: [
/// api.UnfollowPair(
/// UnfollowPair(
/// source: FeedId.user('john').rawValue,
/// target: FeedId.user('jane').rawValue,
/// ),
/// api.UnfollowPair(
/// UnfollowPair(
/// source: FeedId.user('john').rawValue,
/// target: FeedId.user('bob').rawValue,
/// ),
Expand All @@ -840,7 +844,7 @@ abstract interface class StreamFeedsClient {
/// );
///
/// switch (result) {
/// case Success(value: final unfollowedFollows):
/// case Success(data: final unfollowedFollows):
/// print('Unfollowed ${unfollowedFollows.length} feeds');
/// case Failure(error: final error):
/// print('Failed to unfollow feeds: $error');
Expand All @@ -863,7 +867,7 @@ abstract interface class StreamFeedsClient {
/// );
///
/// switch (result) {
/// case Success(value: final response):
/// case Success(data: final response):
/// print('Found ${response.collections.length} collections');
/// case Failure(error: final error):
/// print('Failed to read collections: $error');
Expand All @@ -883,9 +887,9 @@ abstract interface class StreamFeedsClient {
/// Example:
/// ```dart
/// final result = await client.createCollections(
/// request: api.CreateCollectionsRequest(
/// request: CreateCollectionsRequest(
/// collections: [
/// api.CollectionRequest(
/// CollectionRequest(
/// id: '123',
/// name: 'my_collection',
/// custom: const {'key': 'value'},
Expand All @@ -895,7 +899,7 @@ abstract interface class StreamFeedsClient {
/// );
///
/// switch (result) {
/// case Success(value: final response):
/// case Success(data: final response):
/// print('Created ${response.collections.length} collections');
/// case Failure(error: final error):
/// print('Failed to create collections: $error');
Expand All @@ -914,9 +918,9 @@ abstract interface class StreamFeedsClient {
/// Example:
/// ```dart
/// final result = await client.updateCollections(
/// request: api.UpdateCollectionsRequest(
/// request: UpdateCollectionsRequest(
/// collections: [
/// api.UpdateCollectionRequest(
/// UpdateCollectionRequest(
/// id: '123',
/// name: 'my_collection',
/// custom: const {'updated_key': 'updated_value'},
Expand All @@ -926,7 +930,7 @@ abstract interface class StreamFeedsClient {
/// );
///
/// switch (result) {
/// case Success(value: final response):
/// case Success(data: final response):
/// print('Updated ${response.collections.length} collections');
/// case Failure(error: final error):
/// print('Failed to update collections: $error');
Expand All @@ -949,7 +953,7 @@ abstract interface class StreamFeedsClient {
/// );
///
/// switch (result) {
/// case Success(value: final response):
/// case Success(data: final response):
/// print('Deleted collections successfully');
/// case Failure(error: final error):
/// print('Failed to delete collections: $error');
Expand All @@ -961,6 +965,29 @@ abstract interface class StreamFeedsClient {
required List<String> refs,
});

/// Creates a [UserList] object that represents a collection of users matching
/// the provided query.
///
/// Example:
/// ```dart
/// final userList = client.userList(UsersQuery(
/// filter: Filter.autoComplete(UsersFilterField.name, 'Al'),
/// sort: [UsersSort.asc(UsersSortField.name)],
/// limit: 25,
/// ));
///
/// // Fetch the first page of users
/// final result = await userList.get();
///
/// // Load more users if available
/// if (userList.state.canLoadMore) {
/// await userList.queryMoreUsers();
/// }
/// ```
///
/// Returns a [UserList] instance that can be used to interact with the collection of users.
UserList userList(UsersQuery query);

/// The moderation client for managing moderation-related operations.
///
/// Provides access to moderation configurations, content moderation, and moderation-related
Expand Down
25 changes: 25 additions & 0 deletions packages/stream_feeds/lib/src/models/user_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,28 @@ extension UserResponseMapper on UserResponse {
);
}
}

/// Extension function to convert a [FullUserResponse] to a [UserData] model.
extension FullUserResponseMapper on FullUserResponse {
/// Converts this API full user response to a domain [UserData] instance.
UserData toModel() {
return UserData(
banned: banned,
blockedUserIds: blockedUserIds,
createdAt: createdAt,
custom: custom,
deactivatedAt: deactivatedAt,
deletedAt: deletedAt,
id: id,
image: image,
language: language,
lastActive: lastActive,
name: name,
online: online,
revokeTokensIssuedBefore: revokeTokensIssuedBefore,
role: role,
teams: teams,
updatedAt: updatedAt,
);
}
}
Loading
Loading