Skip to content
Merged
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 packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

✅ Added

- Added support for Channel pinning and archiving.
- Added support for 'DraftMessage' feature, which allows users to save draft messages in channels.
Several methods have been added to the `Client` and `Channel` class to manage draft messages:
- `channel.createDraft`: Saves a draft message for a specific channel.
Expand Down
74 changes: 74 additions & 0 deletions packages/stream_chat/lib/src/client/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,32 @@
return state!.channelStateStream.map((cs) => cs.channel?.hidden == true);
}

/// Channel pinned status.
/// Status is specific to the current user.
bool get isPinned {
_checkInitialized();
return membership?.pinnedAt != null;
}

/// Channel pinned status as a stream.
/// Status is specific to the current user.
Stream<bool> get isPinnedStream {
return membershipStream.map((m) => m?.pinnedAt != null);

Check warning on line 249 in packages/stream_chat/lib/src/client/channel.dart

View check run for this annotation

Codecov / codecov/patch

packages/stream_chat/lib/src/client/channel.dart#L248-L249

Added lines #L248 - L249 were not covered by tests
}

/// Channel archived status.
/// Status is specific to the current user.
bool get isArchived {
_checkInitialized();
return membership?.archivedAt != null;
}

/// Channel archived status as a stream.
/// Status is specific to the current user.
Stream<bool> get isArchivedStream {
return membershipStream.map((m) => m?.archivedAt != null);

Check warning on line 262 in packages/stream_chat/lib/src/client/channel.dart

View check run for this annotation

Codecov / codecov/patch

packages/stream_chat/lib/src/client/channel.dart#L261-L262

Added lines #L261 - L262 were not covered by tests
}

/// The last date at which the channel got truncated.
DateTime? get truncatedAt {
_checkInitialized();
Expand Down Expand Up @@ -1935,6 +1961,54 @@
return _client.showChannel(id!, type);
}

/// Pins the channel for the current user.
Future<Member> pin() async {
_checkInitialized();

final response = await _client.pinChannel(
channelId: id!,
channelType: type,
);

return response.channelMember;
}

/// Unpins the channel.
Future<Member?> unpin() async {
_checkInitialized();

final response = await _client.unpinChannel(
channelId: id!,
channelType: type,
);

return response.channelMember;
}

/// Archives the channel.
Future<Member?> archive() async {
_checkInitialized();

final response = await _client.archiveChannel(
channelId: id!,
channelType: type,
);

return response.channelMember;
}

/// Unarchives the channel for the current user.
Future<Member?> unarchive() async {
_checkInitialized();

final response = await _client.unarchiveChannel(
channelId: id!,
channelType: type,
);

return response.channelMember;
}

/// Stream of [Event] coming from websocket connection specific for the
/// channel. Pass an eventType as parameter in order to filter just a type
/// of event.
Expand Down
77 changes: 77 additions & 0 deletions packages/stream_chat/lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1877,6 +1877,83 @@ class StreamChatClient {
unset: unset,
);

/// Pins the channel for the current user.
Future<PartialUpdateMemberResponse> pinChannel({
required String channelId,
required String channelType,
}) {
return partialMemberUpdate(
channelId: channelId,
channelType: channelType,
set: const MemberUpdatePayload(pinned: true).toJson(),
);
}

/// Unpins the channel for the current user.
Future<PartialUpdateMemberResponse> unpinChannel({
required String channelId,
required String channelType,
}) {
return partialMemberUpdate(
channelId: channelId,
channelType: channelType,
unset: [MemberUpdateType.pinned.name],
);
}

/// Archives the channel for the current user.
Future<PartialUpdateMemberResponse> archiveChannel({
required String channelId,
required String channelType,
}) {
final currentUser = state.currentUser;
if (currentUser == null) {
throw const StreamChatError(
'User is not set on client, '
'use `connectUser` or `connectAnonymousUser` instead',
);
}

return partialMemberUpdate(
channelId: channelId,
channelType: channelType,
set: const MemberUpdatePayload(archived: true).toJson(),
);
}

/// Unarchives the channel for the current user.
Future<PartialUpdateMemberResponse> unarchiveChannel({
required String channelId,
required String channelType,
}) {
return partialMemberUpdate(
channelId: channelId,
channelType: channelType,
unset: [MemberUpdateType.archived.name],
);
}

/// Partially updates the member of the given channel.
///
/// Use [set] to define values to be set.
/// Use [unset] to define values to be unset.
/// When [userId] is not provided, the current user will be used.
Future<PartialUpdateMemberResponse> partialMemberUpdate({
required String channelId,
required String channelType,
Map<String, Object?>? set,
List<String>? unset,
}) {
assert(set != null || unset != null, 'Set or unset must be provided.');

return _chatApi.channel.updateMemberPartial(
channelId: channelId,
channelType: channelType,
set: set,
unset: unset,
);
}

/// Closes the [_ws] connection and resets the [state]
/// If [flushChatPersistence] is true the client deletes all offline
/// user's data.
Expand Down
20 changes: 20 additions & 0 deletions packages/stream_chat/lib/src/core/api/channel_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -375,4 +375,24 @@ class ChannelApi {
);
return EmptyResponse.fromJson(response.data);
}

/// Updates some of the member data
Future<PartialUpdateMemberResponse> updateMemberPartial({
required String channelId,
required String channelType,
Map<String, Object?>? set,
List<String>? unset,
}) async {
final response = await _client.patch(
// Note: user_id is not required for client side Apis as it can be fetched
// directly from the user token but, for the api path is built with it
// so we need to pass it as a placeholder.
'${_getChannelUrl(channelId, channelType)}/member/{user_id}',
data: {
if (set != null) 'set': set,
if (unset != null) 'unset': unset,
},
);
return PartialUpdateMemberResponse.fromJson(response.data);
}
}
28 changes: 28 additions & 0 deletions packages/stream_chat/lib/src/core/api/requests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,31 @@ class ThreadOptions extends Equatable {
@override
List<Object?> get props => [watch, replyLimit, participantLimit, memberLimit];
}

/// Payload for updating a member.
@JsonSerializable(createFactory: false, includeIfNull: false)
class MemberUpdatePayload {
/// Creates a new MemberUpdatePayload instance.
const MemberUpdatePayload({
this.archived,
this.pinned,
});

/// Set to true to archive the channel for a user.
final bool? archived;

/// Set to true to pin the channel for a user.
final bool? pinned;

/// Serialize model to json
Map<String, dynamic> toJson() => _$MemberUpdatePayloadToJson(this);
}

/// Type of member update to unset.
enum MemberUpdateType {
/// Unset the archived flag.
archived,

/// Unset the pinned flag.
pinned,
}
7 changes: 7 additions & 0 deletions packages/stream_chat/lib/src/core/api/requests.g.dart

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

12 changes: 12 additions & 0 deletions packages/stream_chat/lib/src/core/api/responses.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ class QueryMembersResponse extends _BaseResponse {
_$QueryMembersResponseFromJson(json);
}

/// Model response for update member API calls, such as
/// [StreamChatClient.updateMemberPartial]
@JsonSerializable(createToJson: false)
class PartialUpdateMemberResponse extends _BaseResponse {
/// The updated member state
late Member channelMember;

/// Create a new instance from a json
static PartialUpdateMemberResponse fromJson(Map<String, dynamic> json) =>
_$PartialUpdateMemberResponseFromJson(json);
}

/// Model response for [StreamChatClient.queryUsers] api call
@JsonSerializable(createToJson: false)
class QueryUsersResponse extends _BaseResponse {
Expand Down
7 changes: 7 additions & 0 deletions packages/stream_chat/lib/src/core/api/responses.g.dart

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

4 changes: 4 additions & 0 deletions packages/stream_chat/lib/src/core/models/channel_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class ChannelState implements ComparableFieldProvider {
ChannelSortKey.updatedAt => channel?.updatedAt,
ChannelSortKey.lastMessageAt => channel?.lastMessageAt,
ChannelSortKey.memberCount => channel?.memberCount,
ChannelSortKey.pinnedAt => membership?.pinnedAt,
// TODO: Support providing default value for hasUnread, unreadCount
ChannelSortKey.hasUnread => null,
ChannelSortKey.unreadCount => null,
Expand Down Expand Up @@ -134,4 +135,7 @@ extension type const ChannelSortKey(String key) implements String {

/// Sort channels by the count of unread messages.
static const unreadCount = ChannelSortKey('unread_count');

/// Sort channels by the date they were pinned.
static const pinnedAt = ChannelSortKey('pinned_at');
}
16 changes: 16 additions & 0 deletions packages/stream_chat/lib/src/core/models/member.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
this.banned = false,
this.banExpires,
this.shadowBanned = false,
this.pinnedAt,
this.archivedAt,
this.extraData = const {},
}) : userId = userId ?? user?.id,
createdAt = createdAt ?? DateTime.now(),
Expand All @@ -50,6 +52,8 @@
'shadow_banned',
'created_at',
'updated_at',
'pinned_at',
'archived_at'
];

/// The interested user
Expand Down Expand Up @@ -82,6 +86,12 @@
/// True if the member is shadow banned from the channel
final bool shadowBanned;

/// The date at which the channel was pinned by the member
final DateTime? pinnedAt;

/// The date at which the channel was archived by the member
final DateTime? archivedAt;

/// The date of creation
final DateTime createdAt;

Expand All @@ -103,6 +113,8 @@
bool? isModerator,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? pinnedAt,
DateTime? archivedAt,
bool? banned,
DateTime? banExpires,
bool? shadowBanned,
Expand All @@ -119,6 +131,8 @@
channelRole: channelRole ?? this.channelRole,
userId: userId ?? this.userId,
isModerator: isModerator ?? this.isModerator,
pinnedAt: pinnedAt ?? this.pinnedAt,
archivedAt: archivedAt ?? this.archivedAt,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
extraData: extraData ?? this.extraData,
Expand All @@ -141,6 +155,8 @@
banned,
banExpires,
shadowBanned,
pinnedAt,
archivedAt,

Check warning on line 159 in packages/stream_chat/lib/src/core/models/member.dart

View check run for this annotation

Codecov / codecov/patch

packages/stream_chat/lib/src/core/models/member.dart#L158-L159

Added lines #L158 - L159 were not covered by tests
createdAt,
updatedAt,
extraData,
Expand Down
8 changes: 8 additions & 0 deletions packages/stream_chat/lib/src/core/models/member.g.dart

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

10 changes: 9 additions & 1 deletion packages/stream_chat/lib/src/db/chat_persistence_client.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:stream_chat/src/core/api/requests.dart';
import 'package:stream_chat/src/core/api/sort_order.dart';
import 'package:stream_chat/src/core/models/attachment_file.dart';
Expand Down Expand Up @@ -90,8 +91,15 @@ abstract class ChatPersistenceClient {
getMessagesByCid(cid, messagePagination: messagePagination),
getPinnedMessagesByCid(cid, messagePagination: pinnedMessagePagination),
]);

final members = data[0] as List<Member>?;
final membership = userId == null
? null
: members?.firstWhereOrNull((it) => it.userId == userId);

return ChannelState(
members: data[0] as List<Member>?,
members: members,
membership: membership,
read: data[1] as List<Read>?,
channel: data[2] as ChannelModel?,
messages: data[3] as List<Message>?,
Expand Down
Loading