diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index f7e02ba4..7e172a23 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -3,6 +3,8 @@ ✅ Added - Added `client.enrichUrl` endpoint for enriching URLs with metadata. +- Fixed `unreadCount` after removing user from a channel. +- `ChannelModel` now supplies individual user capabilities. ## 3.3.1 diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 1925e325..1693aeff 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -300,6 +300,18 @@ class Channel { return data; } + /// List of user permissions on this channel + List get ownCapabilities => + state?._channelState.channel?.ownCapabilities ?? []; + + /// List of user permissions on this channel + Stream> get ownCapabilitiesStream { + _checkInitialized(); + return state!.channelStateStream + .map((cs) => cs.channel?.ownCapabilities ?? []) + .distinct(); + } + /// Channel extra data as a stream. Stream> get extraDataStream { _checkInitialized(); @@ -1548,6 +1560,7 @@ class ChannelClientState { members: List.from( channelState.members..removeWhere((m) => m.userId == user!.id), ), + read: channelState.read..removeWhere((r) => r.user.id == user!.id), )); })); } diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index ed7c2c2e..967fe827 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -13,6 +13,7 @@ class ChannelModel { String? id, String? type, String? cid, + this.ownCapabilities = const [], ChannelConfig? config, this.createdBy, this.frozen = false, @@ -51,6 +52,10 @@ class ChannelModel { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String cid; + /// List of user permissions on this channel + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) + final List ownCapabilities; + /// The channel configuration data @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final ChannelConfig config; @@ -101,6 +106,7 @@ class ChannelModel { 'id', 'type', 'cid', + 'own_capabilities', 'config', 'created_by', 'frozen', @@ -127,6 +133,7 @@ class ChannelModel { String? id, String? type, String? cid, + List? ownCapabilities, ChannelConfig? config, User? createdBy, bool? frozen, @@ -143,6 +150,7 @@ class ChannelModel { id: id ?? this.id, type: type ?? this.type, cid: cid ?? this.cid, + ownCapabilities: ownCapabilities ?? this.ownCapabilities, config: config ?? this.config, createdBy: createdBy ?? this.createdBy, frozen: frozen ?? this.frozen, @@ -164,6 +172,7 @@ class ChannelModel { id: other.id, type: other.type, cid: other.cid, + ownCapabilities: other.ownCapabilities, config: other.config, createdBy: other.createdBy, frozen: other.frozen, diff --git a/packages/stream_chat/lib/src/core/models/channel_model.g.dart b/packages/stream_chat/lib/src/core/models/channel_model.g.dart index 94d9a81a..974ef383 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.g.dart @@ -10,6 +10,10 @@ ChannelModel _$ChannelModelFromJson(Map json) => ChannelModel( id: json['id'] as String?, type: json['type'] as String?, cid: json['cid'] as String?, + ownCapabilities: (json['own_capabilities'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], config: json['config'] == null ? null : ChannelConfig.fromJson(json['config'] as Map), @@ -48,6 +52,7 @@ Map _$ChannelModelToJson(ChannelModel instance) { } writeNotNull('cid', readonly(instance.cid)); + writeNotNull('own_capabilities', readonly(instance.ownCapabilities)); writeNotNull('config', readonly(instance.config)); writeNotNull('created_by', readonly(instance.createdBy)); val['frozen'] = instance.frozen; diff --git a/packages/stream_chat/lib/src/permission_type.dart b/packages/stream_chat/lib/src/permission_type.dart new file mode 100644 index 00000000..993351b7 --- /dev/null +++ b/packages/stream_chat/lib/src/permission_type.dart @@ -0,0 +1,96 @@ +/// Describes capabilities of a user vis-a-vis a channel +class PermissionType { + /// Capability required to send a message in the channel + /// Channel is not frozen (or user has UseFrozenChannel permission) + /// and user has CreateMessage permission. + static const String sendMessage = 'send-message'; + + /// Capability required to receive connect events in the channel + static const String connectEvents = 'connect-events'; + + /// Capability required to send a message + /// Reactions are enabled for the channel, channel is not frozen + /// (or user has UseFrozenChannel permission) and user has + /// CreateReaction permission + static const String sendReaction = 'send-reaction'; + + /// Capability required to send links in a channel + /// send-message + user has AddLinks permission + static const String sendLinks = 'send-links'; + + /// Capability required to send thread reply + /// send-message + channel has replies enabled + static const String sendReply = 'send-reply'; + + /// Capability to freeze a channel + /// User has UpdateChannelFrozen permission. + /// The name implies freezing, + /// but unfreezing is also allowed when this capability is present + static const String freezeChannel = 'freeze-channel'; + + /// User has UpdateChannelCooldown permission. + /// Allows to enable/disable slow mode in the channel + static const String setChannelCooldown = 'set-channel-cooldown'; + + /// User has RemoveOwnChannelMembership or UpdateChannelMembers permission + static const String leaveChannel = 'leave-channel'; + + /// User can mute channel + static const String muteChannel = 'mute-channel'; + + /// Ability to receive read events + static const String readEvents = 'read-events'; + + /// Capability required to pin a message in a channel + /// Corresponds to PinMessage permission + static const String pinMessage = 'pin-message'; + + /// Capability required to quote a message in a channel + static const String quoteMessage = 'quote-message'; + + /// Capability required to flag a message in a channel + static const String flagMessage = 'flag-message'; + + /// User has ability to delete any message in the channel + /// User has DeleteMessage permission + /// which applies to any message in the channel + static const String deleteAnyMessage = 'delete-any-message'; + + /// User has ability to delete their own message in the channel + /// User has DeleteMessage permission which applies only to owned messages + static const String deleteOwnMessage = 'delete-own-message'; + + /// User has ability to update/edit any message in the channel + /// User has UpdateMessage permission which + /// applies to any message in the channel + static const String updateAnyMessage = 'update-any-message'; + + /// User has ability to update/edit their own message in the channel + /// User has UpdateMessage permission which applies only to owned messages + static const String updateOwnMessage = 'update-own-message'; + + /// User can search for message in a channel + /// Search feature is enabled (it will also have + /// permission check in the future) + static const String searchMessages = 'search-messages'; + + /// Capability required to send typing events in a channel + /// (Typing events are enabled) + static const String sendTypingEvents = 'send-typing-events'; + + /// Capability required to upload a file in a channel + /// Uploads are enabled and user has UploadAttachment + static const String uploadFile = 'upload-file'; + + /// Capability required to delete channel + /// User has DeleteChannel permission + static const String deleteChannel = 'delete-channel'; + + /// Capability required update/edit channel info + /// User has UpdateChannel permission + static const String updateChannel = 'update-channel'; + + /// Capability required to update/edit channel members + /// Channel is not distinct and user has UpdateChannelMembers permission + static const String updateChannelMembers = 'update-channel-members'; +} diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 367b27af..c0ceab3a 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -37,6 +37,7 @@ export './src/core/util/extension.dart'; export './src/db/chat_persistence_client.dart'; export './src/event_type.dart'; export './src/location.dart'; +export './src/permission_type.dart'; export './src/ws/connection_status.dart'; export 'src/client/channel.dart'; export 'src/client/client.dart'; diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index db9fef23..130e1b8a 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -2,10 +2,12 @@ 🛑️ Breaking Changes +- `pinPermissions` is no longer needed in `MessageListView`. - `MessageInput` now works with a `MessageInputController` instead of a `TextEditingController` 🐞 Fixed +- SVG rendering fixes - Use file extension instead of mimeType for downloading files ✅ Added diff --git a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 1721e6f1..4b1a8d5d 100644 --- a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -156,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cf..3db53b6e 100644 --- a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ { title: context.translations.viewInfoLabel, onTap: widget.onViewInfoTap, ), - if (!channel.isDistinct) + if (!channel.isDistinct && + channel.ownCapabilities + .contains(PermissionType.leaveChannel)) OptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -174,7 +176,9 @@ class _ChannelBottomSheetState extends State { }); }, ), - if (isOwner) + if (isOwner && + channel.ownCapabilities + .contains(PermissionType.deleteChannel)) OptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index 62bfd1de..791805af 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -61,7 +61,8 @@ class ChannelInfo extends StatelessWidget { var text = context.translations.membersCountText(memberCount); final onlineCount = members?.where((m) => m.user?.online == true).length ?? 0; - if (onlineCount > 0) { + if (channel.ownCapabilities.contains(PermissionType.connectEvents) && + onlineCount > 0) { text += ', ${context.translations.watchersCountText(onlineCount)}'; } alternativeWidget = Text( diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index fd59489a..c6c3ec35 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -560,14 +559,8 @@ class _ChannelListViewState extends State { ); }, ), - if ([ - 'admin', - 'owner', - ].contains(channel.state!.members - .firstWhereOrNull( - (m) => m.userId == channel.client.state.currentUser?.id, - ) - ?.role)) + if (channel.ownCapabilities + .contains(PermissionType.deleteChannel)) IconSlideAction( color: backgroundColor, iconWidget: StreamSvgIcon.delete( diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 73956da6..754ca8bf 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -47,6 +47,7 @@ extension IterableX on Iterable { extension PlatformFileX on PlatformFile { /// Converts the [PlatformFile] into [AttachmentFile] AttachmentFile get toAttachmentFile => AttachmentFile( + // ignore: avoid_redundant_argument_values path: kIsWeb ? null : path, name: name, bytes: bytes, diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 8039ac47..ed15dad8 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -93,6 +93,9 @@ abstract class Translations { /// The label for search Gif String get searchGifLabel; + /// The label for the MessageInput hint when permission denied on sendMessage + String get sendMessagePermissionError; + /// The label for add a comment or send in case of /// attachments inside [MessageInput] String get addACommentOrSendLabel; @@ -141,6 +144,12 @@ abstract class Translations { /// The label for "OK" String get okLabel; + /// The label for a link disabled error + String get linkDisabledError; + + /// The additional info on a link disabled error + String get linkDisabledDetails; + /// The label for "add more files" String get addMoreFilesLabel; @@ -377,6 +386,10 @@ class DefaultTranslations implements Translations { return 'Pinned by ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'You don\'t have permission to send messages'; + @override String get emptyMessagesText => 'There are no messages currently'; @@ -685,4 +698,11 @@ class DefaultTranslations implements Translations { @override String attachmentLimitExceedError(int limit) => """ Attachment limit exceeded: it's not possible to add more than $limit attachments"""; + + @override + String get linkDisabledDetails => + 'Sending links is not allowed in this conversation.'; + + @override + String get linkDisabledError => 'Links are disabled'; } diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index e85e7cd8..f72acf2c 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -9,6 +9,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Constructs a modal with actions for a message class MessageActionsModal extends StatefulWidget { @@ -18,17 +19,17 @@ class MessageActionsModal extends StatefulWidget { required this.message, required this.messageWidget, required this.messageTheme, - this.showReactions = true, - this.showDeleteMessage = true, - this.showEditMessage = true, + this.showReactions, + this.showDeleteMessage, + this.showEditMessage, this.onReplyTap, this.onThreadReplyTap, this.showCopyMessage = true, this.showReplyMessage = true, this.showResendMessage = true, - this.showThreadReplyMessage = true, - this.showFlagButton = true, - this.showPinButton = true, + this.showThreadReplyMessage, + this.showFlagButton, + this.showPinButton, this.editMessageInputBuilder, this.reverse = false, this.customActions = const [], @@ -54,34 +55,34 @@ class MessageActionsModal extends StatefulWidget { final MessageThemeData messageTheme; /// Flag for showing reactions - final bool showReactions; + final bool? showReactions; /// Callback when copy is tapped final OnMessageTap? onCopyTap; /// Callback when delete is tapped - final bool showDeleteMessage; + final bool? showDeleteMessage; /// Flag for showing copy action final bool showCopyMessage; /// Flag for showing edit action - final bool showEditMessage; + final bool? showEditMessage; /// Flag for showing resend action final bool showResendMessage; /// Flag for showing reply action - final bool showReplyMessage; + final bool? showReplyMessage; /// Flag for showing thread reply action - final bool showThreadReplyMessage; + final bool? showThreadReplyMessage; /// Flag for showing flag action - final bool showFlagButton; + final bool? showFlagButton; /// Flag for showing pin action - final bool showPinButton; + final bool? showPinButton; /// Flag for reversing message final bool reverse; @@ -95,6 +96,8 @@ class MessageActionsModal extends StatefulWidget { class _MessageActionsModalState extends State { bool _showActions = true; + late List _userPermissions; + late bool _isMyMessage; @override Widget build(BuildContext context) => _showMessageOptionsModal(); @@ -129,6 +132,19 @@ class _MessageActionsModalState extends State { final shiftFactor = numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0; + final hasEditPermission = _userPermissions.contains( + PermissionType.updateAnyMessage, + ) || + _userPermissions.contains(PermissionType.updateOwnMessage); + + final hasDeletePermission = _userPermissions.contains( + PermissionType.deleteAnyMessage, + ) || + _userPermissions.contains(PermissionType.deleteOwnMessage); + + final hasReactionPermission = + _userPermissions.contains(PermissionType.sendReaction); + final child = Center( child: SingleChildScrollView( child: Padding( @@ -138,7 +154,7 @@ class _MessageActionsModalState extends State { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - if (widget.showReactions && + if ((widget.showReactions ?? hasReactionPermission) && (widget.message.status == MessageSendingStatus.sent)) Align( alignment: Alignment( @@ -175,21 +191,35 @@ class _MessageActionsModalState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (widget.showReplyMessage && - widget.message.status == MessageSendingStatus.sent) + if (widget.showReplyMessage ?? + (_userPermissions + .contains(PermissionType.quoteMessage) && + widget.message.status == + MessageSendingStatus.sent)) _buildReplyButton(context), - if (widget.showThreadReplyMessage && - (widget.message.status == - MessageSendingStatus.sent) && - widget.message.parentId == null) + if (widget.showThreadReplyMessage ?? + _userPermissions + .contains(PermissionType.sendReply) && + (widget.message.status == + MessageSendingStatus.sent) && + widget.message.parentId == null) _buildThreadReplyButton(context), if (widget.showResendMessage) _buildResendMessage(context), - if (widget.showEditMessage) _buildEditMessage(context), + if (widget.showEditMessage ?? + _isMyMessage && hasEditPermission) + _buildEditMessage(context), if (widget.showCopyMessage) _buildCopyButton(context), - if (widget.showFlagButton) _buildFlagButton(context), - if (widget.showPinButton) _buildPinButton(context), - if (widget.showDeleteMessage) + if (widget.showFlagButton ?? + _userPermissions + .contains(PermissionType.flagMessage)) + _buildFlagButton(context), + if (widget.showPinButton ?? + _userPermissions + .contains(PermissionType.pinMessage)) + _buildPinButton(context), + if (widget.showDeleteMessage ?? + (_isMyMessage && hasDeletePermission)) _buildDeleteButton(context), ...widget.customActions .map((action) => _buildCustomAction( @@ -652,4 +682,13 @@ class _MessageActionsModalState extends State { ), ); } + + @override + void didChangeDependencies() { + final newStreamChannel = StreamChannel.of(context); + _userPermissions = newStreamChannel.channel.ownCapabilities; + _isMyMessage = + widget.message.user?.id == StreamChat.of(context).currentUser?.id; + super.didChangeDependencies(); + } } diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 9d5ae3d0..816a4aec 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -471,120 +471,138 @@ class MessageInputState extends State void _stopSlowMode() => _slowModeTimer?.cancel(); @override - Widget build(BuildContext context) => MessageValueListenableBuilder( - valueListenable: _effectiveController, - builder: (context, value, _) { - Widget child = DecoratedBox( - decoration: BoxDecoration( - color: _messageInputTheme.inputBackgroundColor, - ), - child: SafeArea( - child: GestureDetector( - onPanUpdate: (details) { - if (details.delta.dy > 0) { - _focusNode.unfocus(); - if (_openFilePickerSection) { - setState(() { - _openFilePickerSection = false; - }); - } + Widget build(BuildContext context) { + if (!StreamChannel.of(context) + .channel + .ownCapabilities + .contains(PermissionType.sendMessage)) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 15, + ), + child: Text( + context.translations.sendMessagePermissionError, + style: _messageInputTheme.inputTextStyle, + ), + ), + ); + } + return MessageValueListenableBuilder( + valueListenable: _effectiveController, + builder: (context, value, _) { + Widget child = DecoratedBox( + decoration: BoxDecoration( + color: _messageInputTheme.inputBackgroundColor, + ), + child: SafeArea( + child: GestureDetector( + onPanUpdate: (details) { + if (details.delta.dy > 0) { + _focusNode.unfocus(); + if (_openFilePickerSection) { + setState(() { + _openFilePickerSection = false; + }); } - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (_hasQuotedMessage) - Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: StreamSvgIcon.reply( - color: _streamChatTheme.colorTheme.disabled, - ), - ), - Text( - context.translations.replyToMessageLabel, - style: - const TextStyle(fontWeight: FontWeight.bold), - ), - IconButton( - visualDensity: VisualDensity.compact, - icon: StreamSvgIcon.closeSmall(), - onPressed: () { - _effectiveController.clearQuotedMessage(); - _focusNode.unfocus(); - }, - ), - ], - ), - ) - else if (_effectiveController.ogAttachment != null) - OGAttachmentPreview( - attachment: _effectiveController.ogAttachment!, - onDismissPreviewPressed: () { - _effectiveController.clearOGAttachment(); - _focusNode.unfocus(); - }, - ), + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_hasQuotedMessage) Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: _buildTextField(context), - ), - if (_effectiveController.value.parentId != null && - !widget.hideSendAsDm) - Padding( - padding: const EdgeInsets.only( - right: 12, - left: 12, - bottom: 12, - ), - child: _buildDmCheckbox(), + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: StreamSvgIcon.reply( + color: _streamChatTheme.colorTheme.disabled, + ), + ), + Text( + context.translations.replyToMessageLabel, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: StreamSvgIcon.closeSmall(), + onPressed: () { + _effectiveController.clearQuotedMessage(); + _focusNode.unfocus(); + }, + ), + ], ), - _buildFilePickerSection(), - ], - ), + ) + else if (_effectiveController.ogAttachment != null) + OGAttachmentPreview( + attachment: _effectiveController.ogAttachment!, + onDismissPreviewPressed: () { + _effectiveController.clearOGAttachment(); + _focusNode.unfocus(); + }, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: _buildTextField(context), + ), + if (_effectiveController.value.parentId != null && + !widget.hideSendAsDm) + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + bottom: 12, + ), + child: _buildDmCheckbox(), + ), + _buildFilePickerSection(), + ], ), ), - ); - if (!_isEditing) { - child = Material( - elevation: 8, - child: child, - ); - } - return MultiOverlay( - childAnchor: Alignment.topCenter, - overlayAnchor: Alignment.bottomCenter, - overlayOptions: [ - OverlayOptions( - visible: _showCommandsOverlay, - widget: _buildCommandsOverlayEntry(), - ), - OverlayOptions( - visible: _focusNode.hasFocus && - _effectiveController.text.isNotEmpty && - _effectiveController.baseOffset > 0 && - _effectiveController.text - .substring( - 0, - _effectiveController.baseOffset, - ) - .contains(':'), - widget: _buildEmojiOverlay(), - ), - OverlayOptions( - visible: _showMentionsOverlay, - widget: _buildMentionsOverlayEntry(), - ), - ...widget.customOverlays, - ], + ), + ); + if (!_isEditing) { + child = Material( + elevation: 8, child: child, ); - }, - ); + } + return MultiOverlay( + childAnchor: Alignment.topCenter, + overlayAnchor: Alignment.bottomCenter, + overlayOptions: [ + OverlayOptions( + visible: _showCommandsOverlay, + widget: _buildCommandsOverlayEntry(), + ), + OverlayOptions( + visible: _focusNode.hasFocus && + _effectiveController.text.isNotEmpty && + _effectiveController.baseOffset > 0 && + _effectiveController.text + .substring( + 0, + _effectiveController.baseOffset, + ) + .contains(':'), + widget: _buildEmojiOverlay(), + ), + OverlayOptions( + visible: _showMentionsOverlay, + widget: _buildMentionsOverlayEntry(), + ), + ...widget.customOverlays, + ], + child: child, + ); + }, + ); + } Flex _buildTextField(BuildContext context) => Flex( direction: Axis.horizontal, @@ -712,7 +730,9 @@ class MessageInputState extends State ? const Offstage() : Wrap( children: [ - if (!widget.disableAttachments) + if (!widget.disableAttachments && + channel.ownCapabilities + .contains(PermissionType.uploadFile)) _buildAttachmentButton(context), if (widget.showCommandsButton && !_isEditing && @@ -892,7 +912,8 @@ class MessageInputState extends State value = value.trim(); final channel = StreamChannel.of(context).channel; - if (value.isNotEmpty) { + if (channel.ownCapabilities.contains(PermissionType.sendTypingEvents) && + value.isNotEmpty) { channel .keyStroke(_effectiveController.value.parentId) // ignore: no-empty-block @@ -948,7 +969,11 @@ class MessageInputState extends State ..removeWhere((it) => it.group(0)?.split('.').last.isValidTLD() == false); // Reset the og attachment if the text doesn't contain any url - if (matchedUrls.isEmpty) { + if (matchedUrls.isEmpty || + !StreamChannel.of(context) + .channel + .ownCapabilities + .contains(PermissionType.sendLinks)) { _effectiveController ..text = value ..clearOGAttachment(); @@ -1625,9 +1650,26 @@ class MessageInputState extends State /// Sends the current message Future sendMessage() async { - final skipEnrichUrl = _effectiveController.ogAttachment == null; - + final streamChannel = StreamChannel.of(context); var message = _effectiveController.value; + if (!streamChannel.channel.ownCapabilities + .contains(PermissionType.sendLinks) && + _urlRegex.allMatches(message.text ?? '').any((element) => + element.group(0)?.split('.').last.isValidTLD() == true)) { + showInfoDialog( + context, + icon: StreamSvgIcon.error( + color: StreamChatTheme.of(context).colorTheme.accentError, + size: 24, + ), + title: 'Links are disabled', + details: 'Sending links is not allowed in this conversation.', + okText: context.translations.okLabel, + ); + return; + } + + final skipEnrichUrl = _effectiveController.ogAttachment == null; var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; @@ -1639,7 +1681,6 @@ class MessageInputState extends State message = await widget.preMessageSending!(message); } - final streamChannel = StreamChannel.of(context); final channel = streamChannel.channel; if (!channel.state!.isUpToDate) { await streamChannel.reloadChannel(); diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 40e74f23..8a22920e 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -204,7 +204,6 @@ class MessageListView extends StatefulWidget { this.messageFilter, this.onMessageTap, this.onSystemMessageTap, - this.pinPermissions = const [], this.showFloatingDateDivider = true, this.threadSeparatorBuilder, this.messageListController, @@ -322,9 +321,6 @@ class MessageListView extends StatefulWidget { /// Called when system message is tapped final OnMessageTap? onSystemMessageTap; - /// A List of user types that have permission to pin messages - final List pinPermissions; - /// Builder used to build the thread separator in case it's a thread view final WidgetBuilder? threadSeparatorBuilder; @@ -353,6 +349,7 @@ class _MessageListViewState extends State { int? _messageListLength; StreamChannelState? streamChannel; late StreamChatThemeData _streamTheme; + late List _userPermissions; int get _initialIndex { final initialScrollIndex = widget.initialScrollIndex; @@ -1031,7 +1028,7 @@ class _MessageListViewState extends State { FocusScope.of(context).unfocus(); }, showPinButton: currentUserMember != null && - widget.pinPermissions.contains(currentUserMember.role), + _userPermissions.contains(PermissionType.pinMessage), ); if (widget.parentMessageBuilder != null) { @@ -1150,7 +1147,10 @@ class _MessageListViewState extends State { }, showEditMessage: isMyMessage, showDeleteMessage: isMyMessage, - showThreadReplyMessage: !isThreadMessage, + showThreadReplyMessage: !isThreadMessage && + streamChannel?.channel.ownCapabilities + .contains(PermissionType.sendReply) == + true, showFlagButton: !isMyMessage, borderSide: borderSide, onThreadTap: _onThreadTap, @@ -1219,7 +1219,7 @@ class _MessageListViewState extends State { FocusScope.of(context).unfocus(); }, showPinButton: currentUserMember != null && - widget.pinPermissions.contains(currentUserMember.role), + _userPermissions.contains(PermissionType.pinMessage), ); if (widget.messageBuilder != null) { @@ -1299,6 +1299,7 @@ class _MessageListViewState extends State { void didChangeDependencies() { final newStreamChannel = StreamChannel.of(context); _streamTheme = StreamChatTheme.of(context); + _userPermissions = newStreamChannel.channel.ownCapabilities; if (newStreamChannel != streamChannel) { streamChannel = newStreamChannel; diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 6a73c439..37c8afff 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -18,7 +18,7 @@ class MessageReactionsModal extends StatelessWidget { required this.message, required this.messageWidget, required this.messageTheme, - this.showReactions = true, + this.showReactions, this.reverse = false, this.onUserAvatarTap, }) : super(key: key); @@ -36,7 +36,7 @@ class MessageReactionsModal extends StatelessWidget { final bool reverse; /// Flag to show reactions on message - final bool showReactions; + final bool? showReactions; /// Callback when user avatar is tapped final void Function(User)? onUserAvatarTap; @@ -45,6 +45,10 @@ class MessageReactionsModal extends StatelessWidget { Widget build(BuildContext context) { final size = MediaQuery.of(context).size; final user = StreamChat.of(context).currentUser; + final _userPermissions = StreamChannel.of(context).channel.ownCapabilities; + + final hasReactionPermission = + _userPermissions.contains(PermissionType.sendReaction); final roughMaxSize = size.width * 2 / 3; var messageTextLength = message.text!.length; @@ -76,7 +80,7 @@ class MessageReactionsModal extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (showReactions && + if ((showReactions ?? hasReactionPermission) && (message.status == MessageSendingStatus.sent)) Align( alignment: Alignment( diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 4010b11b..2e97c9da 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -1095,7 +1095,8 @@ class _MessageWidgetState extends State showSendingIndicator: false, padding: const EdgeInsets.all(0), showReactionPickerIndicator: widget.showReactions && - (widget.message.status == MessageSendingStatus.sent), + (widget.message.status == MessageSendingStatus.sent) && + channel.ownCapabilities.contains(PermissionType.sendReaction), showPinHighlight: false, showUserAvatar: widget.message.user!.id == channel.client.state.currentUser!.id @@ -1106,7 +1107,6 @@ class _MessageWidgetState extends State Clipboard.setData(ClipboardData(text: message.text)), messageTheme: widget.messageTheme, reverse: widget.reverse, - showDeleteMessage: widget.showDeleteMessage || isDeleteFailed, message: widget.message, editMessageInputBuilder: widget.editMessageInputBuilder, onReplyTap: widget.onReplyTap, @@ -1116,11 +1116,6 @@ class _MessageWidgetState extends State showCopyMessage: widget.showCopyMessage && !isFailedState && widget.message.text?.trim().isNotEmpty == true, - showEditMessage: widget.showEditMessage && - !isDeleteFailed && - !widget.message.attachments - .any((element) => element.type == 'giphy'), - showReactions: widget.showReactions, showReplyMessage: widget.showReplyMessage && !isFailedState && widget.onReplyTap != null, @@ -1128,7 +1123,6 @@ class _MessageWidgetState extends State !isFailedState && widget.onThreadTap != null, showFlagButton: widget.showFlagButton, - showPinButton: widget.showPinButton, customActions: widget.customActions, ), ), @@ -1158,7 +1152,8 @@ class _MessageWidgetState extends State showSendingIndicator: false, padding: const EdgeInsets.all(0), showReactionPickerIndicator: widget.showReactions && - (widget.message.status == MessageSendingStatus.sent), + (widget.message.status == MessageSendingStatus.sent) && + channel.ownCapabilities.contains(PermissionType.sendReaction), showPinHighlight: false, showUserAvatar: widget.message.user!.id == channel.client.state.currentUser!.id @@ -1169,7 +1164,8 @@ class _MessageWidgetState extends State messageTheme: widget.messageTheme, reverse: widget.reverse, message: widget.message, - showReactions: widget.showReactions, + showReactions: widget.showReactions && + channel.ownCapabilities.contains(PermissionType.sendReaction), ), ), ); @@ -1257,6 +1253,13 @@ class _MessageWidgetState extends State final channel = StreamChannel.of(context).channel; + if (!channel.ownCapabilities.contains(PermissionType.readEvents)) { + return SendingIndicator( + message: message, + size: style!.fontSize, + ); + } + return BetterStreamBuilder>( stream: channel.state?.readStream, initialData: channel.state?.read, diff --git a/packages/stream_chat_flutter/lib/svgs/STREAM MARK 1.svg b/packages/stream_chat_flutter/lib/svgs/STREAM MARK 1.svg index 57ccef0f..91fedc05 100644 --- a/packages/stream_chat_flutter/lib/svgs/STREAM MARK 1.svg +++ b/packages/stream_chat_flutter/lib/svgs/STREAM MARK 1.svg @@ -1,10 +1,10 @@ + + + + + - - - - - diff --git a/packages/stream_chat_flutter/lib/svgs/UI_Reverse Pagination Loading.svg b/packages/stream_chat_flutter/lib/svgs/UI_Reverse Pagination Loading.svg index dfc3754f..487388a5 100644 --- a/packages/stream_chat_flutter/lib/svgs/UI_Reverse Pagination Loading.svg +++ b/packages/stream_chat_flutter/lib/svgs/UI_Reverse Pagination Loading.svg @@ -1,14 +1,14 @@ + + + + + + + + + - - - - - - - - - diff --git a/packages/stream_chat_flutter/lib/svgs/filetype_DOC.svg b/packages/stream_chat_flutter/lib/svgs/filetype_DOC.svg index 4233055a..36ce7610 100644 --- a/packages/stream_chat_flutter/lib/svgs/filetype_DOC.svg +++ b/packages/stream_chat_flutter/lib/svgs/filetype_DOC.svg @@ -1,12 +1,12 @@ + + + + + + - - - - - - diff --git a/packages/stream_chat_flutter/lib/svgs/filetype_DOCX.svg b/packages/stream_chat_flutter/lib/svgs/filetype_DOCX.svg index 3077f515..dea4b040 100644 --- a/packages/stream_chat_flutter/lib/svgs/filetype_DOCX.svg +++ b/packages/stream_chat_flutter/lib/svgs/filetype_DOCX.svg @@ -1,12 +1,12 @@ + + + + + + - - - - - - diff --git a/packages/stream_chat_flutter/lib/svgs/filetype_Generic.svg b/packages/stream_chat_flutter/lib/svgs/filetype_Generic.svg index 3ec0f239..453ee694 100644 --- a/packages/stream_chat_flutter/lib/svgs/filetype_Generic.svg +++ b/packages/stream_chat_flutter/lib/svgs/filetype_Generic.svg @@ -1,11 +1,11 @@ + + + + + + - - - - - - diff --git a/packages/stream_chat_flutter/lib/svgs/filetype_PPT.svg b/packages/stream_chat_flutter/lib/svgs/filetype_PPT.svg index 0541a496..1d3d429c 100644 --- a/packages/stream_chat_flutter/lib/svgs/filetype_PPT.svg +++ b/packages/stream_chat_flutter/lib/svgs/filetype_PPT.svg @@ -1,12 +1,12 @@ + + + + + + - - - - - - diff --git a/packages/stream_chat_flutter/lib/svgs/filetype_PPTX-1.svg b/packages/stream_chat_flutter/lib/svgs/filetype_PPTX-1.svg index a50efd64..c6d866a1 100644 --- a/packages/stream_chat_flutter/lib/svgs/filetype_PPTX-1.svg +++ b/packages/stream_chat_flutter/lib/svgs/filetype_PPTX-1.svg @@ -1,12 +1,12 @@ + + + + + + - - - - - - diff --git a/packages/stream_chat_flutter/lib/svgs/filetype_PPTX.svg b/packages/stream_chat_flutter/lib/svgs/filetype_PPTX.svg index 83339acc..920fbb62 100644 --- a/packages/stream_chat_flutter/lib/svgs/filetype_PPTX.svg +++ b/packages/stream_chat_flutter/lib/svgs/filetype_PPTX.svg @@ -1,12 +1,12 @@ + + + + + + - - - - - - diff --git a/packages/stream_chat_flutter/lib/svgs/filetype_XLS.svg b/packages/stream_chat_flutter/lib/svgs/filetype_XLS.svg index eec9a05d..60820220 100644 --- a/packages/stream_chat_flutter/lib/svgs/filetype_XLS.svg +++ b/packages/stream_chat_flutter/lib/svgs/filetype_XLS.svg @@ -1,12 +1,12 @@ + + + + + + - - - - - - diff --git a/packages/stream_chat_flutter/lib/svgs/filetype_XLSX.svg b/packages/stream_chat_flutter/lib/svgs/filetype_XLSX.svg index 6203aa27..c71ff20b 100644 --- a/packages/stream_chat_flutter/lib/svgs/filetype_XLSX.svg +++ b/packages/stream_chat_flutter/lib/svgs/filetype_XLSX.svg @@ -1,12 +1,12 @@ + + + + + + - - - - - - diff --git a/packages/stream_chat_flutter/lib/svgs/imgur.svg b/packages/stream_chat_flutter/lib/svgs/imgur.svg index 1df5d71f..71ab1fc5 100644 --- a/packages/stream_chat_flutter/lib/svgs/imgur.svg +++ b/packages/stream_chat_flutter/lib/svgs/imgur.svg @@ -1,12 +1,12 @@ + + + + + - - - - - diff --git a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart index cfd6f4ec..72b08757 100644 --- a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart @@ -19,6 +19,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -32,19 +33,25 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: MessageActionsModal( + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + status: MessageSendingStatus.sent, ), - status: MessageSendingStatus.sent, + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), + messageTheme: streamTheme.ownMessageTheme, + showThreadReplyMessage: true, + showEditMessage: true, + showDeleteMessage: true, ), - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), - ), - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -66,6 +73,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -79,22 +87,23 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - showEditMessage: false, - showCopyMessage: false, - showDeleteMessage: false, - showReplyMessage: false, - showThreadReplyMessage: false, - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: MessageActionsModal( + showCopyMessage: false, + showReplyMessage: false, + showThreadReplyMessage: false, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), ), - ), - messageTheme: streamTheme.ownMessageTheme, - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), ), ), ), @@ -117,6 +126,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -132,24 +142,27 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: MessageActionsModal( + messageWidget: const Text('test'), + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), ), + messageTheme: streamTheme.ownMessageTheme, + customActions: [ + MessageAction( + leading: const Icon(Icons.check), + title: const Text('title'), + onTap: (m) { + tapped = true; + }, + ), + ], ), - messageTheme: streamTheme.ownMessageTheme, - customActions: [ - MessageAction( - leading: const Icon(Icons.check), - title: const Text('title'), - onTap: (m) { - tapped = true; - }, - ), - ], ), ), ), @@ -172,6 +185,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -188,19 +202,22 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - onReplyTap: (m) { - tapped = true; - }, - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: MessageActionsModal( + messageWidget: const Text('test'), + onReplyTap: (m) { + tapped = true; + }, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + status: MessageSendingStatus.sent, ), - status: MessageSendingStatus.sent, + messageTheme: streamTheme.ownMessageTheme, ), - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -219,6 +236,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -235,19 +253,23 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - onThreadReplyTap: (m) { - tapped = true; - }, - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: MessageActionsModal( + messageWidget: const Text('test'), + onThreadReplyTap: (m) { + tapped = true; + }, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + status: MessageSendingStatus.sent, ), - status: MessageSendingStatus.sent, + messageTheme: streamTheme.ownMessageTheme, + showThreadReplyMessage: true, ), - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -297,6 +319,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showEditMessage: true, ), ), ), @@ -347,6 +370,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showEditMessage: true, ), ), ), @@ -547,6 +571,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showFlagButton: true, ), ), ), @@ -603,6 +628,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showFlagButton: true, ), ), ), @@ -659,6 +685,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showFlagButton: true, ), ), ), @@ -713,6 +740,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showDeleteMessage: true, ), ), ), @@ -769,6 +797,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showDeleteMessage: true, ), ), ), diff --git a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart index 3732ca0d..14877ce7 100644 --- a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart @@ -13,6 +13,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); final themeData = ThemeData(); when(() => client.state).thenReturn(clientState); @@ -33,13 +34,16 @@ void main() { home: StreamChat( client: client, streamChatThemeData: streamTheme, - child: MessageReactionsModal( - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), + child: StreamChannel( + channel: channel, + child: MessageReactionsModal( + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), + message: message, + messageTheme: streamTheme.ownMessageTheme, ), - message: message, - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -58,6 +62,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); final themeData = ThemeData(); when(() => client.state).thenReturn(clientState); @@ -89,16 +94,19 @@ void main() { home: StreamChat( client: client, streamChatThemeData: streamTheme, - child: MessageReactionsModal( - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), + child: StreamChannel( + channel: channel, + child: MessageReactionsModal( + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), + message: message, + messageTheme: streamTheme.ownMessageTheme, + reverse: true, + showReactions: false, + onUserAvatarTap: onUserAvatarTap, ), - message: message, - messageTheme: streamTheme.ownMessageTheme, - reverse: true, - showReactions: false, - onUserAvatarTap: onUserAvatarTap, ), ), ), diff --git a/packages/stream_chat_flutter/test/src/mocks.dart b/packages/stream_chat_flutter/test/src/mocks.dart index 443f770c..1fa1e99b 100644 --- a/packages/stream_chat_flutter/test/src/mocks.dart +++ b/packages/stream_chat_flutter/test/src/mocks.dart @@ -21,6 +21,9 @@ class MockChannel extends Mock implements Channel { Future keyStroke([String? parentId]) async { return; } + + @override + List get ownCapabilities => ['send-message']; } class MockChannelState extends Mock implements ChannelClientState { diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 6fa7689b..d111ead0 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -84,6 +84,10 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { return 'Pinned by ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'You don\'t have permission to send messages'; + @override String get emptyMessagesText => 'There are no messages currently'; @@ -392,6 +396,13 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'Slow mode ON'; + + @override + String get linkDisabledDetails => + 'Sending links is not allowed in this conversation.'; + + @override + String get linkDisabledError => 'Links are disabled'; } void main() async { diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index c1106cab..08dbb02a 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -60,6 +60,10 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { return 'Pinned by ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'You don\'t have permission to send messages'; + @override String get emptyMessagesText => 'There are no messages currently'; @@ -368,4 +372,11 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'Slow mode ON'; + + @override + String get linkDisabledDetails => + 'Sending links is not allowed in this conversation.'; + + @override + String get linkDisabledError => 'Links are disabled'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index 49e66184..50c748cb 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -61,6 +61,10 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { return 'Fijado por ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'No tienes permiso para enviar mensajes'; + @override String get emptyMessagesText => 'Actualmente no hay mensajes'; @@ -374,4 +378,11 @@ No es posible añadir más de $limit archivos adjuntos @override String get slowModeOnLabel => 'Modo lento activado'; + + @override + String get linkDisabledDetails => + 'No se permite enviar enlaces en esta conversación.'; + + @override + String get linkDisabledError => 'Los enlaces están deshabilitados'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index 0057b7cc..0671f5af 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -61,6 +61,10 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { return 'Épinglé par ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'Vous n\'êtes pas autorisé à envoyer des messages'; + @override String get emptyMessagesText => "Il n'y a pas de messages actuellement"; @@ -373,4 +377,11 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ @override String get slowModeOnLabel => 'Mode lent activé'; + + @override + String get linkDisabledDetails => + 'L\'envoi de liens n\'est pas autorisé dans cette conversation.'; + + @override + String get linkDisabledError => 'Les liens sont désactivés'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 0d7e1ec0..b8d3f3dc 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -60,6 +60,9 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { return '${pinnedBy.name} द्वारा पिन किया गया'; } + @override + String get sendMessagePermissionError => 'आपको संदेश भेजने की अनुमति नहीं है'; + @override String get emptyMessagesText => 'वर्तमान में कोई संदेश नहीं है'; @@ -368,4 +371,11 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'स्लो मोड चालू'; + + @override + String get linkDisabledDetails => + 'इस बातचीत में लिंक भेजने की अनुमति नहीं है.'; + + @override + String get linkDisabledError => 'लिंक भेजना प्रतिबंधित'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index 18e961d6..1f7fd048 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -60,6 +60,10 @@ class StreamChatLocalizationsIt extends GlobalStreamChatLocalizations { return 'Messo in evidenza da ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'Non hai l\'autorizzazione per inviare messaggi'; + @override String get emptyMessagesText => 'Non c\'é nessun messaggio al momento'; @@ -370,4 +374,11 @@ Attenzione: il limite massimo di $limit file è stato superato. @override String get slowModeOnLabel => 'Slowmode attiva'; + + @override + String get linkDisabledDetails => + 'Non è permesso condividere link in questa convesazione.'; + + @override + String get linkDisabledError => 'I links sono disattivati'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index 8b58630d..1659f637 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -60,6 +60,9 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { return '${pinnedBy.name}のピン'; } + @override + String get sendMessagePermissionError => 'メッセージを送信する権限がありません'; + @override String get emptyMessagesText => '現在、メッセージはありません。'; @@ -354,4 +357,10 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String attachmentLimitExceedError(int limit) => ''' 添付ファイルの制限を超えました:$limit個のファイル以上を添付することはできません '''; + + @override + String get linkDisabledDetails => 'この会話では、リンクの送信は許可されていません。'; + + @override + String get linkDisabledError => 'リンクが無効になっています'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index 3693bc11..820d58e4 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -60,6 +60,9 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { return '${pinnedBy.name}의 핀'; } + @override + String get sendMessagePermissionError => '메시지를 보낼 수 있는 권한이 없습니다'; + @override String get emptyMessagesText => '현재 메시지가 없습니다'; @@ -354,4 +357,10 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String attachmentLimitExceedError(int limit) => '첨부 파일 제한 초과: $limit 이상의 첨부 파일을 추가할 수 없습니다'; + + @override + String get linkDisabledDetails => '이 대화에서는 링크를 보낼 수 없습니다.'; + + @override + String get linkDisabledError => '링크가 비활성화되었습니다.'; }