refactor message widget
This commit is contained in:
+61
-61
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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>[
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: () {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+583
-853
File diff suppressed because it is too large
Load Diff
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class ReplyIndicator extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: row,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user