Merge pull request #169 from GetStream/feat/messg-reply
Add Message Reply feature
This commit is contained in:
@@ -1 +1 @@
|
|||||||
13a41b8138c4868054e44b5158f3bdd6
|
c770358a10272b4ee1cdc2eb432445f6
|
||||||
@@ -5,27 +5,39 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: Verifying fastlane version" time="0.00094">
|
<testcase classname="fastlane.lanes" name="0: Verifying fastlane version" time="0.00047">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: default_platform" time="0.000521">
|
<testcase classname="fastlane.lanes" name="1: default_platform" time="0.000176">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: is_ci" time="0.000605">
|
<testcase classname="fastlane.lanes" name="2: is_ci" time="0.000168">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: is_ci" time="0.000675">
|
<testcase classname="fastlane.lanes" name="3: Switch to ios match_me lane" time="0.000157">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: match" time="48.727951">
|
<testcase classname="fastlane.lanes" name="4: is_ci" time="0.000161">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="5: match" time="32.299713">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="6: gym" time="9.737819">
|
||||||
|
|
||||||
|
<failure message="/Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:229:in `chdir' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing' Fastfile:28:in `block (2 levels) in parsing_binding' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/lane.rb:33:in `call' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:45:in `chdir' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/runner.rb:45:in `execute' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:108:in `block (2 levels) in run' /Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/command.rb:178:in `call' /Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/command.rb:153:in `run' /Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/runner.rb:476:in `run_active_command' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:76:in `run!' /Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/delegates.rb:15:in `run!' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:352:in `run' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/commands_generator.rb:41:in `start' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/fastlane/lib/fastlane/cli_tools_distributor.rb:119:in `take_off' /Library/Ruby/Gems/2.6.0/gems/fastlane-2.162.0/bin/fastlane:23:in `<top (required)>' /usr/local/bin/fastlane:23:in `load' /usr/local/bin/fastlane:23:in `<top (required)>' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `load' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `kernel_load' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:28:in `run' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:465:in `exec' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/invocation.rb:126:in `invoke_command' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor.rb:387:in `dispatch' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:27:in `dispatch' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/base.rb:466:in `start' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/cli.rb:18:in `start' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/exe/bundle:30:in `block in <top (required)>' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/lib/bundler/friendly_errors.rb:124:in `with_friendly_errors' /Library/Ruby/Gems/2.6.0/gems/bundler-2.0.2/exe/bundle:22:in `<top (required)>' /usr/local/bin/bundle:23:in `load' /usr/local/bin/bundle:23:in `<main>' Error building the application - see the log above" />
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
+38
-5
@@ -444,7 +444,7 @@ class ChannelPageArgs {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChannelPage extends StatelessWidget {
|
class ChannelPage extends StatefulWidget {
|
||||||
final int initialScrollIndex;
|
final int initialScrollIndex;
|
||||||
final double initialAlignment;
|
final double initialAlignment;
|
||||||
final bool highlightInitialMessage;
|
final bool highlightInitialMessage;
|
||||||
@@ -456,6 +456,31 @@ class ChannelPage extends StatelessWidget {
|
|||||||
this.highlightInitialMessage = false,
|
this.highlightInitialMessage = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ChannelPageState createState() => _ChannelPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChannelPageState extends State<ChannelPage> {
|
||||||
|
Message _quotedMessage;
|
||||||
|
FocusNode _focusNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_focusNode = FocusNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _reply(Message message) {
|
||||||
|
setState(() => _quotedMessage = message);
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -507,9 +532,11 @@ class ChannelPage extends StatelessWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
MessageListView(
|
MessageListView(
|
||||||
initialScrollIndex: initialScrollIndex,
|
initialScrollIndex: widget.initialScrollIndex,
|
||||||
initialAlignment: initialAlignment,
|
initialAlignment: widget.initialAlignment,
|
||||||
highlightInitialMessage: highlightInitialMessage,
|
highlightInitialMessage: widget.highlightInitialMessage,
|
||||||
|
onMessageSwiped: _reply,
|
||||||
|
onReplyTap: _reply,
|
||||||
threadBuilder: (_, parentMessage) {
|
threadBuilder: (_, parentMessage) {
|
||||||
return ThreadPage(
|
return ThreadPage(
|
||||||
parent: parentMessage,
|
parent: parentMessage,
|
||||||
@@ -557,7 +584,13 @@ class ChannelPage extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MessageInput(),
|
MessageInput(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
quotedMessage: _quotedMessage,
|
||||||
|
onQuotedMessageCleared: () {
|
||||||
|
setState(() => _quotedMessage = null);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: example
|
name: example
|
||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
version: 1.0.108+111
|
version: 1.0.110+113
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.2.2 <3.0.0"
|
sdk: ">=2.2.2 <3.0.0"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
extension StringExtension on String {
|
||||||
|
String capitalize() {
|
||||||
|
return "${this[0].toUpperCase()}${this.substring(1)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List extension
|
||||||
|
extension ListX<T> on List<T> {
|
||||||
|
/// Insert any item<T> inBetween the list items
|
||||||
|
List<T> insertBetween(T item) => expand((e) sync* {
|
||||||
|
yield item;
|
||||||
|
yield e;
|
||||||
|
}).skip(1).toList(growable: false);
|
||||||
|
}
|
||||||
@@ -97,7 +97,7 @@ class _FileAttachmentState extends State<FileAttachment> {
|
|||||||
height: 3.0,
|
height: 3.0,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${_getSizeText(widget.attachment.extraData['file_size'])}',
|
'${getSizeText(widget.attachment.extraData['file_size'])}',
|
||||||
style: StreamChatTheme.of(context).textTheme.body.copyWith(
|
style: StreamChatTheme.of(context).textTheme.body.copyWith(
|
||||||
color: StreamChatTheme.of(context)
|
color: StreamChatTheme.of(context)
|
||||||
.colorTheme
|
.colorTheme
|
||||||
@@ -122,37 +122,6 @@ class _FileAttachmentState extends State<FileAttachment> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// ListTile(
|
|
||||||
// dense: true,
|
|
||||||
// leading: Container(
|
|
||||||
// child: _getFileTypeImage(attachment.extraData['mime_type']),
|
|
||||||
// height: 40.0,
|
|
||||||
// width: 33.33,
|
|
||||||
// ),
|
|
||||||
// title: Text(
|
|
||||||
// attachment?.title ?? 'File',
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontWeight: FontWeight.bold,
|
|
||||||
// ),
|
|
||||||
// maxLines: 3,
|
|
||||||
// ),
|
|
||||||
// subtitle: Text(
|
|
||||||
// '${attachment.extraData['file_size'] ?? 'N/A'} bytes',
|
|
||||||
// style: TextStyle(
|
|
||||||
// color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// trailing: trailing ??
|
|
||||||
// IconButton(
|
|
||||||
// icon: StreamSvgIcon.cloud_download(
|
|
||||||
// color: StreamChatTheme.of(context).colorTheme.black,
|
|
||||||
// ),
|
|
||||||
// onPressed: () {
|
|
||||||
// launchURL(context, attachment.assetUrl);
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -226,76 +195,6 @@ class _FileAttachmentState extends State<FileAttachment> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return getFileTypeImage(widget.attachment.extraData['mime_type']);
|
||||||
switch (widget.attachment.extraData['mime_type']) {
|
|
||||||
case '7z':
|
|
||||||
return StreamSvgIcon.filetype_7z();
|
|
||||||
break;
|
|
||||||
case 'csv':
|
|
||||||
return StreamSvgIcon.filetype_csv();
|
|
||||||
break;
|
|
||||||
case 'doc':
|
|
||||||
return StreamSvgIcon.filetype_doc();
|
|
||||||
break;
|
|
||||||
case 'docx':
|
|
||||||
return StreamSvgIcon.filetype_docx();
|
|
||||||
break;
|
|
||||||
case 'html':
|
|
||||||
return StreamSvgIcon.filetype_html();
|
|
||||||
break;
|
|
||||||
case 'md':
|
|
||||||
return StreamSvgIcon.filetype_md();
|
|
||||||
break;
|
|
||||||
case 'odt':
|
|
||||||
return StreamSvgIcon.filetype_odt();
|
|
||||||
break;
|
|
||||||
case 'pdf':
|
|
||||||
return StreamSvgIcon.filetype_pdf();
|
|
||||||
break;
|
|
||||||
case 'ppt':
|
|
||||||
return StreamSvgIcon.filetype_ppt();
|
|
||||||
break;
|
|
||||||
case 'pptx':
|
|
||||||
return StreamSvgIcon.filetype_pptx();
|
|
||||||
break;
|
|
||||||
case 'rar':
|
|
||||||
return StreamSvgIcon.filetype_rar();
|
|
||||||
break;
|
|
||||||
case 'rtf':
|
|
||||||
return StreamSvgIcon.filetype_rtf();
|
|
||||||
break;
|
|
||||||
case 'tar':
|
|
||||||
return StreamSvgIcon.filetype_tar();
|
|
||||||
break;
|
|
||||||
case 'txt':
|
|
||||||
return StreamSvgIcon.filetype_txt();
|
|
||||||
break;
|
|
||||||
case 'xls':
|
|
||||||
return StreamSvgIcon.filetype_xls();
|
|
||||||
break;
|
|
||||||
case 'xlsx':
|
|
||||||
return StreamSvgIcon.filetype_xlsx();
|
|
||||||
break;
|
|
||||||
case 'zip':
|
|
||||||
return StreamSvgIcon.filetype_zip();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return StreamSvgIcon.filetype_Generic();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getSizeText(int bytes) {
|
|
||||||
if (bytes == null) {
|
|
||||||
return 'Size N/A';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bytes <= 1000) {
|
|
||||||
return '${bytes} bytes';
|
|
||||||
} else if (bytes <= 100000) {
|
|
||||||
return '${(bytes / 1000).toStringAsFixed(2)} KB';
|
|
||||||
} else {
|
|
||||||
return '${(bytes / 1000000).toStringAsFixed(2)} MB';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class LazyLoadScrollView extends StatefulWidget {
|
|||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
|
||||||
/// Initiates a LazyLoadScrollView widget
|
/// Initiates a LazyLoadScrollView widget
|
||||||
LazyLoadScrollView({
|
const LazyLoadScrollView({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
this.onStartOfPage,
|
this.onStartOfPage,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class MediaListView extends StatefulWidget {
|
|||||||
this.selectedIds = const [],
|
this.selectedIds = const [],
|
||||||
this.onSelect,
|
this.onSelect,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MediaListViewState createState() => _MediaListViewState();
|
_MediaListViewState createState() => _MediaListViewState();
|
||||||
}
|
}
|
||||||
@@ -41,9 +42,7 @@ class _MediaListViewState extends State<MediaListView> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LazyLoadScrollView(
|
return LazyLoadScrollView(
|
||||||
onEndOfPage: () async {
|
onEndOfPage: () async => _getMedia(),
|
||||||
return _getMedia();
|
|
||||||
},
|
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
itemCount: _media.length,
|
itemCount: _media.length,
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import 'stream_chat_theme.dart';
|
|||||||
|
|
||||||
class MessageActionsModal extends StatelessWidget {
|
class MessageActionsModal extends StatelessWidget {
|
||||||
final Widget Function(BuildContext, Message) editMessageInputBuilder;
|
final Widget Function(BuildContext, Message) editMessageInputBuilder;
|
||||||
final void Function(Message) onThreadTap;
|
final void Function(Message) onThreadReplyTap;
|
||||||
|
final void Function(Message) onReplyTap;
|
||||||
final Message message;
|
final Message message;
|
||||||
final MessageTheme messageTheme;
|
final MessageTheme messageTheme;
|
||||||
final bool showReactions;
|
final bool showReactions;
|
||||||
@@ -24,6 +25,7 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
final bool showEditMessage;
|
final bool showEditMessage;
|
||||||
final bool showResendMessage;
|
final bool showResendMessage;
|
||||||
final bool showReply;
|
final bool showReply;
|
||||||
|
final bool showThreadReply;
|
||||||
final bool reverse;
|
final bool reverse;
|
||||||
final ShapeBorder messageShape;
|
final ShapeBorder messageShape;
|
||||||
final DisplayWidget showUserAvatar;
|
final DisplayWidget showUserAvatar;
|
||||||
@@ -35,10 +37,12 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
this.showReactions = true,
|
this.showReactions = true,
|
||||||
this.showDeleteMessage = true,
|
this.showDeleteMessage = true,
|
||||||
this.showEditMessage = true,
|
this.showEditMessage = true,
|
||||||
this.onThreadTap,
|
this.onReplyTap,
|
||||||
|
this.onThreadReplyTap,
|
||||||
this.showCopyMessage = true,
|
this.showCopyMessage = true,
|
||||||
this.showReply = true,
|
this.showReply = true,
|
||||||
this.showResendMessage = true,
|
this.showResendMessage = true,
|
||||||
|
this.showThreadReply = true,
|
||||||
this.showUserAvatar = DisplayWidget.show,
|
this.showUserAvatar = DisplayWidget.show,
|
||||||
this.editMessageInputBuilder,
|
this.editMessageInputBuilder,
|
||||||
this.messageShape,
|
this.messageShape,
|
||||||
@@ -51,17 +55,25 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
final user = StreamChat.of(context).user;
|
final user = StreamChat.of(context).user;
|
||||||
|
|
||||||
final roughMaxSize = 2 * size.width / 3;
|
final roughMaxSize = 2 * size.width / 3;
|
||||||
|
var messageTextLength = message.text.length;
|
||||||
|
if (message.quotedMessage != null) {
|
||||||
|
var quotedMessageLength = message.quotedMessage.text.length + 40;
|
||||||
|
if (message.quotedMessage.attachments?.isNotEmpty == true) {
|
||||||
|
quotedMessageLength += 40;
|
||||||
|
}
|
||||||
|
if (quotedMessageLength > messageTextLength) {
|
||||||
|
messageTextLength = quotedMessageLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
final roughSentenceSize =
|
final roughSentenceSize =
|
||||||
message.text.length * messageTheme.messageText.fontSize * 1.2;
|
messageTextLength * messageTheme.messageText.fontSize * 1.2;
|
||||||
final divFactor = message.attachments?.isNotEmpty == true
|
final divFactor = message.attachments?.isNotEmpty == true
|
||||||
? 1
|
? 1
|
||||||
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
|
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onTap: () {
|
onTap: () => Navigator.pop(context),
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
@@ -116,6 +128,7 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
showReactions: false,
|
showReactions: false,
|
||||||
showUsername: false,
|
showUsername: false,
|
||||||
showThreadReplyIndicator: false,
|
showThreadReplyIndicator: false,
|
||||||
|
showReplyIndicator: false,
|
||||||
showUserAvatar: showUserAvatar,
|
showUserAvatar: showUserAvatar,
|
||||||
showTimestamp: false,
|
showTimestamp: false,
|
||||||
translateUserAvatar: false,
|
translateUserAvatar: false,
|
||||||
@@ -165,6 +178,12 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
message.status == null) &&
|
message.status == null) &&
|
||||||
message.parentId == null)
|
message.parentId == null)
|
||||||
_buildReplyButton(context),
|
_buildReplyButton(context),
|
||||||
|
if (showThreadReply &&
|
||||||
|
(message.status ==
|
||||||
|
MessageSendingStatus.SENT ||
|
||||||
|
message.status == null) &&
|
||||||
|
message.parentId == null)
|
||||||
|
_buildThreadReplyButton(context),
|
||||||
if (showResendMessage)
|
if (showResendMessage)
|
||||||
_buildResendMessage(context),
|
_buildResendMessage(context),
|
||||||
if (showEditMessage)
|
if (showEditMessage)
|
||||||
@@ -190,6 +209,24 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildReplyButton(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
'Reply',
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
leading: StreamSvgIcon.reply(
|
||||||
|
color: StreamChatTheme.of(context).primaryIconTheme.color,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
if (onReplyTap != null) {
|
||||||
|
onReplyTap(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDeleteButton(BuildContext context) {
|
Widget _buildDeleteButton(BuildContext context) {
|
||||||
final isDeleteFailed = message.status == MessageSendingStatus.FAILED_DELETE;
|
final isDeleteFailed = message.status == MessageSendingStatus.FAILED_DELETE;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@@ -276,8 +313,8 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(32),
|
topLeft: Radius.circular(16),
|
||||||
topRight: Radius.circular(32),
|
topRight: Radius.circular(16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
@@ -289,39 +326,26 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||||
top: 16.0,
|
|
||||||
left: 16.0,
|
|
||||||
right: 16.0,
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
IconButton(
|
Padding(
|
||||||
icon: StreamSvgIcon.edit(
|
padding: const EdgeInsets.all(8.0),
|
||||||
size: 22,
|
child: StreamSvgIcon.edit(
|
||||||
color:
|
color: StreamChatTheme.of(context)
|
||||||
StreamChatTheme.of(context).primaryIconTheme.color,
|
.colorTheme
|
||||||
|
.greyGainsboro,
|
||||||
),
|
),
|
||||||
onPressed: () {},
|
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Edit message',
|
'Edit Message',
|
||||||
style: Theme.of(context)
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
.textTheme
|
|
||||||
.headline6
|
|
||||||
.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
visualDensity: VisualDensity.compact,
|
||||||
Icons.cancel_outlined,
|
icon: StreamSvgIcon.close_small(),
|
||||||
size: 22,
|
onPressed: Navigator.of(context).pop,
|
||||||
color:
|
|
||||||
StreamChatTheme.of(context).primaryIconTheme.color,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -348,7 +372,7 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildReplyButton(BuildContext context) {
|
Widget _buildThreadReplyButton(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
'Thread reply',
|
'Thread reply',
|
||||||
@@ -359,8 +383,8 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
if (onThreadTap != null) {
|
if (onThreadReplyTap != null) {
|
||||||
onThreadTap(message);
|
onThreadReplyTap(message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+166
-99
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:emojis/emoji.dart';
|
import 'package:emojis/emoji.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
@@ -21,8 +22,10 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
|||||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||||
import 'package:substring_highlight/substring_highlight.dart';
|
import 'package:substring_highlight/substring_highlight.dart';
|
||||||
import 'package:video_compress/video_compress.dart';
|
import 'package:video_compress/video_compress.dart';
|
||||||
|
import 'extension.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
import '../stream_chat_flutter.dart';
|
||||||
|
import 'quoted_message_widget.dart';
|
||||||
import 'stream_channel.dart';
|
import 'stream_channel.dart';
|
||||||
|
|
||||||
typedef FileUploader = Future<String> Function(PlatformFile, Channel);
|
typedef FileUploader = Future<String> Function(PlatformFile, Channel);
|
||||||
@@ -107,6 +110,8 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.actionsLocation = ActionsLocation.left,
|
this.actionsLocation = ActionsLocation.left,
|
||||||
this.attachmentThumbnailBuilders,
|
this.attachmentThumbnailBuilders,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
|
this.quotedMessage,
|
||||||
|
this.onQuotedMessageCleared,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Message to edit
|
/// Message to edit
|
||||||
@@ -155,6 +160,12 @@ class MessageInput extends StatefulWidget {
|
|||||||
/// The focus node associated to the TextField
|
/// The focus node associated to the TextField
|
||||||
final FocusNode focusNode;
|
final FocusNode focusNode;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Message quotedMessage;
|
||||||
|
|
||||||
|
///
|
||||||
|
final VoidCallback onQuotedMessageCleared;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MessageInputState createState() => MessageInputState();
|
MessageInputState createState() => MessageInputState();
|
||||||
|
|
||||||
@@ -166,7 +177,7 @@ class MessageInput extends StatefulWidget {
|
|||||||
|
|
||||||
if (messageInputState == null) {
|
if (messageInputState == null) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'You must have a MessageInput widget as anchestor of your widget tree');
|
'You must have a MessageInput widget as ancestor of your widget tree');
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageInputState;
|
return messageInputState;
|
||||||
@@ -196,9 +207,11 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
/// The editing controller passed to the input TextField
|
/// The editing controller passed to the input TextField
|
||||||
TextEditingController textEditingController;
|
TextEditingController textEditingController;
|
||||||
|
|
||||||
|
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SafeArea(
|
Widget child = SafeArea(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onPanUpdate: (details) {
|
onPanUpdate: (details) {
|
||||||
if (details.delta.dy > 0) {
|
if (details.delta.dy > 0) {
|
||||||
@@ -213,6 +226,32 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
if (_hasQuotedMessage)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: StreamSvgIcon.reply(
|
||||||
|
color: StreamChatTheme.of(context)
|
||||||
|
.colorTheme
|
||||||
|
.greyGainsboro,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Reply to Message',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: StreamSvgIcon.close_small(),
|
||||||
|
onPressed: widget.onQuotedMessageCleared,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: _buildTextField(context),
|
child: _buildTextField(context),
|
||||||
@@ -227,6 +266,14 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (widget.editMessage == null) {
|
||||||
|
child = Material(
|
||||||
|
color: StreamChatTheme.of(context).colorTheme.white,
|
||||||
|
elevation: 8,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
Flex _buildTextField(BuildContext context) {
|
Flex _buildTextField(BuildContext context) {
|
||||||
@@ -328,17 +375,23 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
return AnimatedCrossFade(
|
return AnimatedCrossFade(
|
||||||
crossFadeState:
|
crossFadeState:
|
||||||
_actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
_actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
||||||
firstChild: InkWell(
|
firstChild: Padding(
|
||||||
onTap: () {
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||||
setState(() {
|
child: IconButton(
|
||||||
_actionsShrunk = false;
|
onPressed: () {
|
||||||
});
|
setState(() {
|
||||||
},
|
_actionsShrunk = false;
|
||||||
child: Padding(
|
});
|
||||||
padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0),
|
},
|
||||||
child: StreamSvgIcon.emptyCircleLeft(
|
icon: StreamSvgIcon.emptyCircleLeft(
|
||||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||||
),
|
),
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
),
|
||||||
|
splashRadius: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
secondChild: Row(
|
secondChild: Row(
|
||||||
@@ -370,7 +423,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
padding: _attachments.isEmpty ? null : EdgeInsets.all(6.0),
|
padding: _attachments.isEmpty ? null : EdgeInsets.all(6.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
_buildReplyToMessage(),
|
||||||
_buildAttachments(),
|
_buildAttachments(),
|
||||||
LimitedBox(
|
LimitedBox(
|
||||||
maxHeight: widget.maxHeight,
|
maxHeight: widget.maxHeight,
|
||||||
@@ -416,7 +471,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
.accentBlue,
|
.accentBlue,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
labelPadding:
|
labelPadding:
|
||||||
EdgeInsets.symmetric(horizontal: 9.0),
|
EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
label: Row(
|
label: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -428,7 +483,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
size: 16.0,
|
size: 16.0,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
_chosenCommand?.name?.toUpperCase() ?? "",
|
_chosenCommand?.name?.toUpperCase() ?? '',
|
||||||
style: StreamChatTheme.of(context)
|
style: StreamChatTheme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.footnote
|
.footnote
|
||||||
@@ -443,23 +498,14 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
suffixIcon: _commandEnabled
|
suffixIcon: _commandEnabled
|
||||||
? InkWell(
|
? IconButton(
|
||||||
child: Padding(
|
icon: StreamSvgIcon.close_small(),
|
||||||
padding:
|
splashRadius: 24,
|
||||||
const EdgeInsets.symmetric(horizontal: 8.0),
|
onPressed: () {
|
||||||
child: StreamSvgIcon.close_small(),
|
setState(() => _commandEnabled = false);
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
_commandEnabled = false;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
suffixIconConstraints: BoxConstraints(
|
|
||||||
maxHeight: 24.0,
|
|
||||||
maxWidth: 40.0,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
textCapitalization: TextCapitalization.sentences,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
),
|
),
|
||||||
@@ -1307,6 +1353,25 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
_commandsOverlay = null;
|
_commandsOverlay = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildReplyToMessage() {
|
||||||
|
if (!_hasQuotedMessage) {
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
final containsUrl = widget.quotedMessage.attachments
|
||||||
|
?.any((element) => element.ogScrapeUrl != null) ==
|
||||||
|
true;
|
||||||
|
return Transform(
|
||||||
|
transform: Matrix4.rotationY(pi),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: QuotedMessageWidget(
|
||||||
|
reverse: true,
|
||||||
|
showBorder: !containsUrl,
|
||||||
|
message: widget.quotedMessage,
|
||||||
|
messageTheme: StreamChatTheme.of(context).otherMessageTheme,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildAttachments() {
|
Widget _buildAttachments() {
|
||||||
return _attachments.isEmpty
|
return _attachments.isEmpty
|
||||||
? Container()
|
? Container()
|
||||||
@@ -1520,55 +1585,62 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCommandButton() {
|
Widget _buildCommandButton() {
|
||||||
return InkWell(
|
return Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||||
padding:
|
child: IconButton(
|
||||||
const EdgeInsets.only(left: 4.0, right: 8.0, top: 8.0, bottom: 8.0),
|
icon: StreamSvgIcon.lightning(
|
||||||
child: StreamSvgIcon.lightning(
|
|
||||||
color: _commandsOverlay != null
|
color: _commandsOverlay != null
|
||||||
? StreamChatTheme.of(context).colorTheme.accentBlue
|
? StreamChatTheme.of(context).colorTheme.accentBlue
|
||||||
: StreamChatTheme.of(context).colorTheme.grey,
|
: StreamChatTheme.of(context).colorTheme.grey,
|
||||||
),
|
),
|
||||||
),
|
padding: const EdgeInsets.all(0),
|
||||||
onTap: () async {
|
constraints: BoxConstraints.tightFor(
|
||||||
if (_openFilePickerSection) {
|
height: 24,
|
||||||
setState(() {
|
width: 24,
|
||||||
_animateContainer = false;
|
),
|
||||||
_openFilePickerSection = false;
|
splashRadius: 24,
|
||||||
_filePickerSize = _kMinMediaPickerSize;
|
onPressed: () async {
|
||||||
});
|
if (_openFilePickerSection) {
|
||||||
await Future.delayed(Duration(milliseconds: 300));
|
setState(() {
|
||||||
}
|
_animateContainer = false;
|
||||||
|
_openFilePickerSection = false;
|
||||||
|
_filePickerSize = _kMinMediaPickerSize;
|
||||||
|
});
|
||||||
|
await Future.delayed(Duration(milliseconds: 300));
|
||||||
|
}
|
||||||
|
|
||||||
if (_commandsOverlay == null) {
|
if (_commandsOverlay == null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||||
Overlay.of(context).insert(_commandsOverlay);
|
Overlay.of(context).insert(_commandsOverlay);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
_commandsOverlay?.remove();
|
_commandsOverlay?.remove();
|
||||||
_commandsOverlay = null;
|
_commandsOverlay = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAttachmentButton() {
|
Widget _buildAttachmentButton() {
|
||||||
var padding = widget.editMessage == null ? 4.0 : 8.0;
|
return Padding(
|
||||||
return Center(
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||||
child: InkWell(
|
child: IconButton(
|
||||||
child: Padding(
|
icon: StreamSvgIcon.attach(
|
||||||
padding:
|
color: _openFilePickerSection
|
||||||
EdgeInsets.only(left: 8.0, right: padding, top: 8.0, bottom: 8.0),
|
? StreamChatTheme.of(context).colorTheme.accentBlue
|
||||||
child: StreamSvgIcon.attach(
|
: StreamChatTheme.of(context).colorTheme.grey,
|
||||||
color: _openFilePickerSection
|
|
||||||
? StreamChatTheme.of(context).colorTheme.accentBlue
|
|
||||||
: StreamChatTheme.of(context).colorTheme.grey,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onTap: () async {
|
padding: const EdgeInsets.all(0),
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
),
|
||||||
|
splashRadius: 24,
|
||||||
|
onPressed: () async {
|
||||||
_emojiOverlay?.remove();
|
_emojiOverlay?.remove();
|
||||||
_emojiOverlay = null;
|
_emojiOverlay = null;
|
||||||
_commandsOverlay?.remove();
|
_commandsOverlay?.remove();
|
||||||
@@ -1886,36 +1958,29 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
Widget _buildIdleSendButton(BuildContext context) {
|
Widget _buildIdleSendButton(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0),
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||||
child: Center(
|
child: StreamSvgIcon(
|
||||||
child: InkWell(
|
assetName: _getIdleSendIcon(),
|
||||||
onTap: () {
|
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||||
sendMessage();
|
),
|
||||||
},
|
|
||||||
child: StreamSvgIcon(
|
|
||||||
assetName: _getIdleSendIcon(),
|
|
||||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
|
||||||
height: 24.0,
|
|
||||||
width: 24.0,
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSendButton(BuildContext context) {
|
Widget _buildSendButton(BuildContext context) {
|
||||||
return Center(
|
return Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||||
padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0),
|
child: IconButton(
|
||||||
child: InkWell(
|
onPressed: sendMessage,
|
||||||
onTap: () {
|
visualDensity: VisualDensity.compact,
|
||||||
sendMessage();
|
padding: const EdgeInsets.all(0),
|
||||||
},
|
splashRadius: 24,
|
||||||
child: StreamSvgIcon(
|
constraints: BoxConstraints.tightFor(
|
||||||
assetName: _getSendIcon(),
|
height: 24,
|
||||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
width: 24,
|
||||||
height: 24.0,
|
),
|
||||||
width: 24.0,
|
icon: StreamSvgIcon(
|
||||||
),
|
assetName: _getSendIcon(),
|
||||||
|
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1954,6 +2019,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
textEditingController.clear();
|
textEditingController.clear();
|
||||||
_attachments.clear();
|
_attachments.clear();
|
||||||
|
if (widget.onQuotedMessageCleared != null) {
|
||||||
|
widget.onQuotedMessageCleared();
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_messageIsPresent = false;
|
_messageIsPresent = false;
|
||||||
@@ -1985,6 +2053,12 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (widget.quotedMessage != null) {
|
||||||
|
message = message.copyWith(
|
||||||
|
quotedMessageId: widget.quotedMessage.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (widget.preMessageSending != null) {
|
if (widget.preMessageSending != null) {
|
||||||
message = await widget.preMessageSending(message);
|
message = await widget.preMessageSending(message);
|
||||||
}
|
}
|
||||||
@@ -2023,7 +2097,6 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_focusNode = widget.focusNode ?? FocusNode();
|
_focusNode = widget.focusNode ?? FocusNode();
|
||||||
|
|
||||||
_emojiNames = Emoji.all().map((e) => e.name);
|
_emojiNames = Emoji.all().map((e) => e.name);
|
||||||
@@ -2101,12 +2174,6 @@ class _SendingAttachment {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StringExtension on String {
|
|
||||||
String capitalize() {
|
|
||||||
return "${this[0].toUpperCase()}${this.substring(1)}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a 2-tuple, or pair.
|
/// Represents a 2-tuple, or pair.
|
||||||
class Tuple2<T1, T2> {
|
class Tuple2<T1, T2> {
|
||||||
/// Returns the first item of the tuple
|
/// Returns the first item of the tuple
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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 'stream_channel.dart';
|
import 'stream_channel.dart';
|
||||||
|
import 'swipeable.dart';
|
||||||
|
|
||||||
typedef MessageBuilder = Widget Function(
|
typedef MessageBuilder = Widget Function(
|
||||||
BuildContext,
|
BuildContext,
|
||||||
@@ -29,6 +30,9 @@ typedef ParentMessageBuilder = Widget Function(
|
|||||||
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);
|
||||||
|
|
||||||
|
typedef OnMessageSwiped = void Function(Message);
|
||||||
|
typedef ReplyTapCallback = void Function(Message);
|
||||||
|
|
||||||
class MessageDetails {
|
class MessageDetails {
|
||||||
/// True if the message belongs to the current user
|
/// True if the message belongs to the current user
|
||||||
bool isMyMessage;
|
bool isMyMessage;
|
||||||
@@ -109,12 +113,14 @@ class MessageListView extends StatefulWidget {
|
|||||||
this.parentMessage,
|
this.parentMessage,
|
||||||
this.threadBuilder,
|
this.threadBuilder,
|
||||||
this.onThreadTap,
|
this.onThreadTap,
|
||||||
|
this.onReplyTap,
|
||||||
this.dateDividerBuilder,
|
this.dateDividerBuilder,
|
||||||
this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
|
this.scrollPhysics = const ClampingScrollPhysics(),
|
||||||
this.initialScrollIndex,
|
this.initialScrollIndex,
|
||||||
this.initialAlignment,
|
this.initialAlignment,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
this.itemPositionListener,
|
this.itemPositionListener,
|
||||||
|
this.onMessageSwiped,
|
||||||
this.highlightInitialMessage = false,
|
this.highlightInitialMessage = false,
|
||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@@ -158,6 +164,12 @@ class MessageListView extends StatefulWidget {
|
|||||||
/// The ScrollPhysics used by the ListView
|
/// The ScrollPhysics used by the ListView
|
||||||
final ScrollPhysics scrollPhysics;
|
final ScrollPhysics scrollPhysics;
|
||||||
|
|
||||||
|
/// Called when message item gets swiped
|
||||||
|
final OnMessageSwiped onMessageSwiped;
|
||||||
|
|
||||||
|
///
|
||||||
|
final ReplyTapCallback onReplyTap;
|
||||||
|
|
||||||
/// If true the list will highlight the initialMessage if there is any.
|
/// If true the list will highlight the initialMessage if there is any.
|
||||||
///
|
///
|
||||||
/// Also See [StreamChannel]
|
/// Also See [StreamChannel]
|
||||||
@@ -398,7 +410,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
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: (_) => widget.messageBuilder(
|
builder: (context) => widget.messageBuilder(
|
||||||
context,
|
context,
|
||||||
MessageDetails(
|
MessageDetails(
|
||||||
context,
|
context,
|
||||||
@@ -690,6 +702,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
return MessageWidget(
|
return MessageWidget(
|
||||||
showThreadReplyIndicator: false,
|
showThreadReplyIndicator: false,
|
||||||
showInChannelIndicator: false,
|
showInChannelIndicator: false,
|
||||||
|
showReplyIndicator: false,
|
||||||
message: message,
|
message: message,
|
||||||
reverse: isMyMessage,
|
reverse: isMyMessage,
|
||||||
showUsername: !isMyMessage,
|
showUsername: !isMyMessage,
|
||||||
@@ -748,6 +761,9 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||||
|
|
||||||
|
final isThreadMessage =
|
||||||
|
widget.parentMessage != null || message?.showInChannel == true;
|
||||||
|
|
||||||
Widget child = MessageWidget(
|
Widget child = MessageWidget(
|
||||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||||
message: message,
|
message: message,
|
||||||
@@ -759,6 +775,26 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
|
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
|
||||||
top: 3,
|
top: 3,
|
||||||
),
|
),
|
||||||
|
onQuotedMessageTap: (quotedMessageId) async {
|
||||||
|
final scrollToIndex = () {
|
||||||
|
final index = messages.indexWhere((m) => m.id == quotedMessageId);
|
||||||
|
_scrollController?.scrollTo(
|
||||||
|
index: index,
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
if (messages.map((e) => e.id).contains(quotedMessageId)) {
|
||||||
|
scrollToIndex();
|
||||||
|
} else {
|
||||||
|
streamChannel.loadChannelAtMessage(quotedMessageId).then((_) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (messages.map((e) => e.id).contains(quotedMessageId)) {
|
||||||
|
scrollToIndex();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
showInChannelIndicator: widget.parentMessage == null,
|
showInChannelIndicator: widget.parentMessage == null,
|
||||||
showThreadReplyIndicator: widget.parentMessage == null,
|
showThreadReplyIndicator: widget.parentMessage == null,
|
||||||
showUsername: !isMyMessage && !isNextUser,
|
showUsername: !isMyMessage && !isNextUser,
|
||||||
@@ -771,6 +807,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
showDeleteMessage: isMyMessage,
|
showDeleteMessage: isMyMessage,
|
||||||
borderSide: isMyMessage ? BorderSide.none : null,
|
borderSide: isMyMessage ? BorderSide.none : null,
|
||||||
onThreadTap: _onThreadTap,
|
onThreadTap: _onThreadTap,
|
||||||
|
onReplyTap: widget.onReplyTap,
|
||||||
attachmentBorderRadiusGeometry: BorderRadius.only(
|
attachmentBorderRadiusGeometry: BorderRadius.only(
|
||||||
topLeft: Radius.circular(16),
|
topLeft: Radius.circular(16),
|
||||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||||
@@ -795,6 +832,16 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
onShowMessage: widget.onShowMessage,
|
onShowMessage: widget.onShowMessage,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!isThreadMessage) {
|
||||||
|
child = Swipeable(
|
||||||
|
onSwipeEnd: () => widget.onMessageSwiped(message),
|
||||||
|
backgroundIcon: StreamSvgIcon.reply(
|
||||||
|
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!initialMessageHighlightComplete &&
|
if (!initialMessageHighlightComplete &&
|
||||||
widget.highlightInitialMessage &&
|
widget.highlightInitialMessage &&
|
||||||
_isInitialMessage(message.id)) {
|
_isInitialMessage(message.id)) {
|
||||||
|
|||||||
+227
-213
@@ -11,13 +11,16 @@ import 'package:jiffy/jiffy.dart';
|
|||||||
import 'package:stream_chat_flutter/src/message_actions_modal.dart';
|
import 'package:stream_chat_flutter/src/message_actions_modal.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_reactions_modal.dart';
|
import 'package:stream_chat_flutter/src/message_reactions_modal.dart';
|
||||||
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
||||||
import 'package:stream_chat_flutter/src/url_attachment.dart';
|
import 'package:stream_chat_flutter/src/url_attachment.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
import 'image_group.dart';
|
import 'image_group.dart';
|
||||||
import 'message_text.dart';
|
import 'message_text.dart';
|
||||||
|
import 'extension.dart';
|
||||||
|
|
||||||
typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment);
|
typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment);
|
||||||
|
typedef OnQuotedMessageTap = void Function(String);
|
||||||
|
|
||||||
/// The display behaviour of a widget
|
/// The display behaviour of a widget
|
||||||
enum DisplayWidget {
|
enum DisplayWidget {
|
||||||
@@ -46,6 +49,7 @@ class MessageWidget extends StatefulWidget {
|
|||||||
|
|
||||||
/// The function called when tapping on replies
|
/// The function called when tapping on replies
|
||||||
final void Function(Message) onThreadTap;
|
final void Function(Message) onThreadTap;
|
||||||
|
final void Function(Message) onReplyTap;
|
||||||
final Widget Function(BuildContext, Message) editMessageInputBuilder;
|
final Widget Function(BuildContext, Message) editMessageInputBuilder;
|
||||||
final Widget Function(BuildContext, Message) textBuilder;
|
final Widget Function(BuildContext, Message) textBuilder;
|
||||||
|
|
||||||
@@ -102,6 +106,9 @@ class MessageWidget extends StatefulWidget {
|
|||||||
/// If true the widget will show the thread reply indicator
|
/// If true the widget will show the thread reply indicator
|
||||||
final bool showThreadReplyIndicator;
|
final bool showThreadReplyIndicator;
|
||||||
|
|
||||||
|
/// If true the widget will show the reply indicator
|
||||||
|
final bool showReplyIndicator;
|
||||||
|
|
||||||
/// If true the widget will show the show in channel indicator
|
/// If true the widget will show the show in channel indicator
|
||||||
final bool showInChannelIndicator;
|
final bool showInChannelIndicator;
|
||||||
|
|
||||||
@@ -131,6 +138,9 @@ class MessageWidget extends StatefulWidget {
|
|||||||
/// Center user avatar with bottom of the message
|
/// Center user avatar with bottom of the message
|
||||||
final bool translateUserAvatar;
|
final bool translateUserAvatar;
|
||||||
|
|
||||||
|
/// Function called when quotedMessage is tapped
|
||||||
|
final OnQuotedMessageTap onQuotedMessageTap;
|
||||||
|
|
||||||
///
|
///
|
||||||
MessageWidget({
|
MessageWidget({
|
||||||
Key key,
|
Key key,
|
||||||
@@ -150,6 +160,8 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.showSendingIndicator = DisplayWidget.show,
|
this.showSendingIndicator = DisplayWidget.show,
|
||||||
this.showThreadReplyIndicator = true,
|
this.showThreadReplyIndicator = true,
|
||||||
this.showInChannelIndicator = true,
|
this.showInChannelIndicator = true,
|
||||||
|
this.showReplyIndicator = true,
|
||||||
|
this.onReplyTap,
|
||||||
this.onThreadTap,
|
this.onThreadTap,
|
||||||
this.showUsername = true,
|
this.showUsername = true,
|
||||||
this.showTimestamp = true,
|
this.showTimestamp = true,
|
||||||
@@ -172,6 +184,7 @@ class MessageWidget extends StatefulWidget {
|
|||||||
),
|
),
|
||||||
this.attachmentPadding = EdgeInsets.zero,
|
this.attachmentPadding = EdgeInsets.zero,
|
||||||
this.allRead = false,
|
this.allRead = false,
|
||||||
|
this.onQuotedMessageTap,
|
||||||
}) : attachmentBuilders = {
|
}) : attachmentBuilders = {
|
||||||
'image': (context, message, attachment) {
|
'image': (context, message, attachment) {
|
||||||
return ImageAttachment(
|
return ImageAttachment(
|
||||||
@@ -239,6 +252,8 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
bool get showInChannel =>
|
bool get showInChannel =>
|
||||||
widget.showInChannelIndicator && widget.message?.showInChannel == true;
|
widget.showInChannelIndicator && widget.message?.showInChannel == true;
|
||||||
|
|
||||||
|
bool get _hasQuotedMessage => widget.message?.quotedMessage != null;
|
||||||
|
|
||||||
bool get isSendFailed => widget.message.status == MessageSendingStatus.FAILED;
|
bool get isSendFailed => widget.message.status == MessageSendingStatus.FAILED;
|
||||||
|
|
||||||
bool get isUpdateFailed =>
|
bool get isUpdateFailed =>
|
||||||
@@ -261,185 +276,201 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
? widget.messageTheme.avatarTheme.constraints.maxWidth + 14.5
|
? widget.messageTheme.avatarTheme.constraints.maxWidth + 14.5
|
||||||
: 6.0;
|
: 6.0;
|
||||||
|
|
||||||
|
final isGiphy =
|
||||||
|
widget.message.attachments?.any((element) => element.type == 'giphy') ==
|
||||||
|
true;
|
||||||
|
|
||||||
|
final isOnlyEmoji =
|
||||||
|
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||||
|
|
||||||
final hasFiles =
|
final hasFiles =
|
||||||
widget.message.attachments?.any((element) => element.type == 'file') ==
|
widget.message.attachments?.any((element) => element.type == 'file') ==
|
||||||
true;
|
true;
|
||||||
|
|
||||||
return Portal(
|
return Material(
|
||||||
child: GestureDetector(
|
type: MaterialType.transparency,
|
||||||
onLongPress: () => onLongPress(context),
|
child: Portal(
|
||||||
child: Padding(
|
child: InkWell(
|
||||||
padding: widget.padding ?? EdgeInsets.all(8),
|
onLongPress: widget.message.isDeleted && !isFailedState
|
||||||
child: Transform(
|
? null
|
||||||
alignment: Alignment.center,
|
: () => onLongPress(context),
|
||||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
child: Padding(
|
||||||
child: FractionallySizedBox(
|
padding: widget.padding ?? EdgeInsets.all(8),
|
||||||
alignment: Alignment.centerLeft,
|
child: Transform(
|
||||||
widthFactor: 0.75,
|
alignment: Alignment.center,
|
||||||
child: Column(
|
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: FractionallySizedBox(
|
||||||
mainAxisSize: MainAxisSize.min,
|
alignment: Alignment.centerLeft,
|
||||||
children: <Widget>[
|
widthFactor: 0.75,
|
||||||
Stack(
|
child: Column(
|
||||||
clipBehavior: Clip.none,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
alignment: AlignmentDirectional.bottomStart,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Column(
|
Stack(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
clipBehavior: Clip.none,
|
||||||
mainAxisSize: MainAxisSize.min,
|
alignment: AlignmentDirectional.bottomStart,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: <Widget>[
|
Row(
|
||||||
if (widget.showUserAvatar == DisplayWidget.show)
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
_buildUserAvatar(),
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
SizedBox(width: 6),
|
mainAxisSize: MainAxisSize.min,
|
||||||
if (widget.showUserAvatar == DisplayWidget.hide)
|
children: <Widget>[
|
||||||
SizedBox(
|
if (widget.showUserAvatar == DisplayWidget.show)
|
||||||
width: widget.messageTheme.avatarTheme
|
_buildUserAvatar(),
|
||||||
.constraints.maxWidth +
|
SizedBox(width: 6),
|
||||||
8,
|
if (widget.showUserAvatar == DisplayWidget.hide)
|
||||||
),
|
SizedBox(
|
||||||
Flexible(
|
width: widget.messageTheme.avatarTheme
|
||||||
child: PortalEntry(
|
.constraints.maxWidth +
|
||||||
portal: Container(
|
8,
|
||||||
transform:
|
|
||||||
Matrix4.translationValues(-16, 2, 0),
|
|
||||||
child: _buildReactionIndicator(context),
|
|
||||||
constraints:
|
|
||||||
BoxConstraints(maxWidth: 22 * 6.0),
|
|
||||||
),
|
),
|
||||||
portalAnchor: Alignment(-1.0, -1.0),
|
Flexible(
|
||||||
childAnchor: Alignment(1, -1.0),
|
child: PortalEntry(
|
||||||
child: Stack(
|
portal: Container(
|
||||||
clipBehavior: Clip.none,
|
transform:
|
||||||
children: [
|
Matrix4.translationValues(-16, 2, 0),
|
||||||
Padding(
|
child: _buildReactionIndicator(context),
|
||||||
padding: widget.showReactions
|
constraints:
|
||||||
? EdgeInsets.only(
|
BoxConstraints(maxWidth: 22 * 6.0),
|
||||||
top: widget
|
),
|
||||||
.message
|
portalAnchor: Alignment(-1.0, -1.0),
|
||||||
.reactionCounts
|
childAnchor: Alignment(1, -1.0),
|
||||||
?.isNotEmpty ==
|
child: Stack(
|
||||||
true
|
clipBehavior: Clip.none,
|
||||||
? 18
|
children: [
|
||||||
: 0,
|
Padding(
|
||||||
)
|
padding: widget.showReactions
|
||||||
: EdgeInsets.zero,
|
? EdgeInsets.only(
|
||||||
child: (widget.message.isDeleted &&
|
top: widget
|
||||||
!isFailedState)
|
.message
|
||||||
? Transform(
|
.reactionCounts
|
||||||
alignment: Alignment.center,
|
?.isNotEmpty ==
|
||||||
transform: Matrix4.rotationY(
|
true
|
||||||
widget.reverse ? pi : 0),
|
? 18
|
||||||
child: DeletedMessage(
|
: 0,
|
||||||
reverse: widget.reverse,
|
)
|
||||||
borderRadiusGeometry: widget
|
: EdgeInsets.zero,
|
||||||
.borderRadiusGeometry,
|
child: (widget.message.isDeleted &&
|
||||||
borderSide: widget.borderSide,
|
!isFailedState)
|
||||||
shape: widget.shape,
|
? Transform(
|
||||||
messageTheme:
|
alignment: Alignment.center,
|
||||||
widget.messageTheme,
|
transform: Matrix4.rotationY(
|
||||||
),
|
widget.reverse ? pi : 0),
|
||||||
)
|
child: DeletedMessage(
|
||||||
: Material(
|
reverse: widget.reverse,
|
||||||
clipBehavior: Clip.antiAlias,
|
borderRadiusGeometry: widget
|
||||||
shape: widget.shape ??
|
.borderRadiusGeometry,
|
||||||
RoundedRectangleBorder(
|
borderSide:
|
||||||
side: isOnlyEmoji
|
widget.borderSide,
|
||||||
? BorderSide.none
|
shape: widget.shape,
|
||||||
: widget.borderSide ??
|
messageTheme:
|
||||||
BorderSide(
|
widget.messageTheme,
|
||||||
color: Theme.of(context)
|
),
|
||||||
.brightness ==
|
)
|
||||||
Brightness
|
: Card(
|
||||||
.dark
|
clipBehavior: Clip.antiAlias,
|
||||||
? StreamChatTheme.of(
|
elevation: 0.0,
|
||||||
context)
|
shape: widget.shape ??
|
||||||
.colorTheme
|
RoundedRectangleBorder(
|
||||||
.white
|
side: isOnlyEmoji
|
||||||
.withAlpha(
|
? BorderSide.none
|
||||||
24)
|
: widget.borderSide ??
|
||||||
: StreamChatTheme.of(
|
BorderSide(
|
||||||
context)
|
color: Theme.of(context)
|
||||||
.colorTheme
|
.brightness ==
|
||||||
.black
|
Brightness
|
||||||
.withAlpha(
|
.dark
|
||||||
24),
|
? StreamChatTheme.of(
|
||||||
),
|
context)
|
||||||
borderRadius: widget
|
.colorTheme
|
||||||
.borderRadiusGeometry ??
|
.white
|
||||||
BorderRadius.zero,
|
.withAlpha(
|
||||||
),
|
24)
|
||||||
color: _getBackgroundColor(),
|
: StreamChatTheme.of(
|
||||||
child: Padding(
|
context)
|
||||||
padding: EdgeInsets.all(
|
.colorTheme
|
||||||
hasFiles ? 2.0 : 0.0),
|
.black
|
||||||
child: Column(
|
.withAlpha(
|
||||||
crossAxisAlignment:
|
24),
|
||||||
CrossAxisAlignment
|
),
|
||||||
.start,
|
borderRadius: widget
|
||||||
mainAxisSize:
|
.borderRadiusGeometry ??
|
||||||
MainAxisSize.min,
|
BorderRadius.zero,
|
||||||
children: <Widget>[
|
),
|
||||||
..._parseAttachments(
|
color: _getBackgroundColor(),
|
||||||
context),
|
child: Padding(
|
||||||
if (widget.message.text
|
padding: EdgeInsets.all(
|
||||||
.trim()
|
hasFiles ? 2.0 : 0.0),
|
||||||
.isNotEmpty &&
|
child: Column(
|
||||||
!isGiphy)
|
crossAxisAlignment:
|
||||||
_buildTextBubble(
|
CrossAxisAlignment
|
||||||
|
.start,
|
||||||
|
mainAxisSize:
|
||||||
|
MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
if (_hasQuotedMessage)
|
||||||
|
_buildQuotedMessage(),
|
||||||
|
..._parseAttachments(
|
||||||
context),
|
context),
|
||||||
],
|
if (widget.message.text
|
||||||
|
.trim()
|
||||||
|
.isNotEmpty &&
|
||||||
|
!isGiphy)
|
||||||
|
_buildTextBubble(
|
||||||
|
context),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (widget.showReactionPickerIndicator)
|
||||||
if (widget.showReactionPickerIndicator)
|
Positioned(
|
||||||
Positioned(
|
right: 0,
|
||||||
right: 0,
|
top: -6,
|
||||||
top: -6,
|
child: Transform(
|
||||||
child: Transform(
|
transform: Matrix4.rotationY(
|
||||||
transform: Matrix4.rotationY(
|
widget.reverse ? pi : 0),
|
||||||
widget.reverse ? pi : 0),
|
child: CustomPaint(
|
||||||
child: CustomPaint(
|
painter: ReactionBubblePainter(
|
||||||
painter: ReactionBubblePainter(
|
widget.messageTheme
|
||||||
widget.messageTheme
|
.reactionsBackgroundColor,
|
||||||
.reactionsBackgroundColor,
|
widget.messageTheme
|
||||||
widget.messageTheme
|
.reactionsBorderColor,
|
||||||
.reactionsBorderColor,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
if (showBottomRow) SizedBox(height: 20.0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (showBottomRow) _buildBottomRow(leftPadding),
|
|
||||||
if (isFailedState)
|
|
||||||
Positioned(
|
|
||||||
left: widget.reverse ? -3 : null,
|
|
||||||
right: widget.reverse ? null : -9,
|
|
||||||
bottom: showBottomRow ? 20 : 0,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
),
|
||||||
child: StreamSvgIcon.error(size: 20),
|
if (showBottomRow) SizedBox(height: 20.0),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
if (showBottomRow) _buildBottomRow(leftPadding),
|
||||||
),
|
if (isFailedState)
|
||||||
],
|
Positioned(
|
||||||
|
left: widget.reverse ? -3 : null,
|
||||||
|
right: widget.reverse ? null : -9,
|
||||||
|
bottom: showBottomRow ? 20 : 0,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: StreamSvgIcon.error(size: 20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -448,6 +479,23 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildQuotedMessage() {
|
||||||
|
final isMyMessage =
|
||||||
|
widget.message.user.id == StreamChat.of(context).user.id;
|
||||||
|
return QuotedMessageWidget(
|
||||||
|
onTap: () {
|
||||||
|
if (widget.onQuotedMessageTap != null) {
|
||||||
|
widget.onQuotedMessageTap(widget.message.quotedMessageId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: widget.message.quotedMessage,
|
||||||
|
messageTheme: isMyMessage
|
||||||
|
? StreamChatTheme.of(context).otherMessageTheme
|
||||||
|
: StreamChatTheme.of(context).ownMessageTheme,
|
||||||
|
reverse: widget.reverse,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildBottomRow(double leftPadding) {
|
Widget _buildBottomRow(double leftPadding) {
|
||||||
final deleted = widget.message.isDeleted;
|
final deleted = widget.message.isDeleted;
|
||||||
var children = <Widget>[];
|
var children = <Widget>[];
|
||||||
@@ -572,7 +620,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
var splitList = host.split('.');
|
var splitList = host.split('.');
|
||||||
var hostName = splitList.length == 3 ? splitList[1] : splitList[0];
|
var hostName = splitList.length == 3 ? splitList[1] : splitList[0];
|
||||||
var hostDisplayName = urlAttachment.authorName?.capitalize() ??
|
var hostDisplayName = urlAttachment.authorName?.capitalize() ??
|
||||||
_getWebsiteName(hostName.toLowerCase()) ??
|
getWebsiteName(hostName.toLowerCase()) ??
|
||||||
hostName.capitalize();
|
hostName.capitalize();
|
||||||
|
|
||||||
return UrlAttachment(
|
return UrlAttachment(
|
||||||
@@ -637,7 +685,8 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
showDeleteMessage: widget.showDeleteMessage,
|
showDeleteMessage: widget.showDeleteMessage,
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||||
onThreadTap: widget.onThreadTap,
|
onReplyTap: widget.onReplyTap,
|
||||||
|
onThreadReplyTap: widget.onThreadTap,
|
||||||
showResendMessage:
|
showResendMessage:
|
||||||
widget.showResendMessage && (isSendFailed || isUpdateFailed),
|
widget.showResendMessage && (isSendFailed || isUpdateFailed),
|
||||||
showCopyMessage: !isFailedState &&
|
showCopyMessage: !isFailedState &&
|
||||||
@@ -648,9 +697,11 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
?.any((element) => element.type == 'giphy') !=
|
?.any((element) => element.type == 'giphy') !=
|
||||||
true,
|
true,
|
||||||
showReactions: widget.showReactions,
|
showReactions: widget.showReactions,
|
||||||
showReply: widget.showThreadReplyIndicator &&
|
showReply: widget.showReplyIndicator &&
|
||||||
!isFailedState &&
|
!isFailedState &&
|
||||||
widget.onThreadTap != null,
|
widget.onReplyTap != null,
|
||||||
|
showThreadReply:
|
||||||
|
widget.showThreadReplyIndicator && widget.onThreadTap != null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -835,7 +886,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildTextBubble(BuildContext context) {
|
Widget _buildTextBubble(BuildContext context) {
|
||||||
Widget child = Transform(
|
return Transform(
|
||||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -859,13 +910,13 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.message.attachments
|
if (widget.message.attachments
|
||||||
?.any((element) => element.ogScrapeUrl != null) ==
|
?.any((element) => element.ogScrapeUrl != null) ==
|
||||||
true)
|
true &&
|
||||||
|
!_hasQuotedMessage)
|
||||||
_buildUrlAttachment(),
|
_buildUrlAttachment(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return child;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isOnlyEmoji =>
|
bool get isOnlyEmoji =>
|
||||||
@@ -873,6 +924,10 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
|
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||||
|
|
||||||
Color _getBackgroundColor() {
|
Color _getBackgroundColor() {
|
||||||
|
if (_hasQuotedMessage) {
|
||||||
|
return widget.messageTheme.messageBackgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
if (widget.message.attachments
|
if (widget.message.attachments
|
||||||
?.any((element) => element.ogScrapeUrl != null) ==
|
?.any((element) => element.ogScrapeUrl != null) ==
|
||||||
true) {
|
true) {
|
||||||
@@ -912,47 +967,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getWebsiteName(String hostName) {
|
|
||||||
switch (hostName) {
|
|
||||||
case 'reddit':
|
|
||||||
return 'Reddit';
|
|
||||||
case 'youtube':
|
|
||||||
return 'Youtube';
|
|
||||||
case 'wikipedia':
|
|
||||||
return 'Wikipedia';
|
|
||||||
case 'twitter':
|
|
||||||
return 'Twitter';
|
|
||||||
case 'facebook':
|
|
||||||
return 'Facebook';
|
|
||||||
case 'amazon':
|
|
||||||
return 'Amazon';
|
|
||||||
case 'yelp':
|
|
||||||
return 'Yelp';
|
|
||||||
case 'imdb':
|
|
||||||
return 'IMDB';
|
|
||||||
case 'pinterest':
|
|
||||||
return 'Pinterest';
|
|
||||||
case 'tripadvisor':
|
|
||||||
return 'TripAdvisor';
|
|
||||||
case 'instagram':
|
|
||||||
return 'Instagram';
|
|
||||||
case 'walmart':
|
|
||||||
return 'Walmart';
|
|
||||||
case 'craigslist':
|
|
||||||
return 'Craigslist';
|
|
||||||
case 'ebay':
|
|
||||||
return 'eBay';
|
|
||||||
case 'linkedin':
|
|
||||||
return 'LinkedIn';
|
|
||||||
case 'google':
|
|
||||||
return 'Google';
|
|
||||||
case 'apple':
|
|
||||||
return 'Apple';
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ThreadReplyPainter extends CustomPainter {
|
class _ThreadReplyPainter extends CustomPainter {
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:emojis/emoji.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
import 'attachment_error.dart';
|
||||||
|
import 'image_attachment.dart';
|
||||||
|
import 'message_text.dart';
|
||||||
|
import 'stream_chat_theme.dart';
|
||||||
|
import 'user_avatar.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
import 'extension.dart';
|
||||||
|
|
||||||
|
typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
|
||||||
|
BuildContext,
|
||||||
|
Attachment,
|
||||||
|
);
|
||||||
|
|
||||||
|
class _VideoAttachmentThumbnail extends StatefulWidget {
|
||||||
|
final Size size;
|
||||||
|
final Attachment attachment;
|
||||||
|
|
||||||
|
const _VideoAttachmentThumbnail({
|
||||||
|
Key key,
|
||||||
|
@required this.attachment,
|
||||||
|
this.size = const Size(32, 32),
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_VideoAttachmentThumbnailState createState() =>
|
||||||
|
_VideoAttachmentThumbnailState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
|
||||||
|
VideoPlayerController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = VideoPlayerController.network(widget.attachment.assetUrl)
|
||||||
|
..initialize().then((_) {
|
||||||
|
setState(() {}); //when your thumbnail will show.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_controller.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: widget.size.height,
|
||||||
|
width: widget.size.width,
|
||||||
|
child: _controller.value.initialized
|
||||||
|
? VideoPlayer(_controller)
|
||||||
|
: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
class QuotedMessageWidget extends StatelessWidget {
|
||||||
|
/// The message
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
/// The message theme
|
||||||
|
final MessageTheme messageTheme;
|
||||||
|
|
||||||
|
/// If true the widget will be mirrored
|
||||||
|
final bool reverse;
|
||||||
|
|
||||||
|
/// If true the message will show a grey border
|
||||||
|
final bool showBorder;
|
||||||
|
|
||||||
|
/// limit of the text message shown
|
||||||
|
final int textLimit;
|
||||||
|
|
||||||
|
/// Map that defines a thumbnail builder for an attachment type
|
||||||
|
final Map<String, QuotedMessageAttachmentThumbnailBuilder>
|
||||||
|
attachmentThumbnailBuilders;
|
||||||
|
|
||||||
|
final GestureTapCallback onTap;
|
||||||
|
|
||||||
|
///
|
||||||
|
QuotedMessageWidget({
|
||||||
|
Key key,
|
||||||
|
@required this.message,
|
||||||
|
@required this.messageTheme,
|
||||||
|
this.reverse = false,
|
||||||
|
this.showBorder = false,
|
||||||
|
this.textLimit = 170,
|
||||||
|
this.attachmentThumbnailBuilders,
|
||||||
|
this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
bool get _hasAttachments => message.attachments?.isNotEmpty == true;
|
||||||
|
|
||||||
|
bool get _containsScrapeUrl =>
|
||||||
|
message.attachments?.any((element) => element.ogScrapeUrl != null) ==
|
||||||
|
true;
|
||||||
|
|
||||||
|
bool get _containsText => message?.text?.isNotEmpty == true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8, bottom: 6, right: 4, left: 8),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Flexible(child: _buildMessage(context)),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
_buildUserAvatar(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMessage(BuildContext context) {
|
||||||
|
final isOnlyEmoji =
|
||||||
|
message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||||
|
var msg = _hasAttachments && !_containsText
|
||||||
|
? message.copyWith(text: message.attachments.last?.title ?? '')
|
||||||
|
: message;
|
||||||
|
if (msg.text.length > textLimit) {
|
||||||
|
msg = msg.copyWith(text: '${msg.text.substring(0, textLimit - 3)}...');
|
||||||
|
}
|
||||||
|
|
||||||
|
final children = [
|
||||||
|
if (_hasAttachments) _parseAttachments(context),
|
||||||
|
if (msg.text.isNotEmpty)
|
||||||
|
Flexible(
|
||||||
|
child: Transform(
|
||||||
|
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: MessageText(
|
||||||
|
message: msg,
|
||||||
|
messageTheme: isOnlyEmoji && _containsText
|
||||||
|
? messageTheme.copyWith(
|
||||||
|
messageText: messageTheme.messageText.copyWith(
|
||||||
|
fontSize: 24,
|
||||||
|
))
|
||||||
|
: messageTheme,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
].insertBetween(const SizedBox(width: 8));
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getBackgroundColor(context),
|
||||||
|
border: showBorder
|
||||||
|
? Border.all(
|
||||||
|
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topRight: Radius.circular(12),
|
||||||
|
topLeft: Radius.circular(12),
|
||||||
|
bottomLeft: Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment:
|
||||||
|
reverse ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||||
|
children: reverse ? children.reversed.toList() : children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUrlAttachment(Attachment attachment) {
|
||||||
|
final size = Size(32, 32);
|
||||||
|
if (attachment.thumbUrl != null) {
|
||||||
|
return Container(
|
||||||
|
height: size.height,
|
||||||
|
width: size.width,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
image: CachedNetworkImageProvider(
|
||||||
|
attachment.imageUrl,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return AttachmentError(
|
||||||
|
attachment: attachment,
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _parseAttachments(BuildContext context) {
|
||||||
|
Widget child;
|
||||||
|
Attachment attachment;
|
||||||
|
if (_containsScrapeUrl) {
|
||||||
|
attachment = message.attachments.firstWhere(
|
||||||
|
(element) => element.ogScrapeUrl != null,
|
||||||
|
);
|
||||||
|
child = _buildUrlAttachment(attachment);
|
||||||
|
} else {
|
||||||
|
QuotedMessageAttachmentThumbnailBuilder attachmentBuilder;
|
||||||
|
attachment = message.attachments.last;
|
||||||
|
if (attachmentThumbnailBuilders?.containsKey(attachment?.type) == true) {
|
||||||
|
attachmentBuilder = attachmentThumbnailBuilders[attachment?.type];
|
||||||
|
}
|
||||||
|
attachmentBuilder = _defaultAttachmentBuilder[attachment?.type];
|
||||||
|
if (attachmentBuilder == null) {
|
||||||
|
child = Offstage();
|
||||||
|
}
|
||||||
|
child = attachmentBuilder(context, attachment);
|
||||||
|
}
|
||||||
|
child = AbsorbPointer(child: child);
|
||||||
|
return Transform(
|
||||||
|
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Material(
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
color: Colors.transparent,
|
||||||
|
shape: attachment.type == 'file' ? null : _getDefaultShape(context),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ShapeBorder _getDefaultShape(BuildContext context) {
|
||||||
|
return RoundedRectangleBorder(
|
||||||
|
side: BorderSide(
|
||||||
|
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUserAvatar() {
|
||||||
|
return Transform(
|
||||||
|
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
|
child: UserAvatar(
|
||||||
|
user: message.user,
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
),
|
||||||
|
showOnlineStatus: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, QuotedMessageAttachmentThumbnailBuilder>
|
||||||
|
get _defaultAttachmentBuilder {
|
||||||
|
return {
|
||||||
|
'image': (_, attachment) {
|
||||||
|
return ImageAttachment(
|
||||||
|
attachment: attachment,
|
||||||
|
message: message,
|
||||||
|
messageTheme: messageTheme,
|
||||||
|
size: Size(32, 32),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
'video': (_, attachment) {
|
||||||
|
return _VideoAttachmentThumbnail(
|
||||||
|
key: ValueKey(attachment.assetUrl),
|
||||||
|
attachment: attachment,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
'giphy': (_, attachment) {
|
||||||
|
final size = Size(32, 32);
|
||||||
|
return CachedNetworkImage(
|
||||||
|
height: size?.height,
|
||||||
|
width: size?.width,
|
||||||
|
placeholder: (_, __) {
|
||||||
|
return Container(
|
||||||
|
width: size?.width,
|
||||||
|
height: size?.height,
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
imageUrl:
|
||||||
|
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl,
|
||||||
|
errorWidget: (context, url, error) => AttachmentError(
|
||||||
|
attachment: attachment,
|
||||||
|
size: size,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
'file': (_, attachment) {
|
||||||
|
return Container(
|
||||||
|
height: 32,
|
||||||
|
width: 32,
|
||||||
|
child: getFileTypeImage(attachment.extraData['mime_type']),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getBackgroundColor(BuildContext context) {
|
||||||
|
if (_containsScrapeUrl) {
|
||||||
|
return StreamChatTheme.of(context).colorTheme.blueAlice;
|
||||||
|
}
|
||||||
|
return messageTheme.messageBackgroundColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,9 +10,9 @@ class StreamSvgIcon extends StatelessWidget {
|
|||||||
|
|
||||||
const StreamSvgIcon({
|
const StreamSvgIcon({
|
||||||
this.assetName,
|
this.assetName,
|
||||||
this.width,
|
|
||||||
this.height,
|
|
||||||
this.color,
|
this.color,
|
||||||
|
this.width = 24,
|
||||||
|
this.height = 24,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -326,6 +326,18 @@ class StreamSvgIcon extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
factory StreamSvgIcon.reply({
|
||||||
|
double size,
|
||||||
|
Color color,
|
||||||
|
}) {
|
||||||
|
return StreamSvgIcon(
|
||||||
|
assetName: 'Icon_curve_line_left_up_big.svg',
|
||||||
|
color: color,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory StreamSvgIcon.edit({
|
factory StreamSvgIcon.edit({
|
||||||
double size,
|
double size,
|
||||||
Color color,
|
Color color,
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'stream_chat_theme.dart';
|
||||||
|
|
||||||
|
///
|
||||||
|
class Swipeable extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
final Widget backgroundIcon;
|
||||||
|
final VoidCallback onSwipeStart;
|
||||||
|
final VoidCallback onSwipeCancel;
|
||||||
|
final VoidCallback onSwipeEnd;
|
||||||
|
final double threshold;
|
||||||
|
|
||||||
|
///
|
||||||
|
const Swipeable({
|
||||||
|
@required this.child,
|
||||||
|
@required this.backgroundIcon,
|
||||||
|
this.onSwipeStart,
|
||||||
|
this.onSwipeCancel,
|
||||||
|
this.onSwipeEnd,
|
||||||
|
this.threshold = 82.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _SwipeableState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
|
||||||
|
double _dragExtent = 0.0;
|
||||||
|
AnimationController _moveController;
|
||||||
|
AnimationController _iconMoveController;
|
||||||
|
Animation<Offset> _moveAnimation;
|
||||||
|
Animation<Offset> _iconTransitionAnimation;
|
||||||
|
Animation<double> _iconFadeAnimation;
|
||||||
|
bool _pastThreshold = false;
|
||||||
|
|
||||||
|
final _animationDuration = const Duration(milliseconds: 200);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_moveController =
|
||||||
|
AnimationController(duration: _animationDuration, vsync: this);
|
||||||
|
_iconMoveController =
|
||||||
|
AnimationController(duration: _animationDuration, vsync: this);
|
||||||
|
_moveAnimation = Tween<Offset>(begin: Offset.zero, end: Offset(1.0, 0.0))
|
||||||
|
.animate(_moveController);
|
||||||
|
_iconTransitionAnimation =
|
||||||
|
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset(0.4, 0.0))
|
||||||
|
.animate(_moveController);
|
||||||
|
_iconFadeAnimation =
|
||||||
|
Tween<double>(begin: 0.7, end: 1.0).animate(_iconMoveController);
|
||||||
|
|
||||||
|
final controllerValue = 0.0;
|
||||||
|
_moveController.animateTo(controllerValue);
|
||||||
|
_iconMoveController.animateTo(controllerValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_moveController.dispose();
|
||||||
|
_iconMoveController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragStart(DragStartDetails details) {
|
||||||
|
if (widget.onSwipeStart != null) {
|
||||||
|
widget.onSwipeStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragUpdate(DragUpdateDetails details) {
|
||||||
|
final delta = details.primaryDelta;
|
||||||
|
_dragExtent += delta;
|
||||||
|
|
||||||
|
if (_dragExtent.isNegative) return;
|
||||||
|
|
||||||
|
var movePastThresholdPixels = widget.threshold;
|
||||||
|
var newPos = _dragExtent.abs() / context.size.width;
|
||||||
|
|
||||||
|
if (_dragExtent.abs() > movePastThresholdPixels) {
|
||||||
|
// how many "thresholds" past the threshold we are. 1 = the threshold 2
|
||||||
|
// = two thresholds.
|
||||||
|
var n = _dragExtent.abs() / movePastThresholdPixels;
|
||||||
|
|
||||||
|
// Take the number of thresholds past the threshold, and reduce this
|
||||||
|
// number
|
||||||
|
var reducedThreshold = math.pow(n, 0.3);
|
||||||
|
|
||||||
|
var adjustedPixelPos = movePastThresholdPixels * reducedThreshold;
|
||||||
|
newPos = adjustedPixelPos / context.size.width;
|
||||||
|
|
||||||
|
if (_dragExtent > 0 && !_pastThreshold) {
|
||||||
|
_iconMoveController.value = 1;
|
||||||
|
_pastThreshold = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Send a cancel event if the user has swiped back underneath the
|
||||||
|
// threshold
|
||||||
|
if (_pastThreshold && widget.onSwipeCancel != null) {
|
||||||
|
widget.onSwipeCancel();
|
||||||
|
}
|
||||||
|
_pastThreshold = false;
|
||||||
|
}
|
||||||
|
if (!_pastThreshold || newPos < _moveController.value) {
|
||||||
|
_iconMoveController.value = newPos;
|
||||||
|
}
|
||||||
|
_moveController.value = newPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragEnd(DragEndDetails details) {
|
||||||
|
_moveController.animateTo(0.0, duration: _animationDuration);
|
||||||
|
_iconMoveController.animateTo(0.0, duration: _animationDuration);
|
||||||
|
_dragExtent = 0.0;
|
||||||
|
if (_pastThreshold && widget.onSwipeEnd != null) {
|
||||||
|
widget.onSwipeEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onHorizontalDragStart: _handleDragStart,
|
||||||
|
onHorizontalDragUpdate: _handleDragUpdate,
|
||||||
|
onHorizontalDragEnd: _handleDragEnd,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
fit: StackFit.passthrough,
|
||||||
|
children: [
|
||||||
|
SlideTransition(
|
||||||
|
position: _iconTransitionAnimation,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
FadeTransition(
|
||||||
|
opacity: _iconFadeAnimation,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: StreamChatTheme.of(context)
|
||||||
|
.colorTheme
|
||||||
|
.greyGainsboro,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: widget.backgroundIcon,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SlideTransition(
|
||||||
|
position: _moveAnimation,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+117
-7
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import 'stream_svg_icon.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
import '../stream_chat_flutter.dart';
|
||||||
|
|
||||||
Future<void> launchURL(BuildContext context, String url) async {
|
Future<void> launchURL(BuildContext context, String url) async {
|
||||||
@@ -97,11 +99,119 @@ Future<bool> showConfirmationDialog(
|
|||||||
String getRandomPicUrl(User user) =>
|
String getRandomPicUrl(User user) =>
|
||||||
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
|
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
|
||||||
|
|
||||||
/// List extension
|
/// Get websiteName from [hostName]
|
||||||
extension ListX<T> on List<T> {
|
String getWebsiteName(String hostName) {
|
||||||
/// Insert any item<T> inBetween the list items
|
switch (hostName) {
|
||||||
List<T> insertBetween(T item) => expand((e) sync* {
|
case 'reddit':
|
||||||
yield item;
|
return 'Reddit';
|
||||||
yield e;
|
case 'youtube':
|
||||||
}).skip(1).toList(growable: false);
|
return 'Youtube';
|
||||||
|
case 'wikipedia':
|
||||||
|
return 'Wikipedia';
|
||||||
|
case 'twitter':
|
||||||
|
return 'Twitter';
|
||||||
|
case 'facebook':
|
||||||
|
return 'Facebook';
|
||||||
|
case 'amazon':
|
||||||
|
return 'Amazon';
|
||||||
|
case 'yelp':
|
||||||
|
return 'Yelp';
|
||||||
|
case 'imdb':
|
||||||
|
return 'IMDB';
|
||||||
|
case 'pinterest':
|
||||||
|
return 'Pinterest';
|
||||||
|
case 'tripadvisor':
|
||||||
|
return 'TripAdvisor';
|
||||||
|
case 'instagram':
|
||||||
|
return 'Instagram';
|
||||||
|
case 'walmart':
|
||||||
|
return 'Walmart';
|
||||||
|
case 'craigslist':
|
||||||
|
return 'Craigslist';
|
||||||
|
case 'ebay':
|
||||||
|
return 'eBay';
|
||||||
|
case 'linkedin':
|
||||||
|
return 'LinkedIn';
|
||||||
|
case 'google':
|
||||||
|
return 'Google';
|
||||||
|
case 'apple':
|
||||||
|
return 'Apple';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
String getSizeText(int bytes) {
|
||||||
|
if (bytes == null) {
|
||||||
|
return 'Size N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes <= 1000) {
|
||||||
|
return '${bytes} bytes';
|
||||||
|
} else if (bytes <= 100000) {
|
||||||
|
return '${(bytes / 1000).toStringAsFixed(2)} KB';
|
||||||
|
} else {
|
||||||
|
return '${(bytes / 1000000).toStringAsFixed(2)} MB';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
StreamSvgIcon getFileTypeImage(String type) {
|
||||||
|
switch (type) {
|
||||||
|
case '7z':
|
||||||
|
return StreamSvgIcon.filetype_7z();
|
||||||
|
break;
|
||||||
|
case 'csv':
|
||||||
|
return StreamSvgIcon.filetype_csv();
|
||||||
|
break;
|
||||||
|
case 'doc':
|
||||||
|
return StreamSvgIcon.filetype_doc();
|
||||||
|
break;
|
||||||
|
case 'docx':
|
||||||
|
return StreamSvgIcon.filetype_docx();
|
||||||
|
break;
|
||||||
|
case 'html':
|
||||||
|
return StreamSvgIcon.filetype_html();
|
||||||
|
break;
|
||||||
|
case 'md':
|
||||||
|
return StreamSvgIcon.filetype_md();
|
||||||
|
break;
|
||||||
|
case 'odt':
|
||||||
|
return StreamSvgIcon.filetype_odt();
|
||||||
|
break;
|
||||||
|
case 'pdf':
|
||||||
|
return StreamSvgIcon.filetype_pdf();
|
||||||
|
break;
|
||||||
|
case 'ppt':
|
||||||
|
return StreamSvgIcon.filetype_ppt();
|
||||||
|
break;
|
||||||
|
case 'pptx':
|
||||||
|
return StreamSvgIcon.filetype_pptx();
|
||||||
|
break;
|
||||||
|
case 'rar':
|
||||||
|
return StreamSvgIcon.filetype_rar();
|
||||||
|
break;
|
||||||
|
case 'rtf':
|
||||||
|
return StreamSvgIcon.filetype_rtf();
|
||||||
|
break;
|
||||||
|
case 'tar':
|
||||||
|
return StreamSvgIcon.filetype_tar();
|
||||||
|
break;
|
||||||
|
case 'txt':
|
||||||
|
return StreamSvgIcon.filetype_txt();
|
||||||
|
break;
|
||||||
|
case 'xls':
|
||||||
|
return StreamSvgIcon.filetype_xls();
|
||||||
|
break;
|
||||||
|
case 'xlsx':
|
||||||
|
return StreamSvgIcon.filetype_xlsx();
|
||||||
|
break;
|
||||||
|
case 'zip':
|
||||||
|
return StreamSvgIcon.filetype_zip();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return StreamSvgIcon.filetype_Generic();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ dependencies:
|
|||||||
file_picker: ^2.0.12
|
file_picker: ^2.0.12
|
||||||
image_picker: ^0.6.7+2
|
image_picker: ^0.6.7+2
|
||||||
flutter_keyboard_visibility: ^3.3.0
|
flutter_keyboard_visibility: ^3.3.0
|
||||||
stream_chat: ^0.2.21+2
|
stream_chat: ^0.2.22
|
||||||
mime: ^0.9.6+3
|
mime: ^0.9.6+3
|
||||||
video_compress: ^2.1.1
|
video_compress: ^2.1.1
|
||||||
visibility_detector: ^0.1.5
|
visibility_detector: ^0.1.5
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ void main() {
|
|||||||
showCopyMessage: false,
|
showCopyMessage: false,
|
||||||
showDeleteMessage: false,
|
showDeleteMessage: false,
|
||||||
showReply: false,
|
showReply: false,
|
||||||
|
showThreadReply: false,
|
||||||
message: Message(
|
message: Message(
|
||||||
text: 'test',
|
text: 'test',
|
||||||
user: User(
|
user: User(
|
||||||
@@ -87,6 +88,7 @@ void main() {
|
|||||||
|
|
||||||
await tester.pump(Duration(milliseconds: 1000));
|
await tester.pump(Duration(milliseconds: 1000));
|
||||||
expect(find.byKey(Key('MessageWidget')), findsOneWidget);
|
expect(find.byKey(Key('MessageWidget')), findsOneWidget);
|
||||||
|
expect(find.text('Reply'), findsNothing);
|
||||||
expect(find.text('Thread reply'), findsNothing);
|
expect(find.text('Thread reply'), findsNothing);
|
||||||
expect(find.text('Edit message'), findsNothing);
|
expect(find.text('Edit message'), findsNothing);
|
||||||
expect(find.text('Delete message'), findsNothing);
|
expect(find.text('Delete message'), findsNothing);
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
latestReactions: [
|
latestReactions: [
|
||||||
Reaction(
|
Reaction(
|
||||||
type: 'thumbs_up',
|
type: 'like',
|
||||||
user: User(id: testUserId),
|
user: User(id: testUserId),
|
||||||
),
|
),
|
||||||
Reaction(
|
Reaction(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'it should show a thumb up',
|
'it should show a like',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final client = MockClient();
|
final client = MockClient();
|
||||||
final clientState = MockClientState();
|
final clientState = MockClientState();
|
||||||
@@ -50,7 +50,7 @@ void main() {
|
|||||||
child: ReactionBubble(
|
child: ReactionBubble(
|
||||||
reactions: [
|
reactions: [
|
||||||
Reaction(
|
Reaction(
|
||||||
type: 'thumbs_up',
|
type: 'like',
|
||||||
user: User(id: 'test'),
|
user: User(id: 'test'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -86,7 +86,7 @@ void main() {
|
|||||||
child: ReactionBubble(
|
child: ReactionBubble(
|
||||||
reactions: [
|
reactions: [
|
||||||
Reaction(
|
Reaction(
|
||||||
type: 'thumbs_up',
|
type: 'like',
|
||||||
user: User(id: 'test'),
|
user: User(id: 'test'),
|
||||||
),
|
),
|
||||||
Reaction(
|
Reaction(
|
||||||
|
|||||||
Reference in New Issue
Block a user