diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index 212996b3..afb92711 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -351,9 +351,13 @@ class Channel { } /// Send a [message] to this channel. + /// 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(Message message) async { + Future sendMessage( + Message message, { + bool skipPush = false, + }) async { _checkInitialized(); // Cancelling previous completer in case it's called again in the process // Eg. Updating the message while the previous call is in progress. @@ -396,7 +400,12 @@ class Channel { message = await attachmentsUploadCompleter.future; } - final response = await _client.sendMessage(message, id!, type); + final response = await _client.sendMessage( + message, + id!, + type, + skipPush: skipPush, + ); state!.addMessage(response.message); return response; } catch (error) { diff --git a/packages/stream_chat/lib/src/client.dart b/packages/stream_chat/lib/src/client.dart index 0d584c92..de199b84 100644 --- a/packages/stream_chat/lib/src/client.dart +++ b/packages/stream_chat/lib/src/client.dart @@ -1329,11 +1329,15 @@ class StreamChatClient { Future sendMessage( Message message, String channelId, - String channelType, - ) async { + String channelType, { + bool skipPush = false, + }) async { final response = await post( '/channels/$channelType/$channelId/message', - data: {'message': message.toJson()}, + data: { + 'message': message.toJson(), + 'skip_push': skipPush, + }, ); return decode(response.data, SendMessageResponse.fromJson); } diff --git a/packages/stream_chat/lib/src/models/message.dart b/packages/stream_chat/lib/src/models/message.dart index f928ce3a..6df17497 100644 --- a/packages/stream_chat/lib/src/models/message.dart +++ b/packages/stream_chat/lib/src/models/message.dart @@ -73,7 +73,6 @@ class Message extends Equatable { this.extraData = const {}, this.deletedAt, this.status = MessageSendingStatus.sent, - this.skipPush = false, }) : id = id ?? const Uuid().v4(), pinExpires = pinExpires?.toUtc(), createdAt = createdAt ?? DateTime.now(), @@ -158,10 +157,6 @@ class Message extends Equatable { @JsonKey(defaultValue: false) final bool silent; - /// If true the message will not send a push notification - @JsonKey(defaultValue: false) - final bool skipPush; - /// If true the message is shadowed @JsonKey( includeIfNull: false, @@ -253,7 +248,6 @@ class Message extends Equatable { 'pinned_at', 'pin_expires', 'pinned_by', - 'skip_push', ]; /// Serialize to json @@ -291,7 +285,6 @@ class Message extends Equatable { User? pinnedBy, Map? extraData, MessageSendingStatus? status, - bool? skipPush, }) { assert(() { if (pinExpires is! DateTime && @@ -331,7 +324,6 @@ class Message extends Equatable { pinnedBy: pinnedBy ?? this.pinnedBy, pinExpires: pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?, - skipPush: skipPush ?? this.skipPush, ); } @@ -398,7 +390,6 @@ class Message extends Equatable { pinnedBy, extraData, status, - skipPush, ]; } diff --git a/packages/stream_chat/lib/src/models/message.g.dart b/packages/stream_chat/lib/src/models/message.g.dart index ca094a83..8792035a 100644 --- a/packages/stream_chat/lib/src/models/message.g.dart +++ b/packages/stream_chat/lib/src/models/message.g.dart @@ -67,7 +67,6 @@ Message _$MessageFromJson(Map json) { deletedAt: json['deleted_at'] == null ? null : DateTime.parse(json['deleted_at'] as String), - skipPush: json['skip_push'] as bool? ?? false, ); } @@ -97,7 +96,6 @@ Map _$MessageToJson(Message instance) { writeNotNull('thread_participants', readonly(instance.threadParticipants)); val['show_in_channel'] = instance.showInChannel; val['silent'] = instance.silent; - val['skip_push'] = instance.skipPush; writeNotNull('shadowed', readonly(instance.shadowed)); writeNotNull('command', readonly(instance.command)); writeNotNull('created_at', readonly(instance.createdAt)); diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 6d15f4ef..18a1a3e7 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -52,7 +52,10 @@ void main() { when( () => mockDio.post( '/channels/messaging/testid/message', - data: {'message': message.toJson()}, + data: { + 'message': message.toJson(), + 'skip_push': false, + }, ), ).thenAnswer( (_) async => Response( @@ -67,6 +70,7 @@ void main() { verify(() => mockDio.post('/channels/messaging/testid/message', data: { 'message': message.toJson(), + 'skip_push': false, })).called(1); }); diff --git a/packages/stream_chat/test/src/models/channel_state_test.dart b/packages/stream_chat/test/src/models/channel_state_test.dart index a93166b4..df078583 100644 --- a/packages/stream_chat/test/src/models/channel_state_test.dart +++ b/packages/stream_chat/test/src/models/channel_state_test.dart @@ -902,7 +902,6 @@ void main() { "show_in_channel": null, "mentioned_users": [], "status": "SENT", - "skip_push": false, "silent": false, "pinned": false, "pinned_at": null, @@ -919,7 +918,6 @@ void main() { "show_in_channel": null, "mentioned_users": [], "status": "SENT", - "skip_push": false, "silent": false, "pinned": false, "pinned_at": null, @@ -929,7 +927,6 @@ void main() { { "id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0", "text": "test message", - "skip_push": false, "attachments": [], "parent_id": null, "quoted_message": null, @@ -952,7 +949,6 @@ void main() { "quoted_message_id": null, "show_in_channel": null, "mentioned_users": [], - "skip_push": false, "status": "SENT", "silent": false, "pinned": false, @@ -964,7 +960,6 @@ void main() { "id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe", "text": "test", "attachments": [], - "skip_push": false, "parent_id": null, "quoted_message": null, "quoted_message_id": null, @@ -982,7 +977,6 @@ void main() { "text": "hi", "attachments": [], "parent_id": null, - "skip_push": false, "quoted_message": null, "quoted_message_id": null, "show_in_channel": null, @@ -1000,7 +994,6 @@ void main() { "attachments": [], "parent_id": null, "quoted_message": null, - "skip_push": false, "quoted_message_id": null, "show_in_channel": null, "mentioned_users": [], @@ -1023,7 +1016,6 @@ void main() { "status": "SENT", "silent": false, "pinned": false, - "skip_push": false, "pinned_at": null, "pin_expires": null, "pinned_by": null @@ -1041,7 +1033,6 @@ void main() { "silent": false, "pinned": false, "pinned_at": null, - "skip_push": false, "pin_expires": null, "pinned_by": null }, @@ -1053,7 +1044,6 @@ void main() { "quoted_message": null, "quoted_message_id": null, "show_in_channel": null, - "skip_push": false, "mentioned_users": [], "status": "SENT", "silent": false, @@ -1071,7 +1061,6 @@ void main() { "quoted_message_id": null, "show_in_channel": null, "mentioned_users": [], - "skip_push": false, "status": "SENT", "silent": false, "pinned": false, @@ -1090,7 +1079,6 @@ void main() { "mentioned_users": [], "status": "SENT", "silent": false, - "skip_push": false, "pinned": false, "pinned_at": null, "pin_expires": null, @@ -1100,7 +1088,6 @@ void main() { "id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055", "text": "what are you doing?", "attachments": [], - "skip_push": false, "parent_id": null, "quoted_message": null, "quoted_message_id": null, @@ -1118,7 +1105,6 @@ void main() { "text": "👍", "attachments": [], "parent_id": null, - "skip_push": false, "quoted_message": null, "quoted_message_id": null, "show_in_channel": null, @@ -1134,7 +1120,6 @@ void main() { "id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21", "text": "sdasas", "attachments": [], - "skip_push": false, "parent_id": null, "quoted_message": null, "quoted_message_id": null, @@ -1155,7 +1140,6 @@ void main() { "quoted_message": null, "quoted_message_id": null, "show_in_channel": null, - "skip_push": false, "mentioned_users": [], "status": "SENT", "silent": false, @@ -1168,7 +1152,6 @@ void main() { "id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d", "text": "nhisagdhsadz", "attachments": [], - "skip_push": false, "parent_id": null, "quoted_message": null, "quoted_message_id": null, @@ -1187,7 +1170,6 @@ void main() { "attachments": [], "parent_id": null, "quoted_message": null, - "skip_push": false, "quoted_message_id": null, "show_in_channel": null, "mentioned_users": [], @@ -1204,7 +1186,6 @@ void main() { "attachments": [], "parent_id": null, "quoted_message": null, - "skip_push": false, "quoted_message_id": null, "show_in_channel": null, "mentioned_users": [], @@ -1212,7 +1193,6 @@ void main() { "silent": false, "pinned": false, "pinned_at": null, - "skip_push": false, "pin_expires": null, "pinned_by": null }, @@ -1229,7 +1209,6 @@ void main() { "silent": false, "pinned": false, "pinned_at": null, - "skip_push": false, "pin_expires": null, "pinned_by": null }, @@ -1246,7 +1225,6 @@ void main() { "silent": false, "pinned": false, "pinned_at": null, - "skip_push": false, "pin_expires": null, "pinned_by": null }, @@ -1263,7 +1241,6 @@ void main() { "silent": false, "pinned": false, "pinned_at": null, - "skip_push": false, "pin_expires": null, "pinned_by": null }, @@ -1280,7 +1257,6 @@ void main() { "silent": false, "pinned": false, "pinned_at": null, - "skip_push": false, "pin_expires": null, "pinned_by": null }, @@ -1297,7 +1273,6 @@ void main() { "silent": false, "pinned": false, "pinned_at": null, - "skip_push": false, "pin_expires": null, "pinned_by": null }, @@ -1314,7 +1289,6 @@ void main() { "silent": false, "pinned": false, "pinned_at": null, - "skip_push": false, "pin_expires": null, "pinned_by": null } diff --git a/packages/stream_chat/test/src/models/message_test.dart b/packages/stream_chat/test/src/models/message_test.dart index 1219e137..84ae616e 100644 --- a/packages/stream_chat/test/src/models/message_test.dart +++ b/packages/stream_chat/test/src/models/message_test.dart @@ -133,7 +133,6 @@ void main() { "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", "silent": false, - "skip_push": false, "attachments": [ { "type": "video", 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 766fe4ba..911e0a41 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -12,6 +12,9 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Callback called when tapping on a channel typedef ChannelTapCallback = void Function(Channel, Widget?); +/// Callback called when tapping on a channel +typedef ChannelInfoCallback = void Function(Channel); + /// Builder used to create a custom [ChannelPreview] from a [Channel] typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); @@ -78,6 +81,9 @@ class ChannelListView extends StatefulWidget { this.emptyBuilder, this.loadingBuilder, this.listBuilder, + this.onMoreDetailsPressed, + this.onDeletePressed, + this.swipeActions, }) : super(key: key); /// If true a default swipe to action behaviour will be added to this widget @@ -158,6 +164,15 @@ class ChannelListView extends StatefulWidget { /// The builder used when the channel list is empty. final WidgetBuilder? emptyBuilder; + /// Callback used when the more details slidable option is pressed + final ChannelInfoCallback? onMoreDetailsPressed; + + /// Callback used when the delete slidable option is pressed + final ChannelInfoCallback? onDeletePressed; + + /// List of actions for slidable + final List? swipeActions; + @override _ChannelListViewState createState() => _ChannelListViewState(); } @@ -465,61 +480,79 @@ class _ChannelListViewState extends State { enabled: widget.swipeToAction, actionPane: const SlidableBehindActionPane(), actionExtentRatio: 0.12, - secondaryActions: [ - IconSlideAction( - color: backgroundColor, - icon: Icons.more_horiz, - onTap: () { - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), - context: context, - builder: (context) => StreamChannel( - channel: channel, - child: ChannelBottomSheet( - onViewInfoTap: () { - widget.onViewInfoTap?.call(channel); - }, - ), - ), - ); - }, - ), - if ([ - 'admin', - 'owner', - ].contains(channel.state!.members - .firstWhereOrNull( - (m) => m.userId == channel.client.state.user?.id) - ?.role)) - IconSlideAction( - color: backgroundColor, - iconWidget: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentRed, + secondaryActions: widget.swipeActions + ?.map((e) => IconSlideAction( + color: e.color, + iconWidget: e.iconWidget, + onTap: () { + e.onTap?.call(channel); + }, + )) + .toList() ?? + [ + IconSlideAction( + color: backgroundColor, + icon: Icons.more_horiz, + onTap: widget.onMoreDetailsPressed != null + ? () { + widget.onMoreDetailsPressed!(channel); + } + : () { + showModalBottomSheet( + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + context: context, + builder: (context) => StreamChannel( + channel: channel, + child: ChannelBottomSheet( + onViewInfoTap: () { + widget.onViewInfoTap?.call(channel); + }, + ), + ), + ); + }, ), - onTap: () async { - final res = await showConfirmationDialog( - context, - title: 'Delete Conversation', - okText: 'DELETE', - question: - 'Are you sure you want to delete this conversation?', - cancelText: 'CANCEL', - icon: StreamSvgIcon.delete( + if ([ + 'admin', + 'owner', + ].contains(channel.state!.members + .firstWhereOrNull( + (m) => m.userId == channel.client.state.user?.id) + ?.role)) + IconSlideAction( + color: backgroundColor, + iconWidget: StreamSvgIcon.delete( color: chatThemeData.colorTheme.accentRed, ), - ); - if (res == true) { - await channel.delete(); - } - }, - ), - ], + onTap: widget.onDeletePressed != null + ? () { + widget.onDeletePressed!(channel); + } + : () async { + final res = await showConfirmationDialog( + context, + title: 'Delete Conversation', + okText: 'DELETE', + question: + // ignore: lines_longer_than_80_chars + 'Are you sure you want to delete this conversation?', + cancelText: 'CANCEL', + icon: StreamSvgIcon.delete( + color: chatThemeData.colorTheme.accentRed, + ), + ); + if (res == true) { + await channel.delete(); + } + }, + ), + ], child: Container( color: chatThemeData.colorTheme.whiteSnow, child: widget.channelPreviewBuilder?.call(context, channel) ?? @@ -640,3 +673,22 @@ class _ChannelListViewState extends State { ); } } + +/// Class for slidable action +class SwipeAction { + /// Constructor for creating [SwipeAction] + SwipeAction({ + this.color, + required this.iconWidget, + this.onTap, + }); + + /// Background color of action + Color? color; + + /// Widget to display as icon + Widget iconWidget; + + /// Callback when icon is tapped + ChannelInfoCallback? onTap; +} 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 7f2a2430..1da4fcea 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -37,6 +37,7 @@ class MessageActionsModal extends StatefulWidget { this.customActions = const [], this.attachmentBorderRadiusGeometry, this.onCopyTap, + this.textBuilder, }) : super(key: key); /// Builder for edit message @@ -105,6 +106,9 @@ class MessageActionsModal extends StatefulWidget { /// List of custom actions final List customActions; + /// Customize the MessageWidget textBuilder + final Widget Function(BuildContext context, Message message)? textBuilder; + @override _MessageActionsModalState createState() => _MessageActionsModalState(); } @@ -231,6 +235,7 @@ class _MessageActionsModalState extends State { shape: widget.messageShape, attachmentShape: widget.attachmentShape, showPinHighlight: false, + textBuilder: widget.textBuilder, ), ), const SizedBox(height: 8), 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 f323b763..dfab3630 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -156,9 +156,10 @@ class MessageListView extends StatefulWidget { this.onMessageTap, this.onSystemMessageTap, this.onAttachmentTap, - this.textBuilder, this.onLinkTap, this.pinPermissions = const [], + this.textBuilder, + this.usernameBuilder, }) : super(key: key); /// Function used to build a custom message widget @@ -260,7 +261,10 @@ class MessageListView extends StatefulWidget { final void Function(Message message, Attachment attachment)? onAttachmentTap; /// Customize the MessageWidget textBuilder - final void Function(BuildContext context, Message message)? textBuilder; + final Widget Function(BuildContext context, Message message)? textBuilder; + + /// Customize the MessageWidget usernameBuilder + final Widget Function(BuildContext context, Message message)? usernameBuilder; /// Callback for when link is tapped final void Function(String link)? onLinkTap; @@ -865,8 +869,8 @@ class _MessageListViewState extends State { } FocusScope.of(context).unfocus(); }, - textBuilder: - widget.textBuilder as Widget Function(BuildContext, Message)?, + textBuilder: widget.textBuilder, + usernameBuilder: widget.usernameBuilder, onLinkTap: widget.onLinkTap, showPinButton: widget.pinPermissions.contains(currentUserMember.role), ); @@ -1070,8 +1074,8 @@ class _MessageListViewState extends State { FocusScope.of(context).unfocus(); }, onAttachmentTap: widget.onAttachmentTap, - textBuilder: - widget.textBuilder as Widget Function(BuildContext, Message)?, + textBuilder: widget.textBuilder, + usernameBuilder: widget.usernameBuilder, onLinkTap: widget.onLinkTap, showPinButton: widget.pinPermissions.contains(currentUserMember.role), ); 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 23522508..9d78e3d9 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -23,6 +23,7 @@ class MessageReactionsModal extends StatelessWidget { this.showUserAvatar = DisplayWidget.show, this.onUserAvatarTap, this.attachmentBorderRadiusGeometry, + this.textBuilder, }) : super(key: key); /// Message to display reactions of @@ -52,6 +53,9 @@ class MessageReactionsModal extends StatelessWidget { /// [BorderRadius] to apply to attachments final BorderRadius? attachmentBorderRadiusGeometry; + /// Customize the MessageWidget textBuilder + final Widget Function(BuildContext context, Message message)? textBuilder; + @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; @@ -157,6 +161,7 @@ class MessageReactionsModal extends StatelessWidget { ), showReactionPickerIndicator: showReactions && (message.status == MessageSendingStatus.sent), + textBuilder: textBuilder, showPinHighlight: false, ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index f2f08d82..f1b5428d 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -104,6 +104,7 @@ class MessageWidget extends StatefulWidget { this.onQuotedMessageTap, this.customActions = const [], this.onAttachmentTap, + this.usernameBuilder, }) : attachmentBuilders = { 'image': (context, message, attachments) { final border = RoundedRectangleBorder( @@ -267,6 +268,9 @@ class MessageWidget extends StatefulWidget { /// Widget builder for building text final Widget Function(BuildContext, Message)? textBuilder; + /// Widget builder for building username + final Widget Function(BuildContext, Message)? usernameBuilder; + /// Function called on long press final void Function(BuildContext, Message)? onMessageActions; @@ -751,14 +755,7 @@ class _MessageWidgetState extends State child: Text(msg, style: widget.messageTheme.replies), ), ], - if (showUsername) - Text( - widget.message.user!.name, - maxLines: 1, - key: usernameKey, - style: widget.messageTheme.messageAuthor, - overflow: TextOverflow.ellipsis, - ), + if (showUsername) _buildUsername(usernameKey), if (showTimeStamp) Text( Jiffy(widget.message.createdAt.toLocal()).jm, @@ -821,6 +818,19 @@ class _MessageWidgetState extends State ); } + Widget _buildUsername(Key usernameKey) { + if (widget.usernameBuilder != null) { + return widget.usernameBuilder!(context, widget.message); + } + return Text( + widget.message.user!.name, + maxLines: 1, + key: usernameKey, + style: widget.messageTheme.messageAuthor, + overflow: TextOverflow.ellipsis, + ); + } + Widget _buildUrlAttachment() { final urlAttachment = widget.message.attachments .firstWhere((element) => element.ogScrapeUrl != null); @@ -912,6 +922,7 @@ class _MessageWidgetState extends State builder: (context) => StreamChannel( channel: channel, child: MessageActionsModal( + textBuilder: widget.textBuilder, onCopyTap: (message) => Clipboard.setData(ClipboardData(text: message.text)), attachmentBorderRadiusGeometry: @@ -962,6 +973,7 @@ class _MessageWidgetState extends State builder: (context) => StreamChannel( channel: channel, child: MessageReactionsModal( + textBuilder: widget.textBuilder, attachmentBorderRadiusGeometry: widget.attachmentBorderRadiusGeometry as BorderRadius?, showUserAvatar: