From 5d163b9a860f90fa3d740483a1baabebcf94f4fd Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 5 Jan 2022 11:18:46 +0100 Subject: [PATCH] add missing permissions and ui --- .../stream_chat/lib/src/client/channel.dart | 12 + .../stream_chat/lib/src/permission_type.dart | 12 + .../lib/src/channel_info.dart | 3 +- .../lib/src/channel_list_view.dart | 10 +- .../lib/src/localization/translations.dart | 7 + .../lib/src/message_actions_modal.dart | 13 +- .../lib/src/message_input/message_input.dart | 220 ++++++++++-------- .../lib/src/message_list_view.dart | 4 +- .../lib/src/message_reactions_modal.dart | 8 +- .../lib/src/message_widget.dart | 7 + 10 files changed, 171 insertions(+), 125 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 258122a7..db39628b 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(); diff --git a/packages/stream_chat/lib/src/permission_type.dart b/packages/stream_chat/lib/src/permission_type.dart index b0e2e73f..931f2848 100644 --- a/packages/stream_chat/lib/src/permission_type.dart +++ b/packages/stream_chat/lib/src/permission_type.dart @@ -5,6 +5,9 @@ class PermissionType { /// 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 @@ -32,10 +35,19 @@ class PermissionType { /// User has RemoveOwnChannelMembership or UpdateChannelMembers permission static const String leaveChannel = 'leave-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 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..bf59991e 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -560,14 +560,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/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 8039ac47..5dfaf60b 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; @@ -377,6 +380,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'; 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 115069d9..fa5c3c04 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -191,7 +191,9 @@ class _MessageActionsModalState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (widget.showReplyMessage && + if (_userPermissions + .contains(PermissionType.quoteMessage) && + widget.showReplyMessage && widget.message.status == MessageSendingStatus.sent) _buildReplyButton(context), if ((widget.showThreadReplyMessage ?? @@ -207,7 +209,10 @@ class _MessageActionsModalState extends State { _isMyMessage && hasEditPermission) _buildEditMessage(context), if (widget.showCopyMessage) _buildCopyButton(context), - if (widget.showFlagButton) _buildFlagButton(context), + if (_userPermissions + .contains(PermissionType.flagMessage) && + widget.showFlagButton) + _buildFlagButton(context), if (widget.showPinButton ?? _userPermissions .contains(PermissionType.pinMessage)) @@ -680,9 +685,7 @@ class _MessageActionsModalState extends State { @override void didChangeDependencies() { final newStreamChannel = StreamChannel.of(context); - _userPermissions = - newStreamChannel.channel.state?.channelState.channel?.ownCapabilities ?? - []; + _userPermissions = newStreamChannel.channel.ownCapabilities; _isMyMessage = widget.message.user!.id == newStreamChannel.channel.client.state.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 e55fac19..6e4102af 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 @@ -468,112 +468,127 @@ 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 SizedBox( + height: 50, + child: FittedBox( + 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(); - }, - ), - ], - ), - ), + } + }, + 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(), - ], - ), + ), + 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, @@ -701,7 +716,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 && @@ -881,7 +898,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 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 4531b797..f70acd52 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1296,9 +1296,7 @@ class _MessageListViewState extends State { void didChangeDependencies() { final newStreamChannel = StreamChannel.of(context); _streamTheme = StreamChatTheme.of(context); - _userPermissions = - newStreamChannel.channel.state?.channelState.channel?.ownCapabilities ?? - []; + _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 4b85412c..37c8afff 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -45,13 +45,7 @@ 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 - .state - ?.channelState - .channel - ?.ownCapabilities ?? - []; + final _userPermissions = StreamChannel.of(context).channel.ownCapabilities; final hasReactionPermission = _userPermissions.contains(PermissionType.sendReaction); diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 60bacc29..d48c5099 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -1250,6 +1250,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,