Add Message Reply feature
Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
+31
-5
@@ -415,10 +415,31 @@ class ChannelQuerySearchResultPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelPage extends StatelessWidget {
|
||||
const ChannelPage({
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
class ChannelPage extends StatefulWidget {
|
||||
@override
|
||||
_ChannelPageState createState() => _ChannelPageState();
|
||||
}
|
||||
|
||||
class _ChannelPageState extends State<ChannelPage> {
|
||||
Message _replyMessage;
|
||||
FocusNode _focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _reply(Message message) {
|
||||
setState(() => _replyMessage = message);
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -433,6 +454,8 @@ class ChannelPage extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
MessageListView(
|
||||
onMessageSwiped: _reply,
|
||||
onReplyTap: _reply,
|
||||
threadBuilder: (_, parentMessage) {
|
||||
return ThreadPage(
|
||||
parent: parentMessage,
|
||||
@@ -458,7 +481,10 @@ class ChannelPage extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
MessageInput(),
|
||||
MessageInput(
|
||||
focusNode: _focusNode,
|
||||
replyMessage: _replyMessage,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
extension StringExtension on String {
|
||||
String capitalize() {
|
||||
return "${this[0].toUpperCase()}${this.substring(1)}";
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class FileAttachment extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
child: _getFileTypeImage(attachment.extraData['mime_type']),
|
||||
child: getFileTypeImage(attachment.extraData['mime_type']),
|
||||
height: 40.0,
|
||||
width: 33.33,
|
||||
margin: EdgeInsets.all(8.0),
|
||||
@@ -116,63 +116,4 @@ class FileAttachment extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import 'stream_chat_theme.dart';
|
||||
|
||||
class MessageActionsModal extends StatelessWidget {
|
||||
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 MessageTheme messageTheme;
|
||||
final bool showReactions;
|
||||
@@ -23,6 +24,7 @@ class MessageActionsModal extends StatelessWidget {
|
||||
final bool showCopyMessage;
|
||||
final bool showEditMessage;
|
||||
final bool showReply;
|
||||
final bool showThreadReply;
|
||||
final bool reverse;
|
||||
final ShapeBorder messageShape;
|
||||
final DisplayWidget showUserAvatar;
|
||||
@@ -34,9 +36,11 @@ class MessageActionsModal extends StatelessWidget {
|
||||
this.showReactions = true,
|
||||
this.showDeleteMessage = true,
|
||||
this.showEditMessage = true,
|
||||
this.onThreadTap,
|
||||
this.onReplyTap,
|
||||
this.onThreadReplyTap,
|
||||
this.showCopyMessage = true,
|
||||
this.showReply = true,
|
||||
this.showThreadReply = true,
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.editMessageInputBuilder,
|
||||
this.messageShape,
|
||||
@@ -114,6 +118,7 @@ class MessageActionsModal extends StatelessWidget {
|
||||
showReactions: false,
|
||||
showUsername: false,
|
||||
showReplyIndicator: false,
|
||||
showThreadReplyIndicator: false,
|
||||
showUserAvatar: showUserAvatar,
|
||||
showTimestamp: false,
|
||||
translateUserAvatar: false,
|
||||
@@ -158,6 +163,12 @@ class MessageActionsModal extends StatelessWidget {
|
||||
message.status == null) &&
|
||||
message.parentId == null)
|
||||
_buildReplyButton(context),
|
||||
if (showThreadReply &&
|
||||
(message.status ==
|
||||
MessageSendingStatus.SENT ||
|
||||
message.status == null) &&
|
||||
message.parentId == null)
|
||||
_buildThreadReplyButton(context),
|
||||
if (showEditMessage)
|
||||
_buildEditMessage(context),
|
||||
if (showDeleteMessage)
|
||||
@@ -181,6 +192,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) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
@@ -313,7 +342,7 @@ class MessageActionsModal extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReplyButton(BuildContext context) {
|
||||
Widget _buildThreadReplyButton(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'Thread reply',
|
||||
@@ -324,8 +353,8 @@ class MessageActionsModal extends StatelessWidget {
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
if (onThreadTap != null) {
|
||||
onThreadTap(message);
|
||||
if (onThreadReplyTap != null) {
|
||||
onThreadReplyTap(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
+141
-59
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
@@ -21,9 +22,10 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||
import 'package:substring_highlight/substring_highlight.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'extension.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'reply_message_widget.dart';
|
||||
import 'stream_channel.dart';
|
||||
|
||||
typedef FileUploader = Future<String> Function(PlatformFile, Channel);
|
||||
@@ -108,6 +110,7 @@ class MessageInput extends StatefulWidget {
|
||||
this.actionsLocation = ActionsLocation.left,
|
||||
this.attachmentThumbnailBuilders,
|
||||
this.focusNode,
|
||||
this.replyMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Message to edit
|
||||
@@ -156,6 +159,9 @@ class MessageInput extends StatefulWidget {
|
||||
/// The focus node associated to the TextField
|
||||
final FocusNode focusNode;
|
||||
|
||||
///
|
||||
final Message replyMessage;
|
||||
|
||||
@override
|
||||
MessageInputState createState() => MessageInputState();
|
||||
|
||||
@@ -167,7 +173,7 @@ class MessageInput extends StatefulWidget {
|
||||
|
||||
if (messageInputState == null) {
|
||||
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;
|
||||
@@ -197,9 +203,13 @@ class MessageInputState extends State<MessageInput> {
|
||||
/// The editing controller passed to the input TextField
|
||||
TextEditingController textEditingController;
|
||||
|
||||
Message _replyMessage;
|
||||
|
||||
bool get _hasReplyMessage => _replyMessage != null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
Widget child = SafeArea(
|
||||
child: GestureDetector(
|
||||
onPanUpdate: (details) {
|
||||
if (details.delta.dy > 0) {
|
||||
@@ -214,8 +224,31 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_hasReplyMessage)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: StreamSvgIcon.reply(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Reply to Message',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: StreamSvgIcon.close_small(),
|
||||
onPressed: () {
|
||||
setState(() => _replyMessage = null);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: _buildTextField(context),
|
||||
),
|
||||
if (widget.parentMessage != null)
|
||||
@@ -228,6 +261,14 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
),
|
||||
);
|
||||
if (widget.editMessage == null) {
|
||||
child = Material(
|
||||
color: Colors.white,
|
||||
elevation: 8,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
Flex _buildTextField(BuildContext context) {
|
||||
@@ -332,6 +373,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
_actionsShrunk = false;
|
||||
});
|
||||
},
|
||||
visualDensity: VisualDensity.compact,
|
||||
splashRadius: 24,
|
||||
padding: const EdgeInsets.all(0),
|
||||
icon: StreamSvgIcon.emptyCircleLeft(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
),
|
||||
@@ -356,12 +400,13 @@ class MessageInputState extends State<MessageInput> {
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
border: Border.all(
|
||||
color: Colors.grey,
|
||||
color: Colors.black.withOpacity(0.16),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildRepyToMessage(),
|
||||
_buildAttachments(),
|
||||
LimitedBox(
|
||||
maxHeight: widget.maxHeight,
|
||||
@@ -436,6 +481,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
Timer _debounce;
|
||||
|
||||
void _onChanged(BuildContext context, String s) {
|
||||
if (_debounce?.isActive == true) _debounce.cancel();
|
||||
_debounce = Timer(
|
||||
@@ -799,7 +845,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
Widget _buildPickerSection() {
|
||||
var _attachmentContainsFile =
|
||||
_attachments.any((element) => element.attachment.type == 'file');
|
||||
_attachments.any((element) => element.attachment?.type == 'file');
|
||||
|
||||
switch (_filePickerIndex) {
|
||||
case 0:
|
||||
@@ -1232,6 +1278,25 @@ class MessageInputState extends State<MessageInput> {
|
||||
_commandsOverlay = null;
|
||||
}
|
||||
|
||||
Widget _buildRepyToMessage() {
|
||||
if (!_hasReplyMessage) {
|
||||
return Offstage();
|
||||
}
|
||||
final containsUrl = _replyMessage.attachments
|
||||
?.any((element) => element.ogScrapeUrl != null) ==
|
||||
true;
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(pi),
|
||||
alignment: Alignment.center,
|
||||
child: ReplyMessageWidget(
|
||||
reverse: true,
|
||||
showBorder: !containsUrl,
|
||||
message: _replyMessage,
|
||||
messageTheme: StreamChatTheme.of(context).otherMessageTheme,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttachments() {
|
||||
return _attachments.isEmpty
|
||||
? Container()
|
||||
@@ -1437,57 +1502,68 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
Widget _buildCommandButton() {
|
||||
return InkWell(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 4.0, right: 8.0, top: 8.0, bottom: 8.0),
|
||||
child: StreamSvgIcon.lightning(
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: IconButton(
|
||||
icon: StreamSvgIcon.lightning(
|
||||
color: Color(0xFF000000).withAlpha(128),
|
||||
),
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
splashRadius: 24,
|
||||
onPressed: () {
|
||||
if (_commandsOverlay == null) {
|
||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||
Overlay.of(context).insert(_commandsOverlay);
|
||||
} else {
|
||||
_commandsOverlay?.remove();
|
||||
_commandsOverlay = null;
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
if (_commandsOverlay == null) {
|
||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||
Overlay.of(context).insert(_commandsOverlay);
|
||||
} else {
|
||||
_commandsOverlay?.remove();
|
||||
_commandsOverlay = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttachmentButton() {
|
||||
var padding = widget.editMessage == null ? 4.0 : 8.0;
|
||||
return Center(
|
||||
child: InkWell(
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsets.only(left: 8.0, right: padding, top: 8.0, bottom: 8.0),
|
||||
child: StreamSvgIcon.attach(
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsets.only(left: 8.0, right: padding, top: 8.0, bottom: 8.0),
|
||||
child: IconButton(
|
||||
icon: StreamSvgIcon.attach(
|
||||
color: _openFilePickerSection
|
||||
? StreamChatTheme.of(context).accentColor
|
||||
: Color(0xFF000000).withAlpha(128),
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
_emojiOverlay?.remove();
|
||||
_emojiOverlay = null;
|
||||
_commandsOverlay?.remove();
|
||||
_commandsOverlay = null;
|
||||
_mentionsOverlay?.remove();
|
||||
_mentionsOverlay = null;
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
splashRadius: 24,
|
||||
onPressed: () async {
|
||||
_emojiOverlay?.remove();
|
||||
_emojiOverlay = null;
|
||||
_commandsOverlay?.remove();
|
||||
_commandsOverlay = null;
|
||||
_mentionsOverlay?.remove();
|
||||
_mentionsOverlay = null;
|
||||
|
||||
if (_openFilePickerSection) {
|
||||
setState(() {
|
||||
_animateContainer = true;
|
||||
_openFilePickerSection = false;
|
||||
_filePickerSize = _kMinMediaPickerSize;
|
||||
});
|
||||
} else {
|
||||
showAttachmentModal();
|
||||
}
|
||||
},
|
||||
if (_openFilePickerSection) {
|
||||
setState(() {
|
||||
_animateContainer = true;
|
||||
_openFilePickerSection = false;
|
||||
_filePickerSize = _kMinMediaPickerSize;
|
||||
});
|
||||
} else {
|
||||
showAttachmentModal();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1786,15 +1862,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
sendMessage();
|
||||
},
|
||||
child: StreamSvgIcon(
|
||||
assetName: _getIdleSendIcon(),
|
||||
color: Colors.grey,
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1802,11 +1874,16 @@ class MessageInputState extends State<MessageInput> {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
sendMessage();
|
||||
},
|
||||
child: StreamSvgIcon(
|
||||
child: IconButton(
|
||||
onPressed: sendMessage,
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: const EdgeInsets.all(0),
|
||||
splashRadius: 24,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
icon: StreamSvgIcon(
|
||||
assetName: _getSendIcon(),
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
),
|
||||
@@ -1918,6 +1995,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_replyMessage = widget.replyMessage;
|
||||
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
|
||||
_emojiNames = Emoji.all().map((e) => e.name);
|
||||
@@ -1970,6 +2049,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
bool _initialized = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
if (widget.editMessage != null && !_initialized) {
|
||||
@@ -1978,6 +2058,14 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MessageInput oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.replyMessage?.id != _replyMessage?.id) {
|
||||
_replyMessage = widget.replyMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SendingAttachment {
|
||||
@@ -1994,12 +2082,6 @@ class _SendingAttachment {
|
||||
});
|
||||
}
|
||||
|
||||
extension StringExtension on String {
|
||||
String capitalize() {
|
||||
return "${this[0].toUpperCase()}${this.substring(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a 2-tuple, or pair.
|
||||
class Tuple2<T1, T2> {
|
||||
/// Returns the first item of the tuple
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:visibility_detector/visibility_detector.dart';
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'date_divider.dart';
|
||||
import 'stream_channel.dart';
|
||||
import 'swipeable.dart';
|
||||
|
||||
typedef MessageBuilder = Widget Function(
|
||||
BuildContext,
|
||||
@@ -26,6 +27,9 @@ typedef ParentMessageBuilder = Widget Function(
|
||||
typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
|
||||
typedef ThreadTapCallback = void Function(Message, Widget);
|
||||
|
||||
typedef OnMessageSwiped = void Function(Message);
|
||||
typedef ReplyTapCallback = void Function(Message);
|
||||
|
||||
class MessageDetails {
|
||||
/// True if the message belongs to the current user
|
||||
bool isMyMessage;
|
||||
@@ -106,12 +110,14 @@ class MessageListView extends StatefulWidget {
|
||||
this.parentMessage,
|
||||
this.threadBuilder,
|
||||
this.onThreadTap,
|
||||
this.onReplyTap,
|
||||
this.dateDividerBuilder,
|
||||
this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
|
||||
this.initialScrollIndex = 0,
|
||||
this.initialAlignment = 0,
|
||||
this.scrollController,
|
||||
this.itemPositionListener,
|
||||
this.onMessageSwiped,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Function used to build a custom message widget
|
||||
@@ -153,6 +159,12 @@ class MessageListView extends StatefulWidget {
|
||||
/// The ScrollPhysics used by the ListView
|
||||
final ScrollPhysics scrollPhysics;
|
||||
|
||||
/// Called when message item gets swiped
|
||||
final OnMessageSwiped onMessageSwiped;
|
||||
|
||||
///
|
||||
final ReplyTapCallback onReplyTap;
|
||||
|
||||
@override
|
||||
_MessageListViewState createState() => _MessageListViewState();
|
||||
}
|
||||
@@ -273,7 +285,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
if (widget.messageBuilder != null) {
|
||||
messageWidget = Builder(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
builder: (_) => widget.messageBuilder(
|
||||
builder: (context) => widget.messageBuilder(
|
||||
context,
|
||||
MessageDetails(
|
||||
context,
|
||||
@@ -540,6 +552,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
return MessageWidget(
|
||||
showReplyIndicator: false,
|
||||
showThreadReplyIndicator: false,
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showUsername: !isMyMessage,
|
||||
@@ -597,47 +610,54 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||
|
||||
return MessageWidget(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showReactions: !message.isDeleted,
|
||||
padding: EdgeInsets.only(
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
bottom: index == 0 ? 30 : (isNextUser ? 5 : 10),
|
||||
return Swipeable(
|
||||
onSwipeEnd: () => widget.onMessageSwiped(message),
|
||||
backgroundIcon: StreamSvgIcon.reply(
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
),
|
||||
showUsername: !isMyMessage && !isNextUser,
|
||||
showSendingIndicator: isMyMessage &&
|
||||
(index == 0 || message.status != MessageSendingStatus.SENT)
|
||||
? DisplayWidget.show
|
||||
: DisplayWidget.hide,
|
||||
showTimestamp: !isNextUser || readList?.isNotEmpty == true,
|
||||
showEditMessage: isMyMessage,
|
||||
showDeleteMessage: isMyMessage,
|
||||
borderSide: isMyMessage ? BorderSide.none : null,
|
||||
onThreadTap: _onThreadTap,
|
||||
attachmentBorderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
child: MessageWidget(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showReactions: !message.isDeleted,
|
||||
padding: EdgeInsets.only(
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
bottom: index == 0 ? 30 : (isNextUser ? 5 : 10),
|
||||
),
|
||||
showUsername: !isMyMessage && !isNextUser,
|
||||
showSendingIndicator: isMyMessage &&
|
||||
(index == 0 || message.status != MessageSendingStatus.SENT)
|
||||
? DisplayWidget.show
|
||||
: DisplayWidget.hide,
|
||||
showTimestamp: !isNextUser || readList?.isNotEmpty == true,
|
||||
showEditMessage: isMyMessage,
|
||||
showDeleteMessage: isMyMessage,
|
||||
borderSide: isMyMessage ? BorderSide.none : null,
|
||||
onThreadTap: _onThreadTap,
|
||||
onReplyTap: widget.onReplyTap,
|
||||
attachmentBorderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
attachmentPadding: const EdgeInsets.all(2),
|
||||
borderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
showUserAvatar: isMyMessage
|
||||
? DisplayWidget.gone
|
||||
: (isNextUser ? DisplayWidget.hide : DisplayWidget.show),
|
||||
messageTheme: isMyMessage
|
||||
? StreamChatTheme.of(context).ownMessageTheme
|
||||
: StreamChatTheme.of(context).otherMessageTheme,
|
||||
readList: readList,
|
||||
allRead: allRead,
|
||||
),
|
||||
attachmentPadding: const EdgeInsets.all(2),
|
||||
borderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
showUserAvatar: isMyMessage
|
||||
? DisplayWidget.gone
|
||||
: (isNextUser ? DisplayWidget.hide : DisplayWidget.show),
|
||||
messageTheme: isMyMessage
|
||||
? StreamChatTheme.of(context).ownMessageTheme
|
||||
: StreamChatTheme.of(context).otherMessageTheme,
|
||||
readList: readList,
|
||||
allRead: allRead,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
showReactions: false,
|
||||
showUsername: false,
|
||||
showUserAvatar: showUserAvatar,
|
||||
showReplyIndicator: false,
|
||||
showThreadReplyIndicator: false,
|
||||
showTimestamp: false,
|
||||
translateUserAvatar: false,
|
||||
showSendingIndicator: DisplayWidget.gone,
|
||||
|
||||
+43
-47
@@ -11,11 +11,13 @@ import 'package:jiffy/jiffy.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/reaction_bubble.dart';
|
||||
import 'package:stream_chat_flutter/src/reply_message_widget.dart';
|
||||
import 'package:stream_chat_flutter/src/url_attachment.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'image_group.dart';
|
||||
import 'message_text.dart';
|
||||
import 'extension.dart';
|
||||
|
||||
typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment);
|
||||
|
||||
@@ -46,6 +48,7 @@ class MessageWidget extends StatefulWidget {
|
||||
|
||||
/// The function called when tapping on replies
|
||||
final void Function(Message) onThreadTap;
|
||||
final void Function(Message) onReplyTap;
|
||||
final Widget Function(BuildContext, Message) editMessageInputBuilder;
|
||||
final Widget Function(BuildContext, Message) textBuilder;
|
||||
|
||||
@@ -55,6 +58,9 @@ class MessageWidget extends StatefulWidget {
|
||||
/// The message
|
||||
final Message message;
|
||||
|
||||
/// The replyMessage
|
||||
final Message replyMessage;
|
||||
|
||||
/// The message theme
|
||||
final MessageTheme messageTheme;
|
||||
|
||||
@@ -99,6 +105,9 @@ class MessageWidget extends StatefulWidget {
|
||||
|
||||
final bool allRead;
|
||||
|
||||
/// If true the widget will show the thread reply indicator
|
||||
final bool showThreadReplyIndicator;
|
||||
|
||||
/// If true the widget will show the reply indicator
|
||||
final bool showReplyIndicator;
|
||||
|
||||
@@ -127,6 +136,7 @@ class MessageWidget extends StatefulWidget {
|
||||
Key key,
|
||||
@required this.message,
|
||||
@required this.messageTheme,
|
||||
this.replyMessage,
|
||||
this.reverse = false,
|
||||
this.translateUserAvatar = true,
|
||||
this.shape,
|
||||
@@ -140,6 +150,8 @@ class MessageWidget extends StatefulWidget {
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.showSendingIndicator = DisplayWidget.show,
|
||||
this.showReplyIndicator = true,
|
||||
this.showThreadReplyIndicator = true,
|
||||
this.onReplyTap,
|
||||
this.onThreadTap,
|
||||
this.showUsername = true,
|
||||
this.showTimestamp = true,
|
||||
@@ -213,6 +225,8 @@ class MessageWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MessageWidgetState extends State<MessageWidget> {
|
||||
bool get _hasReplyMessage => widget.replyMessage != null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var leftPadding = widget.showUserAvatar != DisplayWidget.gone
|
||||
@@ -230,6 +244,9 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
widget.message.attachments?.any((element) => element.type == 'file') ==
|
||||
true;
|
||||
|
||||
final isMyMessage =
|
||||
widget.message.user.id == StreamChat.of(context).user.id;
|
||||
|
||||
return Portal(
|
||||
child: Padding(
|
||||
padding: widget.padding ?? EdgeInsets.all(8),
|
||||
@@ -306,7 +323,8 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: widget.shape ??
|
||||
RoundedRectangleBorder(
|
||||
side: isOnlyEmoji
|
||||
side: isOnlyEmoji &&
|
||||
!_hasReplyMessage
|
||||
? BorderSide.none
|
||||
: widget.borderSide ??
|
||||
BorderSide(
|
||||
@@ -333,6 +351,19 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (_hasReplyMessage)
|
||||
ReplyMessageWidget(
|
||||
message:
|
||||
widget.replyMessage,
|
||||
messageTheme: isMyMessage
|
||||
? StreamChatTheme.of(
|
||||
context)
|
||||
.otherMessageTheme
|
||||
: StreamChatTheme.of(
|
||||
context)
|
||||
.ownMessageTheme,
|
||||
reverse: widget.reverse,
|
||||
),
|
||||
..._parseAttachments(context),
|
||||
if (widget.message.text
|
||||
.trim()
|
||||
@@ -367,7 +398,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.showReplyIndicator &&
|
||||
if (widget.showThreadReplyIndicator &&
|
||||
widget.message.replyCount > 0)
|
||||
_buildReplyIndicator(leftPadding),
|
||||
],
|
||||
@@ -393,7 +424,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
var splitList = host.split('.');
|
||||
var hostName = splitList.length == 3 ? splitList[1] : splitList[0];
|
||||
var hostDisplayName = urlAttachment.authorName?.capitalize() ??
|
||||
_getWebsiteName(hostName.toLowerCase()) ??
|
||||
getWebsiteName(hostName.toLowerCase()) ??
|
||||
hostName.capitalize();
|
||||
|
||||
return UrlAttachment(
|
||||
@@ -562,14 +593,16 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
showDeleteMessage: widget.showDeleteMessage,
|
||||
message: widget.message,
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
onThreadTap: widget.onThreadTap,
|
||||
onReplyTap: widget.onReplyTap,
|
||||
onThreadReplyTap: widget.onThreadTap,
|
||||
showEditMessage: widget.showEditMessage &&
|
||||
widget.message.attachments
|
||||
?.any((element) => element.type == 'giphy') !=
|
||||
true,
|
||||
showReactions: widget.showReactions,
|
||||
showReply:
|
||||
widget.showReplyIndicator && widget.onThreadTap != null,
|
||||
showReply: widget.showReplyIndicator,
|
||||
showThreadReply:
|
||||
widget.showThreadReplyIndicator && widget.onThreadTap != null,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -878,6 +911,10 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
final isOnlyEmoji =
|
||||
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||
|
||||
if (_hasReplyMessage) {
|
||||
return widget.messageTheme.messageBackgroundColor;
|
||||
}
|
||||
|
||||
if ((widget.message.status == MessageSendingStatus.FAILED ||
|
||||
widget.message.status == MessageSendingStatus.FAILED_UPDATE ||
|
||||
widget.message.status == MessageSendingStatus.FAILED_DELETE)) {
|
||||
@@ -918,45 +955,4 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
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 'attachment_error.dart';
|
||||
import 'image_attachment.dart';
|
||||
import 'message_text.dart';
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'user_avatar.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
///
|
||||
class ReplyMessageWidget 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;
|
||||
|
||||
final Map<String, Widget Function(Attachment attachment)> _attachmentBuilders;
|
||||
|
||||
///
|
||||
ReplyMessageWidget({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@required this.messageTheme,
|
||||
this.reverse = false,
|
||||
this.showBorder = false,
|
||||
this.textLimit = 170,
|
||||
}) : _attachmentBuilders = {
|
||||
'image': (attachment) {
|
||||
return ImageAttachment(
|
||||
attachment: attachment,
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
size: Size(32, 32),
|
||||
);
|
||||
},
|
||||
'video': (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.thumbUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
);
|
||||
},
|
||||
'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']),
|
||||
);
|
||||
},
|
||||
},
|
||||
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 Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 6, right: 4, left: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(child: _buildMessage(context)),
|
||||
SizedBox(width: 4),
|
||||
_buildUserAvatar(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessage(BuildContext context) {
|
||||
final children = [
|
||||
if (_hasAttachments) ...[
|
||||
_parseAttachments(context),
|
||||
SizedBox(width: 8),
|
||||
],
|
||||
Expanded(child: _buildTextMessage()),
|
||||
];
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: 48.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getBackgroundColor(),
|
||||
border: showBorder ? Border.all(color: Colors.black.withOpacity(0.08)) : null,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(12),
|
||||
topLeft: Radius.circular(12),
|
||||
bottomLeft: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
reverse ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: reverse ? children.reversed.toList() : children,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextMessage() {
|
||||
final isOnlyEmoji =
|
||||
message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||
var msg = _hasAttachments && !_containsText
|
||||
? message.copyWith(text: message.attachments.last?.title ?? 'File')
|
||||
: message;
|
||||
if (msg.text.length > textLimit) {
|
||||
msg = msg.copyWith(text: '${msg.text.substring(0, textLimit - 3)}...');
|
||||
}
|
||||
return 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
attachment = message.attachments.last;
|
||||
final attachmentBuilder = _attachmentBuilders[attachment.type];
|
||||
if (attachmentBuilder == null) {
|
||||
child = Offstage();
|
||||
}
|
||||
child = attachmentBuilder(attachment);
|
||||
}
|
||||
return 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: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white.withAlpha(24)
|
||||
: Colors.black.withAlpha(24),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getBackgroundColor() {
|
||||
if (_containsScrapeUrl) {
|
||||
return Color(0xFFE9F2FF);
|
||||
}
|
||||
return messageTheme.messageBackgroundColor;
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
double size,
|
||||
Color color,
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.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) {
|
||||
print(_moveAnimation.value.dx);
|
||||
|
||||
final delta = details.primaryDelta;
|
||||
if (delta.isNegative) return;
|
||||
|
||||
_dragExtent += delta;
|
||||
|
||||
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(
|
||||
margin: const EdgeInsets.all(8),
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
),
|
||||
),
|
||||
child: widget.backgroundIcon,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SlideTransition(
|
||||
position: _moveAnimation,
|
||||
child: widget.child,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'stream_svg_icon.dart';
|
||||
|
||||
Future<void> launchURL(BuildContext context, String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
@@ -47,3 +49,105 @@ Future<bool> showConfirmationDialog(
|
||||
/// Get random png with initials
|
||||
String getRandomPicUrl(User user) =>
|
||||
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
|
||||
|
||||
/// Get websiteName from [hostName]
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user