// ignore_for_file: deprecated_member_use_from_same_package import 'package:collection/collection.dart' show IterableExtension, ListEquality; import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/context_menu_items/stream_chat_context_menu_item.dart'; import 'package:stream_chat_flutter/src/dialogs/dialogs.dart'; import 'package:stream_chat_flutter/src/message_widget/sending_indicator_builder.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@template channelPreview} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview_paint.png) /// /// Shows a preview for the current [Channel]. /// /// Uses a [StreamBuilder] to render the channel information image as soon as /// it updates. /// /// It is not recommended to use this widget directly as it is the /// default channel preview widget used by [ChannelListView]. /// /// The UI is rendered based on the first ancestor of type [StreamChatTheme]. /// Modify it to change the widget's appearance. /// {@endtemplate} @Deprecated('Use StreamChannelListTile instead.') class ChannelPreview extends StatelessWidget { /// {@macro channelPreview} const ChannelPreview({ required this.channel, super.key, this.onTap, this.onLongPress, this.onViewInfoTap, this.onImageTap, this.title, this.subtitle, this.leading, this.sendingIndicator, this.trailing, }); /// The action to perform when this widget is tapped or clicked. final void Function(Channel)? onTap; /// The action to perform when this widget is long pressed. final void Function(Channel)? onLongPress; /// The action to perform when 'View Info' is tapped or clicked. final ViewInfoCallback? onViewInfoTap; /// The [Channel] being previewed. final Channel channel; /// The action to perform when the image is tapped final VoidCallback? onImageTap; /// Widget rendering the title final Widget? title; /// Widget rendering the subtitle final Widget? subtitle; /// Widget rendering the leading element. By default it shows the /// [StreamChannelAvatar]. final Widget? leading; /// Widget rendering the trailing element. By default it shows the date of /// the last message. final Widget? trailing; /// Widget rendering the sending indicator. By default it uses the /// [StreamSendingIndicator] widget. final Widget? sendingIndicator; @override Widget build(BuildContext context) { final channelPreviewTheme = StreamChannelPreviewTheme.of(context); final streamChatState = StreamChat.of(context); final streamChatTheme = StreamChatTheme.of(context); return BetterStreamBuilder( stream: channel.isMutedStream, initialData: channel.isMuted, builder: (context, data) => AnimatedOpacity( opacity: data ? 0.5 : 1, duration: const Duration(milliseconds: 300), child: ContextMenuArea( verticalPadding: 0, builder: (context) => [ StreamChatContextMenuItem( leading: StreamSvgIcon.user( color: Colors.grey, ), title: Text(context.translations.viewInfoLabel), onClick: () { Navigator.of(context, rootNavigator: true).pop(); if (onViewInfoTap != null) { onViewInfoTap?.call(channel); } else { showDialog( context: context, builder: (_) => ChannelInfoDialog( channel: channel, ), ); } }, ), StreamChatContextMenuItem( leading: StreamSvgIcon.mute( color: Colors.grey, ), title: channel.isGroup ? Text( context.translations .toggleMuteUnmuteGroupText(isMuted: channel.isMuted), ) : Text( context.translations .toggleMuteUnmuteUserText(isMuted: channel.isMuted), ), onClick: () async { Navigator.of(context, rootNavigator: true).pop(); showDialog( context: context, builder: (_) => ConfirmationDialog( titleText: channel.isGroup ? context.translations .toggleMuteUnmuteGroupText(isMuted: channel.isMuted) : context.translations .toggleMuteUnmuteUserText(isMuted: channel.isMuted), promptText: channel.isGroup ? context.translations.toggleMuteUnmuteGroupQuestion( isMuted: channel.isMuted, ) : context.translations.toggleMuteUnmuteUserQuestion( isMuted: channel.isMuted, ), affirmativeText: context.translations .toggleMuteUnmuteAction(isMuted: channel.isMuted), onConfirmation: () async { try { if (channel.isMuted) { await channel.unmute(); } else { await channel.mute(); } } catch (e) { showDialog( context: context, builder: (_) => MessageDialog( messageText: e.toString(), ), ); } }, ), ); }, ), if (channel.isGroup) StreamChatContextMenuItem( leading: StreamSvgIcon.userRemove( color: Colors.red, ), title: Text( context.translations.leaveGroupLabel, style: const TextStyle( color: Colors.red, ), ), onClick: () { Navigator.of(context, rootNavigator: true).pop(); showDialog( context: context, builder: (_) => ConfirmationDialog( titleText: context.translations.leaveGroupLabel, promptText: context.translations.leaveConversationQuestion, affirmativeText: context.translations.leaveLabel, onConfirmation: () async { final userAsMember = channel.state?.members.firstWhere( (e) => e.user?.id == StreamChat.of(context).currentUser?.id, ); try { await channel.removeMembers([userAsMember!.user!.id]); } catch (e) { showDialog( context: context, builder: (_) => MessageDialog( messageText: e.toString(), ), ); } }, ), ); }, ), if (!channel.isGroup) StreamChatContextMenuItem( leading: StreamSvgIcon.delete( color: Colors.red, ), title: Text( context.translations.deleteConversationLabel, style: const TextStyle( color: Colors.red, ), ), onClick: () { Navigator.of(context, rootNavigator: true).pop(); showDialog( context: context, builder: (_) => ConfirmationDialog( titleText: context.translations.deleteConversationLabel, promptText: context.translations.deleteConversationQuestion, affirmativeText: context.translations.deleteLabel, onConfirmation: () async { try { await channel.delete(); } catch (e) { showDialog( context: context, builder: (_) => MessageDialog( messageText: e.toString(), ), ); } }, ), ); }, ), ], child: ListTile( visualDensity: VisualDensity.compact, contentPadding: const EdgeInsets.symmetric( horizontal: 8, ), onTap: () => onTap?.call(channel), onLongPress: () => onLongPress?.call(channel), leading: leading ?? StreamChannelAvatar( onTap: onImageTap, channel: channel, ), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: title ?? ChannelName( textStyle: channelPreviewTheme.titleStyle, ), ), BetterStreamBuilder>( stream: channel.state?.membersStream, initialData: channel.state?.members, comparator: const ListEquality().equals, builder: (context, members) { if (members.isEmpty || !members.any((Member e) => e.user!.id == channel.client.state.currentUser?.id)) { return const SizedBox(); } return StreamUnreadIndicator( cid: channel.cid, ); }, ), ], ), subtitle: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: subtitle ?? _Subtitle(channel: channel)), sendingIndicator ?? Builder( builder: (context) { final lastMessage = channel.state?.messages.lastWhereOrNull( (m) => !m.isDeleted && !m.shadowed, ); if (lastMessage?.user?.id == streamChatState.currentUser?.id) { return Padding( padding: const EdgeInsets.only(right: 4), child: BetterStreamBuilder>( stream: channel.state?.readStream, initialData: channel.state?.read, builder: (context, data) { final hasNonUrlAttachments = lastMessage! .attachments .where((it) => it.titleLink == null || it.type == 'giphy') .isNotEmpty; return SendingIndicatorBuilder( messageTheme: streamChatTheme.ownMessageTheme, message: lastMessage, hasNonUrlAttachments: hasNonUrlAttachments, streamChat: streamChatState, streamChatTheme: streamChatTheme, channel: channel, ); }, ), ); } return const SizedBox(); }, ), trailing ?? _Date(channel: channel), ], ), ), ), ), ); } } class _Date extends StatelessWidget { const _Date({ required this.channel, }); final Channel channel; @override Widget build(BuildContext context) { return BetterStreamBuilder( stream: channel.lastMessageAtStream, initialData: channel.lastMessageAt, builder: (context, data) { final lastMessageAt = data.toLocal(); String stringDate; final now = DateTime.now(); final startOfDay = DateTime(now.year, now.month, now.day); if (lastMessageAt.millisecondsSinceEpoch >= startOfDay.millisecondsSinceEpoch) { stringDate = Jiffy.parseFromDateTime(lastMessageAt.toLocal()).jm; } else if (lastMessageAt.millisecondsSinceEpoch >= startOfDay .subtract(const Duration(days: 1)) .millisecondsSinceEpoch) { stringDate = context.translations.yesterdayLabel; } else if (startOfDay.difference(lastMessageAt).inDays < 7) { stringDate = Jiffy.parseFromDateTime(lastMessageAt.toLocal()).EEEE; } else { stringDate = Jiffy.parseFromDateTime(lastMessageAt.toLocal()).yMd; } return Text( stringDate, style: StreamChannelPreviewTheme.of(context).lastMessageAtStyle, ); }, ); } } class _Subtitle extends StatelessWidget { const _Subtitle({ required this.channel, }); final Channel channel; @override Widget build(BuildContext context) { final channelPreviewTheme = StreamChannelPreviewTheme.of(context); if (channel.isMuted) { return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ StreamSvgIcon.mute( size: 16, ), Text( ' ${context.translations.channelIsMutedText}', style: channelPreviewTheme.subtitleStyle, ), ], ); } return StreamTypingIndicator( channel: channel, alternativeWidget: _LastMessage( channel: channel, ), style: channelPreviewTheme.subtitleStyle, ); } } class _LastMessage extends StatelessWidget { const _LastMessage({ required this.channel, }); final Channel channel; @override Widget build(BuildContext context) { return Align( alignment: Alignment.centerLeft, child: BetterStreamBuilder>( stream: channel.state!.messagesStream, initialData: channel.state!.messages, builder: (context, data) { final lastMessage = data.lastWhereOrNull((m) => !m.shadowed && !m.isDeleted); if (lastMessage == null) { return const SizedBox(); } var text = lastMessage.text; final parts = [ ...lastMessage.attachments.map((e) { if (e.type == 'image') { return '📷'; } else if (e.type == 'video') { return '🎬'; } else if (e.type == 'giphy') { return '[GIF]'; } return e == lastMessage.attachments.last ? (e.title ?? 'File') : '${e.title ?? 'File'} , '; }), lastMessage.text ?? '', ]; text = parts.join(' '); final channelPreviewTheme = StreamChannelPreviewTheme.of(context); return Text.rich( _getDisplayText( text, lastMessage.mentionedUsers, lastMessage.attachments, channelPreviewTheme.subtitleStyle?.copyWith( color: channelPreviewTheme.subtitleStyle?.color, fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal, ), channelPreviewTheme.subtitleStyle?.copyWith( color: channelPreviewTheme.subtitleStyle?.color, fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal, fontWeight: FontWeight.bold, ), ), maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.start, ); }, ), ); } TextSpan _getDisplayText( String text, List mentions, List attachments, TextStyle? normalTextStyle, TextStyle? mentionsTextStyle, ) { final textList = text.split(' '); final resList = []; for (final e in textList) { if (mentions.isNotEmpty && mentions.any((element) => '@${element.name}' == e)) { resList.add(TextSpan( text: '$e ', style: mentionsTextStyle, )); } else if (attachments.isNotEmpty && attachments .where((e) => e.title != null) .any((element) => element.title == e)) { resList.add(TextSpan( text: '$e ', style: normalTextStyle?.copyWith(fontStyle: FontStyle.italic), )); } else { resList.add(TextSpan( text: e == textList.last ? e : '$e ', style: normalTextStyle, )); } } return TextSpan(children: resList); } }