diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index e3e31db6..52496a96 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -8,7 +8,8 @@ - `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key on `extraData`, so `user.image` and `user.extraData['image']` is the same. - `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`, so `user.name` and `user.extraData['name']` is the same. - +- `Channel` class now has extra `image` getter and setter. As well as an `updateImage` to do a partial update after a channel has been initialized. +- `Channel` class now has extra `name` getter and setter. As well as an `updateName` to do a partial update after a channel has been initialized. ## 2.1.1 🐞 Fixed diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index a05587ab..b22a57cc 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -15,20 +15,77 @@ import 'package:stream_chat/src/core/util/utils.dart'; import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/stream_chat.dart'; -/// This a the class that manages a specific channel. +/// Class that manages a specific channel. +/// +/// #### Channel name +/// +/// {@template name} +/// If an optional [name] argument is provided in the constructor then it +/// will be set on [extraData] with a key of 'name'. +/// +/// ```dart +/// final channel = Channel(client, type, id, name: 'Channel name'); +/// print(channel.name == channel.extraData['name']); // true +/// ``` +/// +/// Before the channel is initialized the name can be set directly: +/// ```dart +/// channel.name = 'New channel name'; +/// ``` +/// +/// To update the name after the channel has been initialized, call: +/// ```dart +/// channel.updateName('Updated channel name'); +/// ``` +/// +/// This will do a partial update to update the name. +/// {@endtemplate} +/// +/// #### Channel image +/// +/// {@template image} +/// If an optional [image] argument is provided in the constructor then it +/// will be set on [extraData] with a key of 'image'. +/// +/// ```dart +/// final channel = Channel(client, type, id, image: 'https://getstream.io/image.png'); +/// print(channel.image == channel.extraData['image']); // true +/// ``` +/// +/// Before the channel is initialized the image can be set directly: +/// ```dart +/// channel.image = 'https://getstream.io/new-image'; +/// ``` +/// +/// To update the image after the channel has been initialized, call: +/// ```dart +/// channel.updateImage('https://getstream.io/new-image'); +/// ``` +/// +/// This will do a partial update to update the image. +/// {@endtemplate} class Channel { - /// Create a channel client instance. + /// Class that manages a specific channel. + /// + /// Optional [extraData] and [image] properties can be provided. The [image] + /// is exposed to easily set a key of 'image' on [extraData]. Channel( this._client, this._type, this._id, { + String? name, + String? image, Map? extraData, }) : _cid = _id != null ? '$_type:$_id' : null, - _extraData = extraData ?? {} { - _client.logger.info('New Channel instance not initialized created'); + _extraData = { + ...?extraData, + if (name != null) 'name': name, + if (image != null) 'image': image, + } { + _client.logger.info('New Channel instance created, not yet initialized'); } - /// Create a channel client instance from a [ChannelState] object + /// Create a channel client instance from a [ChannelState] object. Channel.fromState(this._client, ChannelState channelState) : assert( channelState.channel != null, @@ -40,7 +97,7 @@ class Channel { _extraData = channelState.channel!.extraData { state = ChannelClientState(this, channelState); _initializedCompleter.complete(true); - _client.logger.info('New Channel instance initialized created'); + _client.logger.info('New Channel instance initialized'); } /// This client state @@ -53,155 +110,181 @@ class Channel { String? _cid; final Map _extraData; + /// Shortcut to set channel name. + /// + /// {@macro name} + set name(String? name) { + if (_initializedCompleter.isCompleted) { + throw StateError( + 'Once the channel is initialized you should use `channel.updateName` ' + 'to update the channel name', + ); + } + _extraData.addAll({'name': name}); + } + + /// Shortcut to set channel image. + /// + /// {@macro image} + set image(String? image) { + if (_initializedCompleter.isCompleted) { + throw StateError( + 'Once the channel is initialized you should use `channel.updateImage` ' + 'to update the channel image', + ); + } + _extraData.addAll({'image': image}); + } + set extraData(Map extraData) { if (_initializedCompleter.isCompleted) { throw StateError( - 'Once the channel is initialized you should use channel.update ' + 'Once the channel is initialized you should use `channel.update` ' 'to update channel data', ); } _extraData.addAll(extraData); } - /// Returns true if the channel is muted + /// Returns true if the channel is muted. bool get isMuted => _client.state.currentUser?.channelMutes .any((element) => element.channel.cid == cid) == true; - /// Returns true if the channel is muted as a stream + /// Returns true if the channel is muted, as a stream. Stream? get isMutedStream => _client.state.currentUserStream .map((event) => event!.channelMutes.any((element) => element.channel.cid == cid) == true) .distinct(); - /// True if the channel is a group + /// True if the channel is a group. bool get isGroup => memberCount != 2; - /// True if the channel is distinct + /// True if the channel is distinct. bool get isDistinct => id?.startsWith('!members') == true; - /// Channel configuration + /// Channel configuration. ChannelConfig? get config { _checkInitialized(); return state?._channelState.channel?.config; } - /// Channel configuration as a stream + /// Channel configuration as a stream. Stream? get configStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.config); } - /// Channel user creator + /// Channel user creator. User? get createdBy { _checkInitialized(); return state?._channelState.channel?.createdBy; } - /// Channel user creator as a stream + /// Channel user creator as a stream. Stream? get createdByStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.createdBy); } - /// Channel frozen status + /// Channel frozen status. bool? get frozen { _checkInitialized(); return state?._channelState.channel?.frozen; } - /// Channel frozen status as a stream + /// Channel frozen status as a stream. Stream? get frozenStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.frozen); } - /// Channel creation date + /// Channel creation date. DateTime? get createdAt { _checkInitialized(); return state?._channelState.channel?.createdAt; } - /// Channel creation date as a stream + /// Channel creation date as a stream. Stream? get createdAtStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.createdAt); } - /// Channel last message date + /// Channel last message date. DateTime? get lastMessageAt { _checkInitialized(); return state?._channelState.channel?.lastMessageAt; } - /// Channel last message date as a stream + /// Channel last message date as a stream. Stream? get lastMessageAtStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.lastMessageAt); } - /// Channel updated date + /// Channel updated date. DateTime? get updatedAt { _checkInitialized(); return state?._channelState.channel?.updatedAt; } - /// Channel updated date as a stream + /// Channel updated date as a stream. Stream? get updatedAtStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.updatedAt); } - /// Channel deletion date + /// Channel deletion date. DateTime? get deletedAt { _checkInitialized(); return state?._channelState.channel?.deletedAt; } - /// Channel deletion date as a stream + /// Channel deletion date as a stream. Stream? get deletedAtStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.deletedAt); } - /// Channel member count + /// Channel member count. int? get memberCount { _checkInitialized(); return state?._channelState.channel?.memberCount; } - /// Channel member count as a stream + /// Channel member count as a stream. Stream? get memberCountStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.memberCount); } - /// Channel id + /// Channel id. String? get id => state?._channelState.channel?.id ?? _id; - /// Channel type + /// Channel type. String get type => state?._channelState.channel?.type ?? _type; - /// Channel cid + /// Channel cid. String? get cid => state?._channelState.channel?.cid ?? _cid; - /// Channel team + /// Channel team. String? get team { _checkInitialized(); return state?._channelState.channel?.team; } - /// Channel extra data + /// Channel extra data. Map get extraData { var data = state?._channelState.channel?.extraData; if (data == null || data.isEmpty) { @@ -210,7 +293,7 @@ class Channel { return data; } - /// Channel extra data as a stream + /// Channel extra data as a stream. Stream> get extraDataStream { _checkInitialized(); return state!.channelStateStream.map( @@ -218,15 +301,46 @@ class Channel { ); } - /// The main Stream chat client + /// Shortcut to get channel name. + /// + /// {@macro name} + String? get name => extraData['name'] as String?; + + /// Channel [name] as a stream. + /// + /// The channel needs to be initialized. + /// + /// {@macro name} + Stream get nameStream { + _checkInitialized(); + return extraDataStream.map((it) => it['name'] as String?); + } + + /// Shortcut to get channel image. + /// + /// {@macro image} + String? get image => extraData['image'] as String?; + + /// Channel [image] as a stream. + /// + /// The channel needs to be initialized. + /// + /// {@macro image} + Stream get imageStream { + _checkInitialized(); + return extraDataStream.map((it) => it['image'] as String?); + } + + /// The main Stream chat client. StreamChatClient get client => _client; final StreamChatClient _client; final Completer _initializedCompleter = Completer(); - /// True if this is initialized + /// True if this is initialized. + /// /// Call [watch] to initialize the client or instantiate it using - /// [Channel.fromState] + /// [Channel.fromState]. Future get initialized => _initializedCompleter.future; final _cancelableAttachmentUploadRequest = {}; @@ -362,7 +476,9 @@ class Channel { } /// Send a [message] to this channel. - /// If [skipPush] is true the message will not send a push notification + /// + /// If [skipPush] is true the message will not send a push notification. + /// /// Waits for a [_messageAttachmentsUploadCompleter] to complete /// before actually sending the message. Future sendMessage( @@ -427,6 +543,7 @@ class Channel { } /// Updates the [message] in this channel. + /// /// Waits for a [_messageAttachmentsUploadCompleter] to complete /// before actually updating the message. Future updateMessage(Message message) async { @@ -489,8 +606,10 @@ class Channel { } /// Partially updates the [message] in this channel. - /// Use [set] to define values to be set - /// Use [unset] to define values to be unset + /// + /// Use [set] to define values to be set. + /// + /// Use [unset] to define values to be unset. Future partialUpdateMessage( Message message, { Map? set, @@ -590,7 +709,7 @@ class Channel { ); } - /// Unpins provided message + /// Unpins provided message. Future unpinMessage(Message message) => partialUpdateMessage( message, @@ -599,7 +718,7 @@ class Channel { }, ); - /// Send a file to this channel + /// Send a file to this channel. Future sendFile( AttachmentFile file, { ProgressCallback? onSendProgress, @@ -615,7 +734,7 @@ class Channel { ); } - /// Send an image to this channel + /// Send an image to this channel. Future sendImage( AttachmentFile file, { ProgressCallback? onSendProgress, @@ -631,7 +750,7 @@ class Channel { ); } - /// A message search. + /// Search for a message with the given options. Future search({ String? query, Filter? messageFilters, @@ -648,7 +767,7 @@ class Channel { ); } - /// Delete a file from this channel + /// Delete a file from this channel. Future deleteFile( String url, { CancelToken? cancelToken, @@ -662,7 +781,7 @@ class Channel { ); } - /// Delete an image from this channel + /// Delete an image from this channel. Future deleteImage( String url, { CancelToken? cancelToken, @@ -676,14 +795,15 @@ class Channel { ); } - /// Send an event on this channel + /// Send an event on this channel. Future sendEvent(Event event) { _checkInitialized(); return _client.sendEvent(id!, type, event); } - /// Send a reaction to this channel - /// Set [enforceUnique] to true to remove the existing user reaction + /// Send a reaction to this channel. + /// + /// Set [enforceUnique] to true to remove the existing user reaction. Future sendReaction( Message message, String type, { @@ -746,7 +866,7 @@ class Channel { } } - /// Delete a reaction from this channel + /// Delete a reaction from this channel. Future deleteReaction( Message message, Reaction reaction) async { final type = reaction.type; @@ -792,7 +912,49 @@ class Channel { } } - /// Edit the channel custom data + /// Update the channel's [name]. + /// + /// This is the same as calling [updatePartial] and providing a map with a + /// 'name' key: + /// + /// ```dart + /// channel.updatePartial( + /// set: {'name': 'Updated channel name'} + /// ); + /// ``` + /// + /// Instead do: + /// ```dart + /// channel.updateName('Updated channel name'); + /// ``` + Future updateName(String name) => + updatePartial(set: {'name': name}); + + /// Update the channel's [image]. + /// + /// This is the same as calling [updatePartial] and providing a map with an + /// 'image' key: + /// + /// ```dart + /// channel.updatePartial( + /// set: {'image': 'https://getstream.io/new-image'} + /// ); + /// ``` + /// + /// Instead do: + /// ```dart + /// channel.updateImage('https://getstream.io/new-image'); + /// ``` + Future updateImage(String image) => + updatePartial(set: {'image': image}); + + /// Update the channel custom data. This replaces all of the channel data + /// with the given [channelData]. + /// + /// If you instead want to do a partial update, use [updatePartial]. + /// + /// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart + /// for more information. Future update( Map channelData, [ Message? updateMessage, @@ -806,7 +968,18 @@ class Channel { ); } - /// Edit the channel custom data + /// A partial update can be used to set and unset specific custom data fields + /// when it is necessary to retain additional custom data fields on the + /// object. + /// + /// - [set] will add, or update existing attributes. + /// - [unset] will remove the attributes with the provided list of + /// values (keys). + /// + /// If you want to do a full update/replacement, use [update] instead. + /// + /// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart + /// for more information. Future updatePartial({ Map? set, List? unset, @@ -821,25 +994,25 @@ class Channel { return _client.deleteChannel(id!, type); } - /// Removes all messages from the channel + /// Removes all messages from the channel. Future truncate() async { _checkInitialized(); return _client.truncateChannel(id!, type); } - /// Accept invitation to the channel + /// Accept invitation to the channel. Future acceptInvite([Message? message]) async { _checkInitialized(); return _client.acceptChannelInvite(id!, type, message: message); } - /// Reject invitation to the channel + /// Reject invitation to the channel. Future rejectInvite([Message? message]) async { _checkInitialized(); return _client.rejectChannelInvite(id!, type, message: message); } - /// Add members to the channel + /// Add members to the channel. Future addMembers( List memberIds, [ Message? message, @@ -848,7 +1021,7 @@ class Channel { return _client.addChannelMembers(id!, type, memberIds, message: message); } - /// Invite members to the channel + /// Invite members to the channel. Future inviteMembers( List memberIds, [ Message? message, @@ -857,7 +1030,7 @@ class Channel { return _client.inviteChannelMembers(id!, type, memberIds, message: message); } - /// Remove members from the channel + /// Remove members from the channel. Future removeMembers( List memberIds, [ Message? message, @@ -866,7 +1039,7 @@ class Channel { return _client.removeChannelMembers(id!, type, memberIds, message: message); } - /// Send action for a specific message of this channel + /// Send action for a specific message of this channel. Future sendAction( Message message, Map formData, @@ -913,9 +1086,10 @@ class Channel { return res; } - /// Mark all messages as read + /// Mark all messages as read. + /// /// Optionally provide a [messageId] if you want to mark a - /// particular message as read + /// particular message as read. Future markRead({String? messageId}) async { _checkInitialized(); client.state.totalUnreadCount = @@ -924,7 +1098,7 @@ class Channel { return _client.markChannelRead(id!, type, messageId: messageId); } - /// Loads the initial channel state and watches for changes + /// Loads the initial channel state and watches for changes. Future watch() async { ChannelState response; @@ -955,15 +1129,16 @@ class Channel { } } - /// Stop watching the channel + /// Stop watching the channel. Future stopWatching() async { _checkInitialized(); return _client.stopChannelWatching(id!, type); } - /// List the message replies for a parent message + /// List the message replies for a parent message. + /// /// Set [preferOffline] to true to avoid the api call if the data is already - /// in the offline storage + /// in the offline storage. Future getReplies( String parentId, { PaginationParams? options, @@ -987,7 +1162,7 @@ class Channel { return repliesResponse; } - /// List the reactions for a message in the channel + /// List the reactions for a message in the channel. Future getReactions( String messageId, { PaginationParams? pagination, @@ -997,7 +1172,7 @@ class Channel { pagination: pagination, ); - /// Retrieves a list of messages by ID + /// Retrieves a list of messages by given [messageIDs]. Future getMessagesById( List messageIDs, ) async { @@ -1008,7 +1183,7 @@ class Channel { return res; } - /// Retrieves a list of messages by ID + /// Translate a message by given [messageId] and [language]. Future translateMessage( String messageId, String language, @@ -1018,12 +1193,13 @@ class Channel { language, ); - /// Creates a new channel + /// Creates a new channel. Future create() async => query(state: false); - /// Query the API, get messages, members or other channel fields - /// Set [preferOffline] to true to avoid the api call if the data is already - /// in the offline storage + /// Query the API, get messages, members or other channel fields. + /// + /// Set [preferOffline] to true to avoid the API call if the data is already + /// in the offline storage. Future query({ bool state = true, bool watch = false, @@ -1077,7 +1253,7 @@ class Channel { } } - /// Query channel members + /// Query channel members. Future queryMembers({ Filter? filter, List? sort, @@ -1092,19 +1268,19 @@ class Channel { pagination: pagination, ); - /// Mutes the channel + /// Mutes the channel. Future mute({Duration? expiration}) { _checkInitialized(); return _client.muteChannel(cid!, expiration: expiration); } - /// Unmutes the channel + /// Unmute the channel. Future unmute() { _checkInitialized(); return _client.unmuteChannel(cid!); } - /// Bans a user from the channel + /// Bans the user with given [userID] from the channel. Future banUser( String userID, Map options, @@ -1118,7 +1294,7 @@ class Channel { return _client.banUser(userID, opts); } - /// Remove the ban for a user in the channel + /// Remove the ban for the user with given [userID] in the channel. Future unbanUser(String userID) async { _checkInitialized(); return _client.unbanUser(userID, { @@ -1127,7 +1303,7 @@ class Channel { }); } - /// Shadow bans a user from the channel + /// Shadow bans the user with the given [userID] from the channel. Future shadowBan( String userID, Map options, @@ -1141,7 +1317,7 @@ class Channel { return _client.shadowBan(userID, opts); } - /// Remove the shadow ban for a user in the channel + /// Remove the shadow ban for the user with the given [userID] in the channel. Future removeShadowBan(String userID) async { _checkInitialized(); return _client.removeShadowBan(userID, { @@ -1151,8 +1327,10 @@ class Channel { } /// Hides the channel from [StreamChatClient.queryChannels] for the user - /// until a message is added If [clearHistory] is set to true - all messages - /// will be removed for the user + /// until a message is added. + /// + /// If [clearHistory] is set to true - all messages + /// will be removed for the user. Future hide({bool clearHistory = false}) async { _checkInitialized(); final response = await _client.hideChannel( @@ -1170,7 +1348,7 @@ class Channel { return response; } - /// Removes the hidden status for the channel + /// Removes the hidden status for the channel. Future show() async { _checkInitialized(); return _client.showChannel(id!, type); @@ -1178,7 +1356,7 @@ class Channel { /// 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 + /// of event. Stream on([ String? eventType, String? eventType2, @@ -1216,7 +1394,7 @@ class Channel { } } - /// Sets last typing to null and sends the typing.stop event + /// Sets last typing to null and sends the typing.stop event. Future stopTyping([String? parentId]) async { if (config?.typingEvents == false) { return; @@ -1230,7 +1408,7 @@ class Channel { )); } - /// Call this method to dispose the channel client + /// Call this method to dispose the channel client. void dispose() { state?.dispose(); } @@ -1244,9 +1422,9 @@ class Channel { } } -/// The class that handles the state of the channel listening to the events +/// The class that handles the state of the channel listening to the events. class ChannelClientState { - /// Creates a new instance listening to events and updating the state + /// Creates a new instance listening to events and updating the state. ChannelClientState( this._channel, ChannelState channelState, @@ -1393,23 +1571,25 @@ class ChannelClientState { } /// Flag which indicates if [ChannelClientState] contain latest/recent messages or not. + /// /// This flag should be managed by UI sdks. - /// When false, any new message (received by WebSocket event - /// - [EventType.messageNew]) will not be pushed on to message list. + /// + /// When false, any new message received by WebSocket event + /// [EventType.messageNew] will not be pushed on to message list. bool get isUpToDate => _isUpToDateController.value; set isUpToDate(bool isUpToDate) => _isUpToDateController.add(isUpToDate); - /// [isUpToDate] flag count as a stream + /// [isUpToDate] flag count as a stream. Stream get isUpToDateStream => _isUpToDateController.stream; final BehaviorSubject _isUpToDateController = BehaviorSubject.seeded(true); - /// The retry queue associated to this channel + /// The retry queue associated to this channel. late final RetryQueue _retryQueue; - /// Retry failed message + /// Retry failed message. Future retryFailedMessages() async { final failedMessages = [...messages, ...threads.values.expand((v) => v)] @@ -1502,7 +1682,7 @@ class ChannelClientState { })); } - /// Add a message to this channel + /// Add a message to this channel. void addMessage(Message message) { if (message.parentId == null || message.showInChannel == true) { final newMessages = List.from(_channelState.messages); @@ -1567,36 +1747,36 @@ class ChannelClientState { ); } - /// Channel message list + /// Channel message list. List get messages => _channelState.messages; - /// Channel message list as a stream + /// Channel message list as a stream. Stream?> get messagesStream => channelStateStream .map((cs) => cs.messages) .distinct(const ListEquality().equals); - /// Channel pinned message list + /// Channel pinned message list. List? get pinnedMessages => _channelState.pinnedMessages.toList(); - /// Channel pinned message list as a stream + /// Channel pinned message list as a stream. Stream?> get pinnedMessagesStream => channelStateStream.map((cs) => cs.pinnedMessages.toList()); - /// Get channel last message + /// Get channel last message. Message? get lastMessage => _channelState.messages.isNotEmpty == true ? _channelState.messages.last : null; - /// Get channel last message + /// Get channel last message. Stream get lastMessageStream => messagesStream .map((event) => event?.isNotEmpty == true ? event!.last : null); - /// Channel members list + /// Channel members list. List get members => _channelState.members .map((e) => e.copyWith(user: _channel.client.state.users[e.user!.id])) .toList(); - /// Channel members list as a stream + /// Channel members list as a stream. Stream> get membersStream => CombineLatestStream.combine2< List?, Map, List>( channelStateStream.map((cs) => cs.members), @@ -1605,19 +1785,19 @@ class ChannelClientState { members!.map((e) => e!.copyWith(user: users[e.user!.id])).toList(), ).distinct(const ListEquality().equals); - /// Channel watcher count + /// Channel watcher count. int? get watcherCount => _channelState.watcherCount; - /// Channel watcher count as a stream + /// Channel watcher count as a stream. Stream get watcherCountStream => channelStateStream.map((cs) => cs.watcherCount); - /// Channel watchers list + /// Channel watchers list. List get watchers => _channelState.watchers .map((e) => _channel.client.state.users[e.id] ?? e) .toList(); - /// Channel watchers list as a stream + /// Channel watchers list as a stream. Stream> get watchersStream => CombineLatestStream.combine2< List?, Map, List>( channelStateStream.map((cs) => cs.watchers), @@ -1625,20 +1805,20 @@ class ChannelClientState { (watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(), ); - /// Channel read list + /// Channel read list. List? get read => _channelState.read; - /// Channel read list as a stream + /// Channel read list as a stream. Stream?> get readStream => channelStateStream.map((cs) => cs.read); final BehaviorSubject _unreadCountController = BehaviorSubject.seeded(0); set unreadCount(int value) => _unreadCountController.add(value); - /// Unread count getter as a stream + /// Unread count getter as a stream. Stream get unreadCountStream => _unreadCountController.stream.distinct(); - /// Unread count getter + /// Unread count getter. int get unreadCount => _unreadCountController.value; bool _countMessageAsUnread(Message message) { @@ -1654,7 +1834,7 @@ class ChannelClientState { !userIsMuted; } - /// Update threads with updated information about messages + /// Update threads with updated information about messages. void updateThreadInfo(String parentId, List messages) { final newThreads = Map>.from(threads); @@ -1676,7 +1856,7 @@ class ChannelClientState { _threads = newThreads; } - /// Delete all channel messages + /// Delete all channel messages. void truncate() { _channelState = _channelState.copyWith( messages: [], @@ -1685,7 +1865,7 @@ class ChannelClientState { final List _updatedMessagesIds = []; - /// Update channelState with updated information + /// Update channelState with updated information. void updateChannelState(ChannelState updatedState) { final newMessages = [ ...updatedState.messages, @@ -1737,13 +1917,13 @@ class ChannelClientState { int _sortByCreatedAt(Message a, Message b) => a.createdAt.compareTo(b.createdAt); - /// The channel state related to this client + /// The channel state related to this client. ChannelState get _channelState => _channelStateController.value; - /// The channel state related to this client as a stream + /// The channel state related to this client as a stream. Stream get channelStateStream => _channelStateController.stream; - /// The channel state related to this client + /// The channel state related to this client. ChannelState get channelState => _channelStateController.value; late BehaviorSubject _channelStateController; @@ -1754,11 +1934,11 @@ class ChannelClientState { _debouncedUpdatePersistenceChannelState.call([v]); } - /// The channel threads related to this channel + /// The channel threads related to this channel. Map> get threads => _threadsController.value.map((key, value) => MapEntry(key, value)); - /// The channel threads related to this channel as a stream + /// The channel threads related to this channel as a stream. Stream>> get threadsStream => _threadsController.stream; final BehaviorSubject>> _threadsController = @@ -1772,10 +1952,10 @@ class ChannelClientState { _threadsController.add(v); } - /// Channel related typing users last value + /// Channel related typing users last value. Map get typingEvents => _typingEventsController.value; - /// Channel related typing users stream + /// Channel related typing users stream. Stream> get typingEventsStream => _typingEventsController.stream; @@ -1903,7 +2083,7 @@ class ChannelClientState { }); } - /// Call this method to dispose this object + /// Call this method to dispose this object. void dispose() { _debouncedUpdatePersistenceChannelState.cancel(); _unreadCountController.close(); diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 104b23fc..74abcb39 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -767,7 +767,9 @@ class StreamChatClient { cancelToken: cancelToken, ); - /// Replaces the [channelId] of type [ChannelType] data with [data] + /// Replaces the [channelId] of type [ChannelType] data with [data]. + /// + /// Use [updateChannelPartial] for a partial update. Future updateChannel( String channelId, String channelType, @@ -781,7 +783,10 @@ class StreamChatClient { message: message, ); - /// Updates the [channelId] of type [ChannelType] data with [data] + /// Partial update for the [channelId] of type [ChannelType]. Sets the + /// data provided in [set], and removes the attributes given in [unset]. + /// + /// Use [updateChannel] for a full update. Future updateChannelPartial( String channelId, String channelType, { diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 5be55491..7c5386c6 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -72,6 +72,40 @@ void main() { expect(channel.extraData.containsKey('name'), isTrue); expect(channel.extraData['name'], 'test-channel-name'); }); + + test('should be able to get and set `image`', () { + expect(channel.extraData.isEmpty, isTrue); + + const imageUrl = 'https://getstream.io/some-image'; + channel.image = imageUrl; + + expect(channel.image, imageUrl); + expect(channel.extraData['image'], imageUrl); + + const newImage = 'https://getstream.io/new-image'; + final newChannelInstance = + Channel(client, channelType, channelId, image: newImage); + + expect(newChannelInstance.image, newImage); + expect(newChannelInstance.extraData['image'], newImage); + }); + + test('should be able to get and set `name`', () { + expect(channel.extraData.isEmpty, isTrue); + + const name = 'Channel name'; + channel.name = name; + + expect(channel.name, name); + expect(channel.extraData['name'], name); + + const newName = 'New channel name'; + final newChannelInstance = + Channel(client, channelType, channelId, name: newName); + + expect(newChannelInstance.name, newName); + expect(newChannelInstance.extraData['name'], newName); + }); }); // TODO : test all persistence related logic in this group @@ -192,6 +226,22 @@ void main() { } }); + test('should throw if trying to set `image`', () { + try { + channel.image = 'https://stream.io/some-image'; + } catch (e) { + expect(e, isA()); + } + }); + + test('should throw if trying to set `name`', () { + try { + channel.name = 'New name'; + } catch (e) { + expect(e, isA()); + } + }); + group('`.sendMessage`', () { test('should work fine', () async { final message = Message(id: 'test-message-id'); @@ -1192,6 +1242,62 @@ void main() { message: any(named: 'message'))).called(1); }); + test('`.updateImage`', () async { + const image = 'https://getstream.io/new-image'; + + final channelModel = ChannelModel( + cid: channelCid, + extraData: {'image': image}, + ); + + when(() => client.updateChannelPartial( + channelId, + channelType, + set: {'image': image}, + )).thenAnswer( + (_) async => PartialUpdateChannelResponse()..channel = channelModel, + ); + + final res = await channel.updateImage(image); + + expect(res, isNotNull); + expect(res.channel.extraData['image'], image); + + verify(() => client.updateChannelPartial( + channelId, + channelType, + set: {'image': image}, + )).called(1); + }); + + test('`.updateName`', () async { + const name = 'Name'; + + final channelModel = ChannelModel( + cid: channelCid, + extraData: {'name': name}, + ); + + when(() => client.updateChannelPartial( + channelId, + channelType, + set: {'name': name}, + )).thenAnswer( + (_) async => PartialUpdateChannelResponse()..channel = channelModel, + ); + + final res = await channel.updateName(name); + + expect(res, isNotNull); + expect(res.channel.extraData['name'], name); + + verify(() => client.updateChannelPartial( + channelId, + channelType, + set: {'name': name}, + )).called(1); + }); + test('`.updatePartial`', () async { const set = { 'name': 'Stream Team', diff --git a/packages/stream_chat/test/src/core/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart index 0891b548..fbe493ef 100644 --- a/packages/stream_chat/test/src/core/models/reaction_test.dart +++ b/packages/stream_chat/test/src/core/models/reaction_test.dart @@ -13,10 +13,11 @@ void main() { expect(reaction.type, 'wow'); expect( reaction.user?.toJson(), - User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const { - 'image': 'https://randomuser.me/api/portraits/women/45.jpg', - 'name': 'Daisy Morgan' - }).toJson(), + User( + id: '2de0297c-f3f2-489d-b930-ef77342edccf', + image: 'https://randomuser.me/api/portraits/women/45.jpg', + name: 'Daisy Morgan', + ).toJson(), ); expect(reaction.score, 1); expect(reaction.userId, '2de0297c-f3f2-489d-b930-ef77342edccf'); @@ -28,11 +29,11 @@ void main() { messageId: '76cd8c82-b557-4e48-9d12-87995d3a0e04', createdAt: DateTime.parse('2020-01-28T22:17:31.108742Z'), type: 'wow', - user: - User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const { - 'image': 'https://randomuser.me/api/portraits/women/45.jpg', - 'name': 'Daisy Morgan' - }), + user: User( + id: '2de0297c-f3f2-489d-b930-ef77342edccf', + image: 'https://randomuser.me/api/portraits/women/45.jpg', + name: 'Daisy Morgan', + ), userId: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: {'bananas': 'yes'}, score: 1, @@ -58,10 +59,11 @@ void main() { expect(newReaction.type, 'wow'); expect( newReaction.user?.toJson(), - User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const { - 'image': 'https://randomuser.me/api/portraits/women/45.jpg', - 'name': 'Daisy Morgan', - }).toJson(), + User( + id: '2de0297c-f3f2-489d-b930-ef77342edccf', + image: 'https://randomuser.me/api/portraits/women/45.jpg', + name: 'Daisy Morgan', + ).toJson(), ); expect(newReaction.score, 1); expect(newReaction.userId, '2de0297c-f3f2-489d-b930-ef77342edccf'); diff --git a/packages/stream_chat_flutter/test/src/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel_image_test.dart index c9293e79..f6d110dd 100644 --- a/packages/stream_chat_flutter/test/src/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_image_test.dart @@ -48,7 +48,7 @@ void main() { ); testWidgets( - 'it should show the the other member image', + 'it should show the other member image', (tester) async { final client = MockClient(); final clientState = MockClientState(); @@ -74,9 +74,7 @@ void main() { userId: 'user-id2', user: User( id: 'user-id2', - extraData: const { - 'image': 'testimage', - }, + image: 'testimage', ), ) ])); @@ -85,9 +83,7 @@ void main() { userId: 'user-id2', user: User( id: 'user-id2', - extraData: const { - 'image': 'testimage', - }, + image: 'testimage', ), ), Member( @@ -98,9 +94,7 @@ void main() { when(() => clientState.usersStream).thenAnswer((i) => Stream.value({ 'user-id2': User( id: 'user-id2', - extraData: const { - 'image': 'testimage', - }, + image: 'testimage', ), })); when(() => channel.extraData).thenReturn({ @@ -149,27 +143,21 @@ void main() { userId: 'user-id', user: User( id: 'user-id', - extraData: const { - 'image': 'testimage1', - }, + image: 'testimage1', ), ), Member( userId: 'user-id2', user: User( id: 'user-id2', - extraData: const { - 'image': 'testimage2', - }, + image: 'testimage2', ), ), Member( userId: 'user-id3', user: User( id: 'user-id3', - extraData: const { - 'image': 'testimage3', - }, + image: 'testimage3', ), ), ]; diff --git a/packages/stream_chat_flutter_core/example/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart index 03e791de..31410e63 100644 --- a/packages/stream_chat_flutter_core/example/lib/main.dart +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -329,21 +329,9 @@ class _MessageScreenState extends State { } } -/// Extensions can be used to add functionality to the SDK. In the examples -/// below, we add two simple extensions to the [StreamChatClient] and [Channel]. +/// Extensions can be used to add functionality to the SDK. In the example +/// below, we add a simple extensions to the [StreamChatClient]. extension on StreamChatClient { /// Fetches the current user id. String get uid => state.currentUser!.id; } - -extension on Channel { - /// Fetches the name of the channel by accessing [extraData] or [cid]. - String? get name { - final _channelName = extraData['name']; - if (_channelName != null) { - return _channelName as String; - } else { - return cid; - } - } -}