refactor message widget

This commit is contained in:
Salvatore Giordano
2020-05-11 16:20:03 +02:00
parent daf0989f2d
commit 5b29057ccb
17 changed files with 1189 additions and 1184 deletions
+61 -61
View File
@@ -1,61 +1,61 @@
include: package:pedantic/analysis_options.yaml #include: package:pedantic/analysis_options.yaml
#
analyzer: #analyzer:
exclude: # exclude:
- lib/**/*.g.dart # - lib/**/*.g.dart
- example/* # - example/*
#
linter: #linter:
rules: # rules:
# these rules are documented on and in the same order as # # these rules are documented on and in the same order as
# the Dart Lint rules page to make maintenance easier # # the Dart Lint rules page to make maintenance easier
# https://github.com/dart-lang/linter/blob/master/example/all.yaml # # https://github.com/dart-lang/linter/blob/master/example/all.yaml
# - always_declare_return_types # # - always_declare_return_types
# - always_specify_types # # - always_specify_types
# - annotate_overrides # # - annotate_overrides
# - avoid_as # # - avoid_as
- avoid_empty_else # - avoid_empty_else
- avoid_init_to_null # - avoid_init_to_null
- avoid_return_types_on_setters # - avoid_return_types_on_setters
- avoid_web_libraries_in_flutter # - avoid_web_libraries_in_flutter
- await_only_futures # - await_only_futures
- camel_case_types # - camel_case_types
- cancel_subscriptions # - cancel_subscriptions
- close_sinks # - close_sinks
# - comment_references # we do not presume as to what people want to reference in their dartdocs # # - comment_references # we do not presume as to what people want to reference in their dartdocs
# - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 # # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204
- control_flow_in_finally # - control_flow_in_finally
- empty_constructor_bodies # - empty_constructor_bodies
- empty_statements # - empty_statements
- hash_and_equals # - hash_and_equals
- implementation_imports # - implementation_imports
# - invariant_booleans # # - invariant_booleans
# - iterable_contains_unrelated_type # # - iterable_contains_unrelated_type
- library_names # - library_names
# - library_prefixes # # - library_prefixes
# - list_remove_unrelated_type # # - list_remove_unrelated_type
# - literal_only_boolean_expressions # # - literal_only_boolean_expressions
- non_constant_identifier_names # - non_constant_identifier_names
# - one_member_abstracts # # - one_member_abstracts
# - only_throw_errors # # - only_throw_errors
# - overridden_fields # # - overridden_fields
# - package_api_docs ## - package_api_docs
- package_names # - package_names
- package_prefixed_library_names # - package_prefixed_library_names
- prefer_is_not_empty # - prefer_is_not_empty
# - prefer_mixin # https://github.com/dart-lang/language/issues/32 # # - prefer_mixin # https://github.com/dart-lang/language/issues/32
- public_member_api_docs # - public_member_api_docs
- slash_for_doc_comments # - slash_for_doc_comments
# - sort_constructors_first # # - sort_constructors_first
# - sort_unnamed_constructors_first # # - sort_unnamed_constructors_first
# - super_goes_last # no longer needed w/ Dart 2 # # - super_goes_last # no longer needed w/ Dart 2
- test_types_in_equals # - test_types_in_equals
- throw_in_finally # - throw_in_finally
# - type_annotate_public_apis # subset of always_specify_types # # - type_annotate_public_apis # subset of always_specify_types
- type_init_formals # - type_init_formals
# - unawaited_futures # # - unawaited_futures
- unnecessary_brace_in_string_interps # - unnecessary_brace_in_string_interps
- unnecessary_getters_setters # - unnecessary_getters_setters
- unnecessary_statements # - unnecessary_statements
- unrelated_type_equality_checks # - unrelated_type_equality_checks
- valid_regexps # - valid_regexps
+6 -1
View File
@@ -90,7 +90,12 @@ class ChannelPage extends StatelessWidget {
); );
} }
Widget _messageBuilder(context, message, index) { Widget _messageBuilder(
BuildContext context,
MessageDetails details,
List<Message> messages,
) {
final message = details.message;
final isCurrentUser = StreamChat.of(context).user.id == message.user.id; final isCurrentUser = StreamChat.of(context).user.id == message.user.id;
final textAlign = isCurrentUser ? TextAlign.right : TextAlign.left; final textAlign = isCurrentUser ? TextAlign.right : TextAlign.left;
final color = isCurrentUser ? Colors.blueGrey : Colors.blue; final color = isCurrentUser ? Colors.blueGrey : Colors.blue;
+9 -8
View File
@@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_apns/apns.dart'; import 'package:flutter_apns/apns.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart' import 'package:flutter_local_notifications/flutter_local_notifications.dart'
@@ -66,9 +67,11 @@ void _initNotifications(Client client) {
void main() async { void main() async {
final client = Client( final client = Client(
's2dxdhpxd94g', 's2dxdhpxd94g',
logLevel: Level.INFO, logLevel: Level.SEVERE,
showLocalNotification: Platform.isAndroid ? showLocalNotification : null, showLocalNotification: Platform.isAndroid ? showLocalNotification : null,
backgroundKeepAlive: Duration.zero, backgroundKeepAlive: Duration.zero,
persistenceEnabled: true,
// baseURL: 'chat-us-east-staging.stream-io-api.com',
); );
await client.setUser( await client.setUser(
@@ -108,11 +111,11 @@ class ChannelListPage extends StatelessWidget {
return Scaffold( return Scaffold(
body: ChannelsBloc( body: ChannelsBloc(
child: ChannelListView( child: ChannelListView(
filter: { // filter: {
'members': { // 'members': {
'\$in': [StreamChat.of(context).user.id], // '\$in': [StreamChat.of(context).user.id],
} // }
}, // },
sort: [SortOption('last_message_at')], sort: [SortOption('last_message_at')],
pagination: PaginationParams( pagination: PaginationParams(
limit: 20, limit: 20,
@@ -139,8 +142,6 @@ class ChannelPage extends StatelessWidget {
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
MessageListView( MessageListView(
showOtherMessageUsername: true,
showVideoFullScreen: false,
threadBuilder: (_, parentMessage) { threadBuilder: (_, parentMessage) {
return ThreadPage( return ThreadPage(
parent: parentMessage, parent: parentMessage,
+26 -36
View File
@@ -22,43 +22,33 @@ class AttachmentTitle extends StatelessWidget {
launchURL(context, attachment.titleLink); launchURL(context, attachment.titleLink);
} }
}, },
child: Container( child: Padding(
constraints: BoxConstraints.loose( padding: const EdgeInsets.all(8.0),
Size( child: Column(
MediaQuery.of(context).size.width * 0.7, mainAxisSize: MainAxisSize.min,
500, crossAxisAlignment: CrossAxisAlignment.start,
), children: <Widget>[
), Text(
child: Padding( attachment.title,
padding: const EdgeInsets.all(8.0), overflow: TextOverflow.ellipsis,
child: Column( style: messageTheme.messageText.copyWith(
mainAxisSize: MainAxisSize.min, color: StreamChatTheme.of(context).accentColor,
crossAxisAlignment: CrossAxisAlignment.start, fontWeight: FontWeight.bold,
children: <Widget>[
Text(
attachment.title,
overflow: TextOverflow.ellipsis,
style: messageTheme.messageText.copyWith(
color: StreamChatTheme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
), ),
if (attachment.titleLink != null || ),
attachment.ogScrapeUrl != null) if (attachment.titleLink != null || attachment.ogScrapeUrl != null)
Text( Text(
Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl) Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl)
.authority .authority
.split('.') .split('.')
.reversed .reversed
.take(2) .take(2)
.toList() .toList()
.reversed .reversed
.join('.'), .join('.'),
overflow: TextOverflow.ellipsis, style: messageTheme.createdAt,
style: messageTheme.createdAt, ),
), ],
],
),
), ),
), ),
); );
+1 -1
View File
@@ -44,7 +44,7 @@ class DateDivider extends StatelessWidget {
} }
return Padding( return Padding(
padding: const EdgeInsets.only(top: 24.0), padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
+7 -18
View File
@@ -5,30 +5,19 @@ class DeletedMessage extends StatelessWidget {
const DeletedMessage({ const DeletedMessage({
Key key, Key key,
@required this.messageTheme, @required this.messageTheme,
this.alignment,
}) : super(key: key); }) : super(key: key);
final MessageTheme messageTheme; final MessageTheme messageTheme;
final Alignment alignment;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Align( return Text(
alignment: alignment, 'This message was deleted...',
child: Padding( style: messageTheme.messageText.copyWith(
padding: const EdgeInsets.symmetric( fontStyle: FontStyle.italic,
horizontal: 14, color: Theme.of(context).brightness == Brightness.dark
vertical: 14, ? Colors.white
), : Colors.black,
child: Text(
'This message was deleted...',
style: messageTheme.messageText.copyWith(
fontStyle: FontStyle.italic,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
),
),
), ),
); );
} }
+1 -3
View File
@@ -26,10 +26,9 @@ class ImageAttachment extends StatelessWidget {
attachment: attachment, attachment: attachment,
); );
} }
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Stack( Stack(
children: <Widget>[ children: <Widget>[
@@ -42,7 +41,6 @@ class ImageAttachment extends StatelessWidget {
return GestureDetector( return GestureDetector(
child: Image( child: Image(
image: provider, image: provider,
width: MediaQuery.of(context).size.width * 0.7,
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
onTap: () { onTap: () {
+209
View File
@@ -0,0 +1,209 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/reaction_picker.dart';
import 'package:stream_chat_flutter/src/stream_channel.dart';
import '../stream_chat_flutter.dart';
import 'message_input.dart';
import 'stream_chat.dart';
class MessageActionsBottomSheet extends StatelessWidget {
final Widget Function(BuildContext, Message) editMessageInputBuilder;
final void Function(Message) onThreadTap;
final Message message;
final bool showReactions;
final bool showDeleteMessage;
final bool showEditMessage;
final bool showReply;
final Map<String, String> reactionToEmoji = const {
'love': '❤️',
'haha': '😂',
'like': '👍',
'sad': '😕',
'angry': '😡',
'wow': '😲',
};
const MessageActionsBottomSheet({
Key key,
this.message,
this.showReactions,
this.showDeleteMessage,
this.showEditMessage,
this.onThreadTap,
this.showReply,
this.editMessageInputBuilder,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (showReactions &&
(message.status == MessageSendingStatus.SENT ||
message.status == null))
ReactionPicker(
channel: channel,
reactionToEmoji: reactionToEmoji,
message: message,
),
if (showDeleteMessage) _buildDeleteButton(context),
if (showEditMessage) _buildEditMessage(context),
if (showReply &&
(message.status == MessageSendingStatus.SENT ||
message.status == null) &&
message.parentId == null)
_buildReplyButton(context),
],
),
);
}
FlatButton _buildDeleteButton(BuildContext context) {
return FlatButton(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Delete message',
style:
Theme.of(context).textTheme.headline5.copyWith(color: Colors.red),
),
),
onPressed: () {
Navigator.pop(context);
StreamChat.of(context).client.deleteMessage(
message,
StreamChannel.of(context).channel.cid,
);
},
);
}
FlatButton _buildEditMessage(BuildContext context) {
return FlatButton(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Edit message',
style: Theme.of(context).textTheme.headline5,
),
),
onPressed: () async {
Navigator.pop(context);
_showEditBottomSheet(context);
},
);
}
void _showEditBottomSheet(BuildContext context) {
final channel = StreamChannel.of(context).channel;
showModalBottomSheet(
context: context,
elevation: 2,
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32),
),
),
builder: (context) {
return StreamChannel(
channel: channel,
child: Flex(
direction: Axis.vertical,
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(
top: 16.0,
left: 16.0,
right: 16.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'Edit message',
style: Theme.of(context).textTheme.headline6,
),
Container(
height: 30,
padding: const EdgeInsets.all(2.0),
child: AspectRatio(
aspectRatio: 1,
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0,
onPressed: () {
Navigator.of(context).pop();
},
fillColor:
Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(.1)
: Colors.black.withOpacity(.1),
padding: EdgeInsets.all(4),
child: Icon(
Icons.close,
size: 15,
color: StreamChatTheme.of(context)
.primaryIconTheme
.color,
),
),
),
),
],
),
),
Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: editMessageInputBuilder != null
? editMessageInputBuilder(context, message)
: MessageInput(
editMessage: message,
onMessageSent: (_) {
FocusScope.of(context).unfocus();
Navigator.pop(context);
},
),
),
],
),
);
},
);
}
FlatButton _buildReplyButton(BuildContext context) {
return FlatButton(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Start a thread',
style: Theme.of(context).textTheme.headline5,
),
),
onPressed: () {
Navigator.pop(context);
if (onThreadTap != null) {
onThreadTap(message);
}
},
);
}
}
-1
View File
@@ -861,7 +861,6 @@ class MessageInputState extends State<MessageInput> {
Message message; Message message;
if (widget.editMessage != null) { if (widget.editMessage != null) {
message = widget.editMessage.copyWith( message = widget.editMessage.copyWith(
parentId: widget.parentMessage?.id,
text: text, text: text,
attachments: _getAttachments(attachments).toList(), attachments: _getAttachments(attachments).toList(),
mentionedUsers: mentionedUsers:
+152 -123
View File
@@ -3,19 +3,46 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/message_widget.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import 'date_divider.dart'; import 'date_divider.dart';
import 'message_widget.dart';
import 'stream_channel.dart'; import 'stream_channel.dart';
typedef MessageBuilder = Widget Function( typedef MessageBuilder = Widget Function(
BuildContext, Message, List<Message>, int index); BuildContext,
typedef ParentMessageBuilder = Widget Function(BuildContext, Message); MessageDetails,
List<Message>,
);
typedef ParentMessageBuilder = Widget Function(
BuildContext,
Message,
);
typedef ThreadBuilder = Widget Function(BuildContext context, Message parent); typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
typedef ThreadTapCallback = void Function(Message, Widget); typedef ThreadTapCallback = void Function(Message, Widget);
class MessageDetails {
bool isMyMessage;
bool isLastUser;
bool isNextUser;
Message message;
int index;
MessageDetails(
BuildContext context,
this.message,
List<Message> messages,
this.index,
) {
isMyMessage = message.user.id == StreamChat.of(context).user.id;
isLastUser = index + 1 < messages.length &&
message.user.id == messages[index + 1]?.user?.id;
isNextUser =
index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id;
}
}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview_paint.png)
/// ///
@@ -65,15 +92,7 @@ class MessageListView extends StatefulWidget {
this.parentMessage, this.parentMessage,
this.threadBuilder, this.threadBuilder,
this.onThreadTap, this.onThreadTap,
this.showOtherMessageUsername = false,
this.showVideoFullScreen = true,
this.onMentionTap,
this.onUserAvatarTap,
this.onMessageActions,
this.attachmentBuilders,
this.dateDividerBuilder, this.dateDividerBuilder,
this.showAvatar = true,
this.editMessageInputBuilder,
this.scrollPhysics = const AlwaysScrollableScrollPhysics(), this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
}) : super(key: key); }) : super(key: key);
@@ -93,38 +112,12 @@ class MessageListView extends StatefulWidget {
/// Parent message in case of a thread /// Parent message in case of a thread
final Message parentMessage; final Message parentMessage;
/// If true show the other users username next to the timestamp of the message
final bool showOtherMessageUsername;
/// True if the video player will allow fullscreen mode
final bool showVideoFullScreen;
/// Function called on message mention tap
final void Function(User) onMentionTap;
/// Function called on User Avatar tap
final void Function(User) onUserAvatarTap;
/// Function called on message long press
final Function(BuildContext, Message) onMessageActions;
/// Map that defines a builder for an attachment type
final Map<String, AttachmentBuilder> attachmentBuilders;
/// Builder used to render date dividers /// Builder used to render date dividers
final Widget Function(DateTime) dateDividerBuilder; final Widget Function(DateTime) dateDividerBuilder;
/// if true shows the user avatar
final bool showAvatar;
/// Builder used to build the message input to edit a message
final Widget Function(BuildContext, Message) editMessageInputBuilder;
/// The ScrollPhysics used by the ListView /// The ScrollPhysics used by the ListView
final ScrollPhysics scrollPhysics; final ScrollPhysics scrollPhysics;
@override @override
_MessageListViewState createState() => _MessageListViewState(); _MessageListViewState createState() => _MessageListViewState();
} }
@@ -171,24 +164,7 @@ class _MessageListViewState extends State<MessageListView> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
MessageWidget( buildParentMessage(widget.parentMessage),
key: ValueKey<String>(
'PARENT-MESSAGE-${widget.parentMessage.id}'),
previousMessage: null,
message: widget.parentMessage,
nextMessage: null,
onThreadTap: _onThreadTap,
isParent: true,
showVideoFullScreen: widget.showVideoFullScreen,
showOtherMessageUsername:
widget.showOtherMessageUsername,
onMentionTap: widget.onMentionTap,
onUserAvatarTap: widget.onUserAvatarTap,
onMessageActions: widget.onMessageActions,
attachmentBuilders: widget.attachmentBuilders,
showAvatar: widget.showAvatar,
editMessageInputBuilder: widget.editMessageInputBuilder,
),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 32), padding: const EdgeInsets.symmetric(horizontal: 32),
child: Container( child: Container(
@@ -212,50 +188,40 @@ class _MessageListViewState extends State<MessageListView> {
return _buildLoadingIndicator(streamChannel); return _buildLoadingIndicator(streamChannel);
} }
final message = _messages[i]; final message = _messages[i];
final previousMessage =
i < _messages.length - 1 ? _messages[i + 1] : null;
final nextMessage = i > 0 ? _messages[i - 1] : null; final nextMessage = i > 0 ? _messages[i - 1] : null;
Widget messageWidget; Widget messageWidget;
if (i == 0) { if (i == 0) {
messageWidget = _buildBottomMessage( messageWidget = _buildBottomMessage(
streamChannel,
previousMessage,
message,
context, context,
message,
_messages,
streamChannel,
); );
} else if (i == _messages.length - 1) { } else if (i == _messages.length - 1) {
messageWidget = _buildTopMessage( messageWidget = _buildTopMessage(
message,
nextMessage,
streamChannel,
context, context,
message,
_messages,
streamChannel,
); );
} else { } else {
if (widget.messageBuilder != null) { if (widget.messageBuilder != null) {
messageWidget = Builder( messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'), key: ValueKey<String>('MESSAGE-${message.id}'),
builder: (_) => builder: (_) => widget.messageBuilder(
widget.messageBuilder(context, message, _messages, i), context,
MessageDetails(
context,
message,
_messages,
i,
),
_messages),
); );
} else { } else {
messageWidget = MessageWidget( messageWidget = buildMessage(message, _messages, i);
key: ValueKey<String>('MESSAGE-${message.id}'),
previousMessage: previousMessage,
message: message,
nextMessage: nextMessage,
onThreadTap: _onThreadTap,
showOtherMessageUsername: widget.showOtherMessageUsername,
showVideoFullScreen: widget.showVideoFullScreen,
onMentionTap: widget.onMentionTap,
onUserAvatarTap: widget.onUserAvatarTap,
onMessageActions: widget.onMessageActions,
attachmentBuilders: widget.attachmentBuilders,
showAvatar: widget.showAvatar,
editMessageInputBuilder: widget.editMessageInputBuilder,
);
} }
} }
@@ -319,38 +285,28 @@ class _MessageListViewState extends State<MessageListView> {
} }
Widget _buildTopMessage( Widget _buildTopMessage(
Message message,
Message nextMessage,
StreamChannelState streamChannelState,
BuildContext context, BuildContext context,
Message message,
List<Message> messages,
StreamChannelState streamChannel,
) { ) {
Widget messageWidget; Widget messageWidget;
if (widget.messageBuilder != null) { if (widget.messageBuilder != null) {
messageWidget = Builder( messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'), key: ValueKey<String>('TOP-MESSAGE'),
builder: (_) => widget.messageBuilder( builder: (_) => widget.messageBuilder(
context, context,
message, MessageDetails(
context,
message,
_messages,
_messages.length - 1,
),
_messages, _messages,
_messages.length - 1,
), ),
); );
} else { } else {
messageWidget = MessageWidget( messageWidget = buildMessage(message, messages, _messages.length - 1);
key: ValueKey<String>('MESSAGE-${message.id}'),
previousMessage: null,
message: message,
nextMessage: nextMessage,
onThreadTap: _onThreadTap,
showVideoFullScreen: widget.showVideoFullScreen,
showOtherMessageUsername: widget.showOtherMessageUsername,
onMentionTap: widget.onMentionTap,
onUserAvatarTap: widget.onUserAvatarTap,
onMessageActions: widget.onMessageActions,
attachmentBuilders: widget.attachmentBuilders,
showAvatar: widget.showAvatar,
editMessageInputBuilder: widget.editMessageInputBuilder,
);
} }
return VisibilityDetector( return VisibilityDetector(
@@ -359,7 +315,7 @@ class _MessageListViewState extends State<MessageListView> {
onVisibilityChanged: (visibility) { onVisibilityChanged: (visibility) {
final topIsVisible = visibility.visibleBounds != Rect.zero; final topIsVisible = visibility.visibleBounds != Rect.zero;
if (topIsVisible && !_topWasVisible) { if (topIsVisible && !_topWasVisible) {
streamChannelState.queryMessages(); streamChannel.queryMessages();
} }
_topWasVisible = topIsVisible; _topWasVisible = topIsVisible;
}, },
@@ -367,33 +323,28 @@ class _MessageListViewState extends State<MessageListView> {
} }
Widget _buildBottomMessage( Widget _buildBottomMessage(
StreamChannelState streamChannel,
Message previousMessage,
Message message,
BuildContext context, BuildContext context,
Message message,
List<Message> messages,
StreamChannelState streamChannel,
) { ) {
Widget messageWidget; Widget messageWidget;
if (widget.messageBuilder != null) { if (widget.messageBuilder != null) {
messageWidget = Builder( messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'), key: ValueKey<String>('BOTTOM-MESSAGE'),
builder: (_) => widget.messageBuilder(context, message, _messages, 0), builder: (_) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
_messages,
0,
),
_messages,
),
); );
} else { } else {
messageWidget = MessageWidget( messageWidget = buildMessage(message, messages, 0);
key: ValueKey<String>('MESSAGE-${message.id}'),
previousMessage: previousMessage,
message: message,
nextMessage: null,
onThreadTap: _onThreadTap,
showVideoFullScreen: widget.showVideoFullScreen,
showOtherMessageUsername: widget.showOtherMessageUsername,
onMentionTap: widget.onMentionTap,
onUserAvatarTap: widget.onUserAvatarTap,
onMessageActions: widget.onMessageActions,
attachmentBuilders: widget.attachmentBuilders,
showAvatar: widget.showAvatar,
editMessageInputBuilder: widget.editMessageInputBuilder,
);
} }
return VisibilityDetector( return VisibilityDetector(
@@ -410,6 +361,84 @@ class _MessageListViewState extends State<MessageListView> {
); );
} }
Widget buildParentMessage(
Message message,
) {
final isMyMessage = message.user.id == StreamChat.of(context).user.id;
return MessageWidget(
showReplyIndicator: false,
message: message,
reverse: isMyMessage,
showUsername: !isMyMessage,
padding: EdgeInsets.only(
top: 8.0,
left: 8.0,
right: 8.0,
bottom: 16.0,
),
showSendingIndicator: DisplayWidget.hide,
onThreadTap: _onThreadTap,
showEditMessage: false,
showDeleteMessage: false,
borderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16),
bottomLeft: Radius.circular(2),
topRight: Radius.circular(16),
bottomRight: Radius.circular(16),
),
borderSide: isMyMessage ? BorderSide.none : null,
showUserAvatar: DisplayWidget.show,
messageTheme: isMyMessage
? StreamChatTheme.of(context).ownMessageTheme
: StreamChatTheme.of(context).otherMessageTheme,
);
}
Widget buildMessage(
Message message,
List<Message> messages,
int index,
) {
final isMyMessage = message.user.id == StreamChat.of(context).user.id;
final isLastUser = index + 1 < messages.length &&
message.user.id == messages[index + 1]?.user?.id;
final isNextUser =
index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id;
return MessageWidget(
message: message,
reverse: isMyMessage,
showReactions: !message.isDeleted,
padding: EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: index == 0 ? 30 : (isLastUser ? 5 : 10),
),
showUsername: !isMyMessage && !isNextUser,
showSendingIndicator: isMyMessage &&
(index == 0 || message.status != MessageSendingStatus.SENT)
? DisplayWidget.show
: DisplayWidget.hide,
showTimestamp: !isNextUser,
showEditMessage: isMyMessage,
showDeleteMessage: isMyMessage,
borderSide: isMyMessage ? BorderSide.none : null,
onThreadTap: _onThreadTap,
attachmentBorderRadiusGeometry: BorderRadius.circular(16),
borderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(isLastUser ? 2 : 16),
bottomLeft: Radius.circular(2),
topRight: Radius.circular(16),
bottomRight: Radius.circular(16),
),
showUserAvatar: isNextUser ? DisplayWidget.hide : DisplayWidget.show,
messageTheme: isMyMessage
? StreamChatTheme.of(context).ownMessageTheme
: StreamChatTheme.of(context).otherMessageTheme,
);
}
StreamSubscription _streamListener; StreamSubscription _streamListener;
@override @override
+64
View File
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat_theme.dart';
import 'utils.dart';
class MessageText extends StatelessWidget {
const MessageText({
Key key,
@required this.message,
@required this.messageTheme,
this.onMentionTap,
}) : super(key: key);
final Message message;
final void Function(User) onMentionTap;
final MessageTheme messageTheme;
@override
Widget build(BuildContext context) {
final text = _replaceMentions(message.text);
return MarkdownBody(
data: text,
onTapLink: (link) {
if (link.startsWith('@')) {
final mentionedUser = message.mentionedUsers.firstWhere(
(u) => '@${u.name.replaceAll(' ', '')}' == link,
orElse: () => null,
);
if (onMentionTap != null) {
onMentionTap(mentionedUser);
} else {
print('tap on ${mentionedUser.name}');
}
} else {
launchURL(context, link);
}
},
styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context).copyWith(
textTheme: Theme.of(context).textTheme.apply(
bodyColor: messageTheme.messageText.color,
decoration: messageTheme.messageText.decoration,
decorationColor: messageTheme.messageText.decorationColor,
decorationStyle: messageTheme.messageText.decorationStyle,
fontFamily: messageTheme.messageText.fontFamily,
),
),
).copyWith(
p: messageTheme.messageText,
),
);
}
String _replaceMentions(String text) {
message.mentionedUsers?.forEach((u) {
text = text.replaceAll(
'@${u.name}', '[@${u.name}](@${u.name.replaceAll(' ', '')})');
});
return text;
}
}
File diff suppressed because it is too large Load Diff
+42 -39
View File
@@ -24,47 +24,50 @@ class ReactionPicker extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Container(
crossAxisAlignment: CrossAxisAlignment.start, color: Colors.black87,
mainAxisAlignment: MainAxisAlignment.center, child: Row(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
children: reactionToEmoji.keys.map((reactionType) { mainAxisAlignment: MainAxisAlignment.center,
final ownReactionIndex = message.ownReactions mainAxisSize: MainAxisSize.min,
?.indexWhere((reaction) => reaction.type == reactionType) ?? children: reactionToEmoji.keys.map((reactionType) {
-1; final ownReactionIndex = message.ownReactions
return Column( ?.indexWhere((reaction) => reaction.type == reactionType) ??
mainAxisSize: MainAxisSize.min, -1;
mainAxisAlignment: MainAxisAlignment.start, return Column(
children: <Widget>[ mainAxisSize: MainAxisSize.min,
IconButton( mainAxisAlignment: MainAxisAlignment.start,
iconSize: size, children: <Widget>[
icon: Text( IconButton(
reactionToEmoji[reactionType], iconSize: size,
style: TextStyle( icon: Text(
fontSize: size - 10, reactionToEmoji[reactionType],
style: TextStyle(
fontSize: size - 10,
),
), ),
onPressed: () {
if (ownReactionIndex != -1) {
removeReaction(
context, message.ownReactions[ownReactionIndex]);
} else {
sendReaction(context, reactionType);
}
},
), ),
onPressed: () { ownReactionIndex != -1
if (ownReactionIndex != -1) { ? Padding(
removeReaction( padding: const EdgeInsets.only(bottom: 4.0),
context, message.ownReactions[ownReactionIndex]); child: Text(
} else { message.ownReactions[ownReactionIndex].score.toString(),
sendReaction(context, reactionType); style: TextStyle(color: Colors.white),
} ),
}, )
), : SizedBox(),
ownReactionIndex != -1 ],
? Padding( );
padding: const EdgeInsets.only(bottom: 4.0), }).toList(),
child: Text( ),
message.ownReactions[ownReactionIndex].score.toString(),
style: TextStyle(color: Colors.white),
),
)
: SizedBox(),
],
);
}).toList(),
); );
} }
+1
View File
@@ -47,6 +47,7 @@ class ReplyIndicator extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0), padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
children: row, children: row,
), ),
), ),
+21 -36
View File
@@ -13,53 +13,38 @@ class SendingIndicator extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (message.status == MessageSendingStatus.SENT || message.status == null) { if (message.status == MessageSendingStatus.SENT || message.status == null) {
return Padding( return CircleAvatar(
padding: const EdgeInsets.symmetric( radius: 4,
horizontal: 1.0, backgroundColor: StreamChatTheme.of(context).accentColor,
), child: Icon(
child: CircleAvatar( Icons.done,
radius: 4, color: Colors.white,
backgroundColor: StreamChatTheme.of(context).accentColor, size: 4,
child: Icon(
Icons.done,
color: Colors.white,
size: 4,
),
), ),
); );
} }
if (message.status == MessageSendingStatus.SENDING || if (message.status == MessageSendingStatus.SENDING ||
message.status == MessageSendingStatus.UPDATING) { message.status == MessageSendingStatus.UPDATING) {
return Padding( return CircleAvatar(
padding: const EdgeInsets.symmetric( radius: 4,
horizontal: 1.0, backgroundColor: Colors.grey,
), child: Icon(
child: CircleAvatar( Icons.access_time,
radius: 4, size: 4,
backgroundColor: Colors.grey, color: Colors.white,
child: Icon(
Icons.access_time,
size: 4,
color: Colors.white,
),
), ),
); );
} }
if (message.status == MessageSendingStatus.FAILED || if (message.status == MessageSendingStatus.FAILED ||
message.status == MessageSendingStatus.FAILED_UPDATE || message.status == MessageSendingStatus.FAILED_UPDATE ||
message.status == MessageSendingStatus.FAILED_DELETE) { message.status == MessageSendingStatus.FAILED_DELETE) {
return Padding( return CircleAvatar(
padding: const EdgeInsets.symmetric( radius: 4,
horizontal: 1.0, backgroundColor: Color(0xffd0021B).withOpacity(.1),
), child: Icon(
child: CircleAvatar( Icons.error_outline,
radius: 4, size: 4,
backgroundColor: Color(0xffd0021B).withOpacity(.1), color: Colors.white,
child: Icon(
Icons.error_outline,
size: 4,
color: Colors.white,
),
), ),
); );
} }
+1
View File
@@ -13,6 +13,7 @@ export 'src/giphy_attachment.dart';
export 'src/image_attachment.dart'; export 'src/image_attachment.dart';
export 'src/message_input.dart'; export 'src/message_input.dart';
export 'src/message_list_view.dart'; export 'src/message_list_view.dart';
export 'src/message_text.dart';
export 'src/message_widget.dart'; export 'src/message_widget.dart';
export 'src/reaction_picker.dart'; export 'src/reaction_picker.dart';
export 'src/reply_indicator.dart'; export 'src/reply_indicator.dart';
+5 -4
View File
@@ -12,18 +12,19 @@ dependencies:
photo_view: ^0.9.2 photo_view: ^0.9.2
rxdart: ^0.24.0 rxdart: ^0.24.0
jiffy: ^3.0.1 jiffy: ^3.0.1
cached_network_image: ^2.2.0 flutter_portal: ^0.1.0
cached_network_image: ^2.2.0+1
flutter_markdown: ^0.3.5 flutter_markdown: ^0.3.5
url_launcher: ^5.4.5 url_launcher: ^5.4.5
video_player: ^0.10.9+1 video_player: ^0.10.10
chewie: ^0.9.10 chewie: ^0.9.10
file_picker: ^1.8.0+2 file_picker: ^1.8.0+2
image_picker: ^0.6.5+3 image_picker: ^0.6.6+1
flutter_keyboard_visibility: ^2.0.0 flutter_keyboard_visibility: ^2.0.0
stream_chat: stream_chat:
path: ../stream_chat_dart path: ../stream_chat_dart
mime: ^0.9.6+3 mime: ^0.9.6+3
visibility_detector: ^0.1.4 visibility_detector: ^0.1.5
http_parser: ^3.1.4 http_parser: ^3.1.4
dev_dependencies: dev_dependencies: