* fix: fixed long system messages overflowing * fix: fixed long system messages overflowing * [UI-Kit] Refactor, add support for downloading attachments. Signed-off-by: Sahil Kumar <[email protected]> * fix: header casing * [UI-Kit] Disable attachment download dialog pop using back button Signed-off-by: Sahil Kumar <[email protected]> * stream_chat_flutter fix svg on web * [UI-Kit] Pop attachment download dialog in case of any error. Signed-off-by: Sahil Kumar <[email protected]> * restore svg * feat: add pr title linter * fix test * fix client headers * update system message design * update gh token env var * add scopes * expose systemMessageBuilder in messagelistview * fix(ui): fix button size * add dispatch workflow * add dispatch workflow * update dispatch acton * add runson * fix ref * fix ref * update secret * specify repo * fix: list scroll keyboard behaviour (#304) * fix: Keyboard now closes by clicking on space or scrolling. * fix: Exposed system and normal message tap builders * fmt: dartfmt * feat: Add support for messages filter in `MessageListView` and `MessageListCore` (#303) * [MessageListView, MessageListCore] Add support for message filter Signed-off-by: Sahil Kumar <[email protected]> * [UI-Kit -> Pubspec] Remove `stream_chat_flutter_core` relative import Signed-off-by: Sahil Kumar <[email protected]> * add relative import * add relative import * remove svg for web * remove badge logic on web * style: Update for team lint (#297) * Update for team lint * fix tests * remove pedantic and sort deps * remove jiffy from system message Co-authored-by: Salvatore Giordano <[email protected]> * add section highlighting sample app repo (#307) Co-authored-by: Salvatore Giordano <[email protected]> * add null check to debounce * fix messagelistview loading errors * remove core relative import * fix: fixes message newline issue (#308) * fix: minor * fix: generate image thumbnails and use transparent image as placeholder for cached network images (#310) * update cdn handling * fix image attachment with new cdn * fix(ui): Fix message input permission request crash (#311) Signed-off-by: Sahil Kumar <[email protected]> * fix(llc): update member presence and add skip_push to message (#314) * update member presence * add skip_push to message * fix tests * fix: Fixed shimmer overflow (#316) * fix dialogs * fix(llc): Fix attachment upload state uneven progress. (#315) * feat(llc): Add rate limiter functions. fix(llc): Fix attachment upload state uneven progress. Signed-off-by: Sahil Kumar <[email protected]> * feat(llc): Export `async.dart` and `rate_limit.dart` Signed-off-by: Sahil Kumar <[email protected]> * refactor(llc): Replace our `rate_limit` implementation with the dart port of js `lodash` to better handlw `cancel` and `flush` functions. Signed-off-by: Sahil Kumar <[email protected]> * fix(UI-Kit): Make android example run on real devices Signed-off-by: Sahil Kumar <[email protected]> * fix offline behaviour Co-authored-by: Salvatore Giordano <[email protected]> * feat: Introduce onAttachmentTap on messages attachment (#309) * introdce onAttachmentTap on message * correct indent * sdfsd Co-authored-by: Salvatore Giordano <[email protected]> * feat: added customization options in main widgets (#312) * polish channelpreview customization options * polish channelheader customization options * polish messageinput customization options * polish threadheader customization options * polish channellistheader customization options * show the parent message if no messages in thread * fix edit message * fix channelinfo textstyle * fix review * fix useravatar * add ontitle tap to thread header * add custom message actions * add borderradius and button builder * add ontap to messageinput custom send button * fix bottom sheet * add doc * use placeholder image * add doc * fix listtile density * remove subtitle from ChannelListHeaderTheme.copyWith * add InputDecoration.merge extension * fix: use shimmer in images * fix: remove notification badge logic from the sdk and move it to sample app * fix import * fix: Method being called during build (#317) * method call bug fix * dartfmt * fix: show error messages as system and keep them in the message input (#319) * fix: pre release (#324) * use share_plus plugin * bump dependency versions * fix ui for web * unfocus messageinput only on commands * fix default error for messagesearchlistview * fix thumbnail animation * fix lint * ignore example in linter * update changelogs * fix lint * fix lint * update stream_chat pana min * add doc Co-authored-by: Sahil Kumar <[email protected]> Co-authored-by: Deven Joshi <[email protected]> Co-authored-by: Neevash Ramdial (Nash) <[email protected]> Co-authored-by: Nelson Nunes <[email protected]>
312 lines
11 KiB
Dart
312 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:jiffy/jiffy.dart';
|
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
|
|
|
import '../stream_chat_flutter.dart';
|
|
import 'channel_name.dart';
|
|
import 'channel_unread_indicator.dart';
|
|
|
|
/// 
|
|
/// 
|
|
///
|
|
/// It shows the current [Channel] preview.
|
|
///
|
|
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
|
|
///
|
|
/// Usually you don't use this widget as it's the default channel preview used by [ChannelListView].
|
|
///
|
|
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
|
|
/// Modify it to change the widget appearance.
|
|
class ChannelPreview extends StatelessWidget {
|
|
/// Function called when tapping this widget
|
|
final void Function(Channel) onTap;
|
|
|
|
/// Function called when long pressing this widget
|
|
final void Function(Channel) onLongPress;
|
|
|
|
/// Channel displayed
|
|
final Channel channel;
|
|
|
|
/// The function called 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 [ChannelImage]
|
|
final Widget leading;
|
|
|
|
/// Widget rendering the trailing element, by default it shows the last message date
|
|
final Widget trailing;
|
|
|
|
/// Widget rendering the sending indicator, by default it uses the [SendingIndicator] widget
|
|
final Widget sendingIndicator;
|
|
|
|
ChannelPreview({
|
|
@required this.channel,
|
|
Key key,
|
|
this.onTap,
|
|
this.onLongPress,
|
|
this.onImageTap,
|
|
this.title,
|
|
this.subtitle,
|
|
this.leading,
|
|
this.sendingIndicator,
|
|
this.trailing,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme;
|
|
return StreamBuilder<bool>(
|
|
stream: channel.isMutedStream,
|
|
initialData: channel.isMuted,
|
|
builder: (context, snapshot) {
|
|
return Opacity(
|
|
opacity: snapshot.data ? 0.5 : 1,
|
|
child: ListTile(
|
|
visualDensity: VisualDensity.compact,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
),
|
|
onTap: () {
|
|
if (onTap != null) {
|
|
onTap(channel);
|
|
}
|
|
},
|
|
onLongPress: () {
|
|
if (onLongPress != null) {
|
|
onLongPress(channel);
|
|
}
|
|
},
|
|
leading: leading ??
|
|
ChannelImage(
|
|
onTap: onImageTap,
|
|
),
|
|
title: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: <Widget>[
|
|
Flexible(
|
|
child: title ??
|
|
ChannelName(
|
|
textStyle: channelPreviewTheme.title,
|
|
),
|
|
),
|
|
StreamBuilder<List<Member>>(
|
|
stream: channel.state.membersStream,
|
|
initialData: channel.state.members,
|
|
builder: (context, snapshot) {
|
|
if (!snapshot.hasData ||
|
|
snapshot.data.isEmpty ||
|
|
!snapshot.data.any((Member e) =>
|
|
e.user.id == channel.client.state.user.id)) {
|
|
return SizedBox();
|
|
}
|
|
return ChannelUnreadIndicator(
|
|
channel: channel,
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
subtitle: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: <Widget>[
|
|
Flexible(child: subtitle ?? _buildSubtitle(context)),
|
|
sendingIndicator ??
|
|
Builder(
|
|
builder: (context) {
|
|
final lastMessage = channel.state.messages.lastWhere(
|
|
(m) => !m.isDeleted && m.shadowed != true,
|
|
orElse: () => null,
|
|
);
|
|
if (lastMessage?.user?.id ==
|
|
StreamChat.of(context).user.id) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 4.0),
|
|
child: SendingIndicator(
|
|
message: lastMessage,
|
|
size: channelPreviewTheme.indicatorIconSize,
|
|
isMessageRead: channel.state.read
|
|
?.where((element) =>
|
|
element.user.id !=
|
|
channel.client.state.user.id)
|
|
?.where((element) => element.lastRead
|
|
.isAfter(lastMessage.createdAt))
|
|
?.isNotEmpty ==
|
|
true,
|
|
),
|
|
);
|
|
}
|
|
return SizedBox();
|
|
},
|
|
),
|
|
trailing ?? _buildDate(context),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget _buildDate(BuildContext context) {
|
|
return StreamBuilder<DateTime>(
|
|
stream: channel.lastMessageAtStream,
|
|
initialData: channel.lastMessageAt,
|
|
builder: (context, snapshot) {
|
|
if (!snapshot.hasData) {
|
|
return SizedBox();
|
|
}
|
|
final lastMessageAt = snapshot.data.toLocal();
|
|
|
|
String stringDate;
|
|
final now = DateTime.now();
|
|
|
|
var startOfDay = DateTime(now.year, now.month, now.day);
|
|
|
|
if (lastMessageAt.millisecondsSinceEpoch >=
|
|
startOfDay.millisecondsSinceEpoch) {
|
|
stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm');
|
|
} else if (lastMessageAt.millisecondsSinceEpoch >=
|
|
startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) {
|
|
stringDate = 'Yesterday';
|
|
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
|
|
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
|
|
} else {
|
|
stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy');
|
|
}
|
|
|
|
return Text(
|
|
stringDate,
|
|
style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildSubtitle(BuildContext context) {
|
|
if (channel.isMuted) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: <Widget>[
|
|
StreamSvgIcon.mute(
|
|
size: 16,
|
|
),
|
|
Text(
|
|
' Channel is muted',
|
|
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
return TypingIndicator(
|
|
channel: channel,
|
|
alternativeWidget: _buildLastMessage(context),
|
|
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
|
|
);
|
|
}
|
|
|
|
Widget _buildLastMessage(BuildContext context) {
|
|
return StreamBuilder<List<Message>>(
|
|
stream: channel.state.messagesStream,
|
|
initialData: channel.state.messages,
|
|
builder: (context, snapshot) {
|
|
final lastMessage = snapshot.data?.lastWhere(
|
|
(m) => m.shadowed != true && !m.isDeleted,
|
|
orElse: () => null);
|
|
if (lastMessage == null) {
|
|
return SizedBox();
|
|
}
|
|
|
|
var text = lastMessage.text;
|
|
if (lastMessage.attachments != null) {
|
|
final parts = <String>[
|
|
...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'} , ';
|
|
}).where((e) => e != null),
|
|
lastMessage.text ?? '',
|
|
];
|
|
|
|
text = parts.join(' ');
|
|
}
|
|
|
|
return Text.rich(
|
|
_getDisplayText(
|
|
text,
|
|
lastMessage.mentionedUsers,
|
|
lastMessage.attachments,
|
|
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
|
color: StreamChatTheme.of(context)
|
|
.channelPreviewTheme
|
|
.subtitle
|
|
.color,
|
|
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
|
|
? FontStyle.italic
|
|
: FontStyle.normal),
|
|
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
|
color: StreamChatTheme.of(context)
|
|
.channelPreviewTheme
|
|
.subtitle
|
|
.color,
|
|
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
|
|
? FontStyle.italic
|
|
: FontStyle.normal,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
TextSpan _getDisplayText(
|
|
String text,
|
|
List<User> mentions,
|
|
List<Attachment> attachments,
|
|
TextStyle normalTextStyle,
|
|
TextStyle mentionsTextStyle) {
|
|
var textList = text.split(' ');
|
|
var resList = <TextSpan>[];
|
|
for (var e in textList) {
|
|
if (mentions != null &&
|
|
mentions.isNotEmpty &&
|
|
mentions.any((element) => '@${element.name}' == e)) {
|
|
resList.add(TextSpan(
|
|
text: '$e ',
|
|
style: mentionsTextStyle,
|
|
));
|
|
} else if (attachments != null &&
|
|
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);
|
|
}
|
|
}
|