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
analyzer:
exclude:
- lib/**/*.g.dart
- example/*
linter:
rules:
# these rules are documented on and in the same order as
# the Dart Lint rules page to make maintenance easier
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
# - always_declare_return_types
# - always_specify_types
# - annotate_overrides
# - avoid_as
- avoid_empty_else
- avoid_init_to_null
- avoid_return_types_on_setters
- avoid_web_libraries_in_flutter
- await_only_futures
- camel_case_types
- cancel_subscriptions
- close_sinks
# - 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
- control_flow_in_finally
- empty_constructor_bodies
- empty_statements
- hash_and_equals
- implementation_imports
# - invariant_booleans
# - iterable_contains_unrelated_type
- library_names
# - library_prefixes
# - list_remove_unrelated_type
# - literal_only_boolean_expressions
- non_constant_identifier_names
# - one_member_abstracts
# - only_throw_errors
# - overridden_fields
# - package_api_docs
- package_names
- package_prefixed_library_names
- prefer_is_not_empty
# - prefer_mixin # https://github.com/dart-lang/language/issues/32
- public_member_api_docs
- slash_for_doc_comments
# - sort_constructors_first
# - sort_unnamed_constructors_first
# - super_goes_last # no longer needed w/ Dart 2
- test_types_in_equals
- throw_in_finally
# - type_annotate_public_apis # subset of always_specify_types
- type_init_formals
# - unawaited_futures
- unnecessary_brace_in_string_interps
- unnecessary_getters_setters
- unnecessary_statements
- unrelated_type_equality_checks
- valid_regexps
#include: package:pedantic/analysis_options.yaml
#
#analyzer:
# exclude:
# - lib/**/*.g.dart
# - example/*
#
#linter:
# rules:
# # these rules are documented on and in the same order as
# # the Dart Lint rules page to make maintenance easier
# # https://github.com/dart-lang/linter/blob/master/example/all.yaml
# # - always_declare_return_types
# # - always_specify_types
# # - annotate_overrides
# # - avoid_as
# - avoid_empty_else
# - avoid_init_to_null
# - avoid_return_types_on_setters
# - avoid_web_libraries_in_flutter
# - await_only_futures
# - camel_case_types
# - cancel_subscriptions
# - close_sinks
# # - 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
# - control_flow_in_finally
# - empty_constructor_bodies
# - empty_statements
# - hash_and_equals
# - implementation_imports
# # - invariant_booleans
# # - iterable_contains_unrelated_type
# - library_names
# # - library_prefixes
# # - list_remove_unrelated_type
# # - literal_only_boolean_expressions
# - non_constant_identifier_names
# # - one_member_abstracts
# # - only_throw_errors
# # - overridden_fields
## - package_api_docs
# - package_names
# - package_prefixed_library_names
# - prefer_is_not_empty
# # - prefer_mixin # https://github.com/dart-lang/language/issues/32
# - public_member_api_docs
# - slash_for_doc_comments
# # - sort_constructors_first
# # - sort_unnamed_constructors_first
# # - super_goes_last # no longer needed w/ Dart 2
# - test_types_in_equals
# - throw_in_finally
# # - type_annotate_public_apis # subset of always_specify_types
# - type_init_formals
# # - unawaited_futures
# - unnecessary_brace_in_string_interps
# - unnecessary_getters_setters
# - unnecessary_statements
# - unrelated_type_equality_checks
# - 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 textAlign = isCurrentUser ? TextAlign.right : TextAlign.left;
final color = isCurrentUser ? Colors.blueGrey : Colors.blue;
+9 -8
View File
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_apns/apns.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'
@@ -66,9 +67,11 @@ void _initNotifications(Client client) {
void main() async {
final client = Client(
's2dxdhpxd94g',
logLevel: Level.INFO,
logLevel: Level.SEVERE,
showLocalNotification: Platform.isAndroid ? showLocalNotification : null,
backgroundKeepAlive: Duration.zero,
persistenceEnabled: true,
// baseURL: 'chat-us-east-staging.stream-io-api.com',
);
await client.setUser(
@@ -108,11 +111,11 @@ class ChannelListPage extends StatelessWidget {
return Scaffold(
body: ChannelsBloc(
child: ChannelListView(
filter: {
'members': {
'\$in': [StreamChat.of(context).user.id],
}
},
// filter: {
// 'members': {
// '\$in': [StreamChat.of(context).user.id],
// }
// },
sort: [SortOption('last_message_at')],
pagination: PaginationParams(
limit: 20,
@@ -139,8 +142,6 @@ class ChannelPage extends StatelessWidget {
child: Stack(
children: <Widget>[
MessageListView(
showOtherMessageUsername: true,
showVideoFullScreen: false,
threadBuilder: (_, parentMessage) {
return ThreadPage(
parent: parentMessage,
+26 -36
View File
@@ -22,43 +22,33 @@ class AttachmentTitle extends StatelessWidget {
launchURL(context, attachment.titleLink);
}
},
child: Container(
constraints: BoxConstraints.loose(
Size(
MediaQuery.of(context).size.width * 0.7,
500,
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
attachment.title,
overflow: TextOverflow.ellipsis,
style: messageTheme.messageText.copyWith(
color: StreamChatTheme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
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)
Text(
Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl)
.authority
.split('.')
.reversed
.take(2)
.toList()
.reversed
.join('.'),
overflow: TextOverflow.ellipsis,
style: messageTheme.createdAt,
),
],
),
),
if (attachment.titleLink != null || attachment.ogScrapeUrl != null)
Text(
Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl)
.authority
.split('.')
.reversed
.take(2)
.toList()
.reversed
.join('.'),
style: messageTheme.createdAt,
),
],
),
),
);
+1 -1
View File
@@ -44,7 +44,7 @@ class DateDivider extends StatelessWidget {
}
return Padding(
padding: const EdgeInsets.only(top: 24.0),
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
+7 -18
View File
@@ -5,30 +5,19 @@ class DeletedMessage extends StatelessWidget {
const DeletedMessage({
Key key,
@required this.messageTheme,
this.alignment,
}) : super(key: key);
final MessageTheme messageTheme;
final Alignment alignment;
@override
Widget build(BuildContext context) {
return Align(
alignment: alignment,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 14,
),
child: Text(
'This message was deleted...',
style: messageTheme.messageText.copyWith(
fontStyle: FontStyle.italic,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
),
),
return 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,
);
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Stack(
children: <Widget>[
@@ -42,7 +41,6 @@ class ImageAttachment extends StatelessWidget {
return GestureDetector(
child: Image(
image: provider,
width: MediaQuery.of(context).size.width * 0.7,
fit: BoxFit.cover,
),
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;
if (widget.editMessage != null) {
message = widget.editMessage.copyWith(
parentId: widget.parentMessage?.id,
text: text,
attachments: _getAttachments(attachments).toList(),
mentionedUsers:
+152 -123
View File
@@ -3,19 +3,46 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.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 '../stream_chat_flutter.dart';
import 'date_divider.dart';
import 'message_widget.dart';
import 'stream_channel.dart';
typedef MessageBuilder = Widget Function(
BuildContext, Message, List<Message>, int index);
typedef ParentMessageBuilder = Widget Function(BuildContext, Message);
BuildContext,
MessageDetails,
List<Message>,
);
typedef ParentMessageBuilder = Widget Function(
BuildContext,
Message,
);
typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
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_paint.png)
///
@@ -65,15 +92,7 @@ class MessageListView extends StatefulWidget {
this.parentMessage,
this.threadBuilder,
this.onThreadTap,
this.showOtherMessageUsername = false,
this.showVideoFullScreen = true,
this.onMentionTap,
this.onUserAvatarTap,
this.onMessageActions,
this.attachmentBuilders,
this.dateDividerBuilder,
this.showAvatar = true,
this.editMessageInputBuilder,
this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
}) : super(key: key);
@@ -93,38 +112,12 @@ class MessageListView extends StatefulWidget {
/// Parent message in case of a thread
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
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
final ScrollPhysics scrollPhysics;
@override
_MessageListViewState createState() => _MessageListViewState();
}
@@ -171,24 +164,7 @@ class _MessageListViewState extends State<MessageListView> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
MessageWidget(
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,
),
buildParentMessage(widget.parentMessage),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Container(
@@ -212,50 +188,40 @@ class _MessageListViewState extends State<MessageListView> {
return _buildLoadingIndicator(streamChannel);
}
final message = _messages[i];
final previousMessage =
i < _messages.length - 1 ? _messages[i + 1] : null;
final nextMessage = i > 0 ? _messages[i - 1] : null;
Widget messageWidget;
if (i == 0) {
messageWidget = _buildBottomMessage(
streamChannel,
previousMessage,
message,
context,
message,
_messages,
streamChannel,
);
} else if (i == _messages.length - 1) {
messageWidget = _buildTopMessage(
message,
nextMessage,
streamChannel,
context,
message,
_messages,
streamChannel,
);
} else {
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'),
builder: (_) =>
widget.messageBuilder(context, message, _messages, i),
builder: (_) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
_messages,
i,
),
_messages),
);
} else {
messageWidget = MessageWidget(
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,
);
messageWidget = buildMessage(message, _messages, i);
}
}
@@ -319,38 +285,28 @@ class _MessageListViewState extends State<MessageListView> {
}
Widget _buildTopMessage(
Message message,
Message nextMessage,
StreamChannelState streamChannelState,
BuildContext context,
Message message,
List<Message> messages,
StreamChannelState streamChannel,
) {
Widget messageWidget;
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'),
key: ValueKey<String>('TOP-MESSAGE'),
builder: (_) => widget.messageBuilder(
context,
message,
MessageDetails(
context,
message,
_messages,
_messages.length - 1,
),
_messages,
_messages.length - 1,
),
);
} else {
messageWidget = MessageWidget(
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,
);
messageWidget = buildMessage(message, messages, _messages.length - 1);
}
return VisibilityDetector(
@@ -359,7 +315,7 @@ class _MessageListViewState extends State<MessageListView> {
onVisibilityChanged: (visibility) {
final topIsVisible = visibility.visibleBounds != Rect.zero;
if (topIsVisible && !_topWasVisible) {
streamChannelState.queryMessages();
streamChannel.queryMessages();
}
_topWasVisible = topIsVisible;
},
@@ -367,33 +323,28 @@ class _MessageListViewState extends State<MessageListView> {
}
Widget _buildBottomMessage(
StreamChannelState streamChannel,
Message previousMessage,
Message message,
BuildContext context,
Message message,
List<Message> messages,
StreamChannelState streamChannel,
) {
Widget messageWidget;
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'),
builder: (_) => widget.messageBuilder(context, message, _messages, 0),
key: ValueKey<String>('BOTTOM-MESSAGE'),
builder: (_) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
_messages,
0,
),
_messages,
),
);
} else {
messageWidget = MessageWidget(
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,
);
messageWidget = buildMessage(message, messages, 0);
}
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;
@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
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: reactionToEmoji.keys.map((reactionType) {
final ownReactionIndex = message.ownReactions
?.indexWhere((reaction) => reaction.type == reactionType) ??
-1;
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
IconButton(
iconSize: size,
icon: Text(
reactionToEmoji[reactionType],
style: TextStyle(
fontSize: size - 10,
return Container(
color: Colors.black87,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: reactionToEmoji.keys.map((reactionType) {
final ownReactionIndex = message.ownReactions
?.indexWhere((reaction) => reaction.type == reactionType) ??
-1;
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
IconButton(
iconSize: size,
icon: Text(
reactionToEmoji[reactionType],
style: TextStyle(
fontSize: size - 10,
),
),
onPressed: () {
if (ownReactionIndex != -1) {
removeReaction(
context, message.ownReactions[ownReactionIndex]);
} else {
sendReaction(context, reactionType);
}
},
),
onPressed: () {
if (ownReactionIndex != -1) {
removeReaction(
context, message.ownReactions[ownReactionIndex]);
} else {
sendReaction(context, reactionType);
}
},
),
ownReactionIndex != -1
? Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
message.ownReactions[ownReactionIndex].score.toString(),
style: TextStyle(color: Colors.white),
),
)
: SizedBox(),
],
);
}).toList(),
ownReactionIndex != -1
? Padding(
padding: const EdgeInsets.only(bottom: 4.0),
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(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: row,
),
),
+21 -36
View File
@@ -13,53 +13,38 @@ class SendingIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (message.status == MessageSendingStatus.SENT || message.status == null) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 1.0,
),
child: CircleAvatar(
radius: 4,
backgroundColor: StreamChatTheme.of(context).accentColor,
child: Icon(
Icons.done,
color: Colors.white,
size: 4,
),
return CircleAvatar(
radius: 4,
backgroundColor: StreamChatTheme.of(context).accentColor,
child: Icon(
Icons.done,
color: Colors.white,
size: 4,
),
);
}
if (message.status == MessageSendingStatus.SENDING ||
message.status == MessageSendingStatus.UPDATING) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 1.0,
),
child: CircleAvatar(
radius: 4,
backgroundColor: Colors.grey,
child: Icon(
Icons.access_time,
size: 4,
color: Colors.white,
),
return CircleAvatar(
radius: 4,
backgroundColor: Colors.grey,
child: Icon(
Icons.access_time,
size: 4,
color: Colors.white,
),
);
}
if (message.status == MessageSendingStatus.FAILED ||
message.status == MessageSendingStatus.FAILED_UPDATE ||
message.status == MessageSendingStatus.FAILED_DELETE) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 1.0,
),
child: CircleAvatar(
radius: 4,
backgroundColor: Color(0xffd0021B).withOpacity(.1),
child: Icon(
Icons.error_outline,
size: 4,
color: Colors.white,
),
return CircleAvatar(
radius: 4,
backgroundColor: Color(0xffd0021B).withOpacity(.1),
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/message_input.dart';
export 'src/message_list_view.dart';
export 'src/message_text.dart';
export 'src/message_widget.dart';
export 'src/reaction_picker.dart';
export 'src/reply_indicator.dart';
+5 -4
View File
@@ -12,18 +12,19 @@ dependencies:
photo_view: ^0.9.2
rxdart: ^0.24.0
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
url_launcher: ^5.4.5
video_player: ^0.10.9+1
video_player: ^0.10.10
chewie: ^0.9.10
file_picker: ^1.8.0+2
image_picker: ^0.6.5+3
image_picker: ^0.6.6+1
flutter_keyboard_visibility: ^2.0.0
stream_chat:
path: ../stream_chat_dart
mime: ^0.9.6+3
visibility_detector: ^0.1.4
visibility_detector: ^0.1.5
http_parser: ^3.1.4
dev_dependencies: