Skip to content

Commit bf400a1

Browse files
renefloorxsahil03x
andauthored
feat(llc, core, persistence): channel pinning and archiving (#2204)
Co-authored-by: Sahil Kumar <sahil@getstream.io> Co-authored-by: Sahil Kumar <xdsahil@gmail.com>
1 parent d8f8f66 commit bf400a1

31 files changed

+1202
-319
lines changed

packages/stream_chat/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
✅ Added
44

5+
- Added support for Channel pinning and archiving.
56
- Added support for 'DraftMessage' feature, which allows users to save draft messages in channels.
67
Several methods have been added to the `Client` and `Channel` class to manage draft messages:
78
- `channel.createDraft`: Saves a draft message for a specific channel.

packages/stream_chat/lib/src/client/channel.dart

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,32 @@ class Channel {
236236
return state!.channelStateStream.map((cs) => cs.channel?.hidden == true);
237237
}
238238

239+
/// Channel pinned status.
240+
/// Status is specific to the current user.
241+
bool get isPinned {
242+
_checkInitialized();
243+
return membership?.pinnedAt != null;
244+
}
245+
246+
/// Channel pinned status as a stream.
247+
/// Status is specific to the current user.
248+
Stream<bool> get isPinnedStream {
249+
return membershipStream.map((m) => m?.pinnedAt != null);
250+
}
251+
252+
/// Channel archived status.
253+
/// Status is specific to the current user.
254+
bool get isArchived {
255+
_checkInitialized();
256+
return membership?.archivedAt != null;
257+
}
258+
259+
/// Channel archived status as a stream.
260+
/// Status is specific to the current user.
261+
Stream<bool> get isArchivedStream {
262+
return membershipStream.map((m) => m?.archivedAt != null);
263+
}
264+
239265
/// The last date at which the channel got truncated.
240266
DateTime? get truncatedAt {
241267
_checkInitialized();
@@ -1935,6 +1961,54 @@ class Channel {
19351961
return _client.showChannel(id!, type);
19361962
}
19371963

1964+
/// Pins the channel for the current user.
1965+
Future<Member> pin() async {
1966+
_checkInitialized();
1967+
1968+
final response = await _client.pinChannel(
1969+
channelId: id!,
1970+
channelType: type,
1971+
);
1972+
1973+
return response.channelMember;
1974+
}
1975+
1976+
/// Unpins the channel.
1977+
Future<Member?> unpin() async {
1978+
_checkInitialized();
1979+
1980+
final response = await _client.unpinChannel(
1981+
channelId: id!,
1982+
channelType: type,
1983+
);
1984+
1985+
return response.channelMember;
1986+
}
1987+
1988+
/// Archives the channel.
1989+
Future<Member?> archive() async {
1990+
_checkInitialized();
1991+
1992+
final response = await _client.archiveChannel(
1993+
channelId: id!,
1994+
channelType: type,
1995+
);
1996+
1997+
return response.channelMember;
1998+
}
1999+
2000+
/// Unarchives the channel for the current user.
2001+
Future<Member?> unarchive() async {
2002+
_checkInitialized();
2003+
2004+
final response = await _client.unarchiveChannel(
2005+
channelId: id!,
2006+
channelType: type,
2007+
);
2008+
2009+
return response.channelMember;
2010+
}
2011+
19382012
/// Stream of [Event] coming from websocket connection specific for the
19392013
/// channel. Pass an eventType as parameter in order to filter just a type
19402014
/// of event.

packages/stream_chat/lib/src/client/client.dart

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1877,6 +1877,83 @@ class StreamChatClient {
18771877
unset: unset,
18781878
);
18791879

1880+
/// Pins the channel for the current user.
1881+
Future<PartialUpdateMemberResponse> pinChannel({
1882+
required String channelId,
1883+
required String channelType,
1884+
}) {
1885+
return partialMemberUpdate(
1886+
channelId: channelId,
1887+
channelType: channelType,
1888+
set: const MemberUpdatePayload(pinned: true).toJson(),
1889+
);
1890+
}
1891+
1892+
/// Unpins the channel for the current user.
1893+
Future<PartialUpdateMemberResponse> unpinChannel({
1894+
required String channelId,
1895+
required String channelType,
1896+
}) {
1897+
return partialMemberUpdate(
1898+
channelId: channelId,
1899+
channelType: channelType,
1900+
unset: [MemberUpdateType.pinned.name],
1901+
);
1902+
}
1903+
1904+
/// Archives the channel for the current user.
1905+
Future<PartialUpdateMemberResponse> archiveChannel({
1906+
required String channelId,
1907+
required String channelType,
1908+
}) {
1909+
final currentUser = state.currentUser;
1910+
if (currentUser == null) {
1911+
throw const StreamChatError(
1912+
'User is not set on client, '
1913+
'use `connectUser` or `connectAnonymousUser` instead',
1914+
);
1915+
}
1916+
1917+
return partialMemberUpdate(
1918+
channelId: channelId,
1919+
channelType: channelType,
1920+
set: const MemberUpdatePayload(archived: true).toJson(),
1921+
);
1922+
}
1923+
1924+
/// Unarchives the channel for the current user.
1925+
Future<PartialUpdateMemberResponse> unarchiveChannel({
1926+
required String channelId,
1927+
required String channelType,
1928+
}) {
1929+
return partialMemberUpdate(
1930+
channelId: channelId,
1931+
channelType: channelType,
1932+
unset: [MemberUpdateType.archived.name],
1933+
);
1934+
}
1935+
1936+
/// Partially updates the member of the given channel.
1937+
///
1938+
/// Use [set] to define values to be set.
1939+
/// Use [unset] to define values to be unset.
1940+
/// When [userId] is not provided, the current user will be used.
1941+
Future<PartialUpdateMemberResponse> partialMemberUpdate({
1942+
required String channelId,
1943+
required String channelType,
1944+
Map<String, Object?>? set,
1945+
List<String>? unset,
1946+
}) {
1947+
assert(set != null || unset != null, 'Set or unset must be provided.');
1948+
1949+
return _chatApi.channel.updateMemberPartial(
1950+
channelId: channelId,
1951+
channelType: channelType,
1952+
set: set,
1953+
unset: unset,
1954+
);
1955+
}
1956+
18801957
/// Closes the [_ws] connection and resets the [state]
18811958
/// If [flushChatPersistence] is true the client deletes all offline
18821959
/// user's data.

packages/stream_chat/lib/src/core/api/channel_api.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,4 +375,24 @@ class ChannelApi {
375375
);
376376
return EmptyResponse.fromJson(response.data);
377377
}
378+
379+
/// Updates some of the member data
380+
Future<PartialUpdateMemberResponse> updateMemberPartial({
381+
required String channelId,
382+
required String channelType,
383+
Map<String, Object?>? set,
384+
List<String>? unset,
385+
}) async {
386+
final response = await _client.patch(
387+
// Note: user_id is not required for client side Apis as it can be fetched
388+
// directly from the user token but, for the api path is built with it
389+
// so we need to pass it as a placeholder.
390+
'${_getChannelUrl(channelId, channelType)}/member/{user_id}',
391+
data: {
392+
if (set != null) 'set': set,
393+
if (unset != null) 'unset': unset,
394+
},
395+
);
396+
return PartialUpdateMemberResponse.fromJson(response.data);
397+
}
378398
}

packages/stream_chat/lib/src/core/api/requests.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,31 @@ class ThreadOptions extends Equatable {
205205
@override
206206
List<Object?> get props => [watch, replyLimit, participantLimit, memberLimit];
207207
}
208+
209+
/// Payload for updating a member.
210+
@JsonSerializable(createFactory: false, includeIfNull: false)
211+
class MemberUpdatePayload {
212+
/// Creates a new MemberUpdatePayload instance.
213+
const MemberUpdatePayload({
214+
this.archived,
215+
this.pinned,
216+
});
217+
218+
/// Set to true to archive the channel for a user.
219+
final bool? archived;
220+
221+
/// Set to true to pin the channel for a user.
222+
final bool? pinned;
223+
224+
/// Serialize model to json
225+
Map<String, dynamic> toJson() => _$MemberUpdatePayloadToJson(this);
226+
}
227+
228+
/// Type of member update to unset.
229+
enum MemberUpdateType {
230+
/// Unset the archived flag.
231+
archived,
232+
233+
/// Unset the pinned flag.
234+
pinned,
235+
}

packages/stream_chat/lib/src/core/api/requests.g.dart

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/stream_chat/lib/src/core/api/responses.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,18 @@ class QueryMembersResponse extends _BaseResponse {
100100
_$QueryMembersResponseFromJson(json);
101101
}
102102

103+
/// Model response for update member API calls, such as
104+
/// [StreamChatClient.updateMemberPartial]
105+
@JsonSerializable(createToJson: false)
106+
class PartialUpdateMemberResponse extends _BaseResponse {
107+
/// The updated member state
108+
late Member channelMember;
109+
110+
/// Create a new instance from a json
111+
static PartialUpdateMemberResponse fromJson(Map<String, dynamic> json) =>
112+
_$PartialUpdateMemberResponseFromJson(json);
113+
}
114+
103115
/// Model response for [StreamChatClient.queryUsers] api call
104116
@JsonSerializable(createToJson: false)
105117
class QueryUsersResponse extends _BaseResponse {

packages/stream_chat/lib/src/core/api/responses.g.dart

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/stream_chat/lib/src/core/models/channel_state.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class ChannelState implements ComparableFieldProvider {
9797
ChannelSortKey.updatedAt => channel?.updatedAt,
9898
ChannelSortKey.lastMessageAt => channel?.lastMessageAt,
9999
ChannelSortKey.memberCount => channel?.memberCount,
100+
ChannelSortKey.pinnedAt => membership?.pinnedAt,
100101
// TODO: Support providing default value for hasUnread, unreadCount
101102
ChannelSortKey.hasUnread => null,
102103
ChannelSortKey.unreadCount => null,
@@ -134,4 +135,7 @@ extension type const ChannelSortKey(String key) implements String {
134135

135136
/// Sort channels by the count of unread messages.
136137
static const unreadCount = ChannelSortKey('unread_count');
138+
139+
/// Sort channels by the date they were pinned.
140+
static const pinnedAt = ChannelSortKey('pinned_at');
137141
}

packages/stream_chat/lib/src/core/models/member.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class Member extends Equatable implements ComparableFieldProvider {
2424
this.banned = false,
2525
this.banExpires,
2626
this.shadowBanned = false,
27+
this.pinnedAt,
28+
this.archivedAt,
2729
this.extraData = const {},
2830
}) : userId = userId ?? user?.id,
2931
createdAt = createdAt ?? DateTime.now(),
@@ -50,6 +52,8 @@ class Member extends Equatable implements ComparableFieldProvider {
5052
'shadow_banned',
5153
'created_at',
5254
'updated_at',
55+
'pinned_at',
56+
'archived_at'
5357
];
5458

5559
/// The interested user
@@ -82,6 +86,12 @@ class Member extends Equatable implements ComparableFieldProvider {
8286
/// True if the member is shadow banned from the channel
8387
final bool shadowBanned;
8488

89+
/// The date at which the channel was pinned by the member
90+
final DateTime? pinnedAt;
91+
92+
/// The date at which the channel was archived by the member
93+
final DateTime? archivedAt;
94+
8595
/// The date of creation
8696
final DateTime createdAt;
8797

@@ -103,6 +113,8 @@ class Member extends Equatable implements ComparableFieldProvider {
103113
bool? isModerator,
104114
DateTime? createdAt,
105115
DateTime? updatedAt,
116+
DateTime? pinnedAt,
117+
DateTime? archivedAt,
106118
bool? banned,
107119
DateTime? banExpires,
108120
bool? shadowBanned,
@@ -119,6 +131,8 @@ class Member extends Equatable implements ComparableFieldProvider {
119131
channelRole: channelRole ?? this.channelRole,
120132
userId: userId ?? this.userId,
121133
isModerator: isModerator ?? this.isModerator,
134+
pinnedAt: pinnedAt ?? this.pinnedAt,
135+
archivedAt: archivedAt ?? this.archivedAt,
122136
createdAt: createdAt ?? this.createdAt,
123137
updatedAt: updatedAt ?? this.updatedAt,
124138
extraData: extraData ?? this.extraData,
@@ -141,6 +155,8 @@ class Member extends Equatable implements ComparableFieldProvider {
141155
banned,
142156
banExpires,
143157
shadowBanned,
158+
pinnedAt,
159+
archivedAt,
144160
createdAt,
145161
updatedAt,
146162
extraData,

packages/stream_chat/lib/src/core/models/member.g.dart

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/stream_chat/lib/src/db/chat_persistence_client.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:collection/collection.dart';
12
import 'package:stream_chat/src/core/api/requests.dart';
23
import 'package:stream_chat/src/core/api/sort_order.dart';
34
import 'package:stream_chat/src/core/models/attachment_file.dart';
@@ -90,8 +91,15 @@ abstract class ChatPersistenceClient {
9091
getMessagesByCid(cid, messagePagination: messagePagination),
9192
getPinnedMessagesByCid(cid, messagePagination: pinnedMessagePagination),
9293
]);
94+
95+
final members = data[0] as List<Member>?;
96+
final membership = userId == null
97+
? null
98+
: members?.firstWhereOrNull((it) => it.userId == userId);
99+
93100
return ChannelState(
94-
members: data[0] as List<Member>?,
101+
members: members,
102+
membership: membership,
95103
read: data[1] as List<Read>?,
96104
channel: data[2] as ChannelModel?,
97105
messages: data[3] as List<Message>?,

0 commit comments

Comments
 (0)