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 {
|
class ChannelPage extends StatefulWidget {
|
||||||
const ChannelPage({
|
@override
|
||||||
Key key,
|
_ChannelPageState createState() => _ChannelPageState();
|
||||||
}) : super(key: key);
|
}
|
||||||
|
|
||||||
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -433,6 +454,8 @@ class ChannelPage extends StatelessWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
MessageListView(
|
MessageListView(
|
||||||
|
onMessageSwiped: _reply,
|
||||||
|
onReplyTap: _reply,
|
||||||
threadBuilder: (_, parentMessage) {
|
threadBuilder: (_, parentMessage) {
|
||||||
return ThreadPage(
|
return ThreadPage(
|
||||||
parent: parentMessage,
|
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(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
child: _getFileTypeImage(attachment.extraData['mime_type']),
|
child: getFileTypeImage(attachment.extraData['mime_type']),
|
||||||
height: 40.0,
|
height: 40.0,
|
||||||
width: 33.33,
|
width: 33.33,
|
||||||
margin: EdgeInsets.all(8.0),
|
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 {
|
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;
|
||||||
@@ -23,6 +24,7 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
final bool showCopyMessage;
|
final bool showCopyMessage;
|
||||||
final bool showEditMessage;
|
final bool showEditMessage;
|
||||||
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;
|
||||||
@@ -34,9 +36,11 @@ 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.showThreadReply = true,
|
||||||
this.showUserAvatar = DisplayWidget.show,
|
this.showUserAvatar = DisplayWidget.show,
|
||||||
this.editMessageInputBuilder,
|
this.editMessageInputBuilder,
|
||||||
this.messageShape,
|
this.messageShape,
|
||||||
@@ -114,6 +118,7 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
showReactions: false,
|
showReactions: false,
|
||||||
showUsername: false,
|
showUsername: false,
|
||||||
showReplyIndicator: false,
|
showReplyIndicator: false,
|
||||||
|
showThreadReplyIndicator: false,
|
||||||
showUserAvatar: showUserAvatar,
|
showUserAvatar: showUserAvatar,
|
||||||
showTimestamp: false,
|
showTimestamp: false,
|
||||||
translateUserAvatar: false,
|
translateUserAvatar: false,
|
||||||
@@ -158,6 +163,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 (showEditMessage)
|
if (showEditMessage)
|
||||||
_buildEditMessage(context),
|
_buildEditMessage(context),
|
||||||
if (showDeleteMessage)
|
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) {
|
Widget _buildDeleteButton(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -313,7 +342,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',
|
||||||
@@ -324,8 +353,8 @@ class MessageActionsModal extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
if (onThreadTap != null) {
|
if (onThreadReplyTap != null) {
|
||||||
onThreadTap(message);
|
onThreadReplyTap(message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+141
-59
@@ -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,9 +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 'package:photo_manager/photo_manager.dart';
|
import 'extension.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
import '../stream_chat_flutter.dart';
|
||||||
|
import 'reply_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);
|
||||||
@@ -108,6 +110,7 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.actionsLocation = ActionsLocation.left,
|
this.actionsLocation = ActionsLocation.left,
|
||||||
this.attachmentThumbnailBuilders,
|
this.attachmentThumbnailBuilders,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
|
this.replyMessage,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Message to edit
|
/// Message to edit
|
||||||
@@ -156,6 +159,9 @@ 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 replyMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MessageInputState createState() => MessageInputState();
|
MessageInputState createState() => MessageInputState();
|
||||||
|
|
||||||
@@ -167,7 +173,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;
|
||||||
@@ -197,9 +203,13 @@ 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;
|
||||||
|
|
||||||
|
Message _replyMessage;
|
||||||
|
|
||||||
|
bool get _hasReplyMessage => _replyMessage != 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) {
|
||||||
@@ -214,8 +224,31 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
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(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: _buildTextField(context),
|
child: _buildTextField(context),
|
||||||
),
|
),
|
||||||
if (widget.parentMessage != null)
|
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) {
|
Flex _buildTextField(BuildContext context) {
|
||||||
@@ -332,6 +373,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
_actionsShrunk = false;
|
_actionsShrunk = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
splashRadius: 24,
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
icon: StreamSvgIcon.emptyCircleLeft(
|
icon: StreamSvgIcon.emptyCircleLeft(
|
||||||
color: StreamChatTheme.of(context).accentColor,
|
color: StreamChatTheme.of(context).accentColor,
|
||||||
),
|
),
|
||||||
@@ -356,12 +400,13 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(20.0),
|
borderRadius: BorderRadius.circular(20.0),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Colors.grey,
|
color: Colors.black.withOpacity(0.16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
_buildRepyToMessage(),
|
||||||
_buildAttachments(),
|
_buildAttachments(),
|
||||||
LimitedBox(
|
LimitedBox(
|
||||||
maxHeight: widget.maxHeight,
|
maxHeight: widget.maxHeight,
|
||||||
@@ -436,6 +481,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Timer _debounce;
|
Timer _debounce;
|
||||||
|
|
||||||
void _onChanged(BuildContext context, String s) {
|
void _onChanged(BuildContext context, String s) {
|
||||||
if (_debounce?.isActive == true) _debounce.cancel();
|
if (_debounce?.isActive == true) _debounce.cancel();
|
||||||
_debounce = Timer(
|
_debounce = Timer(
|
||||||
@@ -799,7 +845,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
Widget _buildPickerSection() {
|
Widget _buildPickerSection() {
|
||||||
var _attachmentContainsFile =
|
var _attachmentContainsFile =
|
||||||
_attachments.any((element) => element.attachment.type == 'file');
|
_attachments.any((element) => element.attachment?.type == 'file');
|
||||||
|
|
||||||
switch (_filePickerIndex) {
|
switch (_filePickerIndex) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -1232,6 +1278,25 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
_commandsOverlay = null;
|
_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() {
|
Widget _buildAttachments() {
|
||||||
return _attachments.isEmpty
|
return _attachments.isEmpty
|
||||||
? Container()
|
? Container()
|
||||||
@@ -1437,57 +1502,68 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCommandButton() {
|
Widget _buildCommandButton() {
|
||||||
return InkWell(
|
return Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.all(8.0),
|
||||||
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: Color(0xFF000000).withAlpha(128),
|
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() {
|
Widget _buildAttachmentButton() {
|
||||||
var padding = widget.editMessage == null ? 4.0 : 8.0;
|
var padding = widget.editMessage == null ? 4.0 : 8.0;
|
||||||
return Center(
|
return Center(
|
||||||
child: InkWell(
|
child: Padding(
|
||||||
child: Padding(
|
padding:
|
||||||
padding:
|
EdgeInsets.only(left: 8.0, right: padding, top: 8.0, bottom: 8.0),
|
||||||
EdgeInsets.only(left: 8.0, right: padding, top: 8.0, bottom: 8.0),
|
child: IconButton(
|
||||||
child: StreamSvgIcon.attach(
|
icon: StreamSvgIcon.attach(
|
||||||
color: _openFilePickerSection
|
color: _openFilePickerSection
|
||||||
? StreamChatTheme.of(context).accentColor
|
? StreamChatTheme.of(context).accentColor
|
||||||
: Color(0xFF000000).withAlpha(128),
|
: Color(0xFF000000).withAlpha(128),
|
||||||
),
|
),
|
||||||
),
|
padding: const EdgeInsets.all(0),
|
||||||
onTap: () async {
|
constraints: BoxConstraints.tightFor(
|
||||||
_emojiOverlay?.remove();
|
height: 24,
|
||||||
_emojiOverlay = null;
|
width: 24,
|
||||||
_commandsOverlay?.remove();
|
),
|
||||||
_commandsOverlay = null;
|
splashRadius: 24,
|
||||||
_mentionsOverlay?.remove();
|
onPressed: () async {
|
||||||
_mentionsOverlay = null;
|
_emojiOverlay?.remove();
|
||||||
|
_emojiOverlay = null;
|
||||||
|
_commandsOverlay?.remove();
|
||||||
|
_commandsOverlay = null;
|
||||||
|
_mentionsOverlay?.remove();
|
||||||
|
_mentionsOverlay = null;
|
||||||
|
|
||||||
if (_openFilePickerSection) {
|
if (_openFilePickerSection) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_animateContainer = true;
|
_animateContainer = true;
|
||||||
_openFilePickerSection = false;
|
_openFilePickerSection = false;
|
||||||
_filePickerSize = _kMinMediaPickerSize;
|
_filePickerSize = _kMinMediaPickerSize;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
showAttachmentModal();
|
showAttachmentModal();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1786,15 +1862,11 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: InkWell(
|
|
||||||
onTap: () {
|
|
||||||
sendMessage();
|
|
||||||
},
|
|
||||||
child: StreamSvgIcon(
|
child: StreamSvgIcon(
|
||||||
assetName: _getIdleSendIcon(),
|
assetName: _getIdleSendIcon(),
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
)),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1802,11 +1874,16 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: InkWell(
|
child: IconButton(
|
||||||
onTap: () {
|
onPressed: sendMessage,
|
||||||
sendMessage();
|
visualDensity: VisualDensity.compact,
|
||||||
},
|
padding: const EdgeInsets.all(0),
|
||||||
child: StreamSvgIcon(
|
splashRadius: 24,
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
),
|
||||||
|
icon: StreamSvgIcon(
|
||||||
assetName: _getSendIcon(),
|
assetName: _getSendIcon(),
|
||||||
color: StreamChatTheme.of(context).accentColor,
|
color: StreamChatTheme.of(context).accentColor,
|
||||||
),
|
),
|
||||||
@@ -1918,6 +1995,8 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_replyMessage = widget.replyMessage;
|
||||||
|
|
||||||
_focusNode = widget.focusNode ?? FocusNode();
|
_focusNode = widget.focusNode ?? FocusNode();
|
||||||
|
|
||||||
_emojiNames = Emoji.all().map((e) => e.name);
|
_emojiNames = Emoji.all().map((e) => e.name);
|
||||||
@@ -1970,6 +2049,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
if (widget.editMessage != null && !_initialized) {
|
if (widget.editMessage != null && !_initialized) {
|
||||||
@@ -1978,6 +2058,14 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(MessageInput oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.replyMessage?.id != _replyMessage?.id) {
|
||||||
|
_replyMessage = widget.replyMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SendingAttachment {
|
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.
|
/// 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
|
||||||
|
|||||||
@@ -13,6 +13,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,
|
||||||
@@ -26,6 +27,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;
|
||||||
@@ -106,12 +110,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 AlwaysScrollableScrollPhysics(),
|
||||||
this.initialScrollIndex = 0,
|
this.initialScrollIndex = 0,
|
||||||
this.initialAlignment = 0,
|
this.initialAlignment = 0,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
this.itemPositionListener,
|
this.itemPositionListener,
|
||||||
|
this.onMessageSwiped,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Function used to build a custom message widget
|
/// Function used to build a custom message widget
|
||||||
@@ -153,6 +159,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;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageListViewState createState() => _MessageListViewState();
|
_MessageListViewState createState() => _MessageListViewState();
|
||||||
}
|
}
|
||||||
@@ -273,7 +285,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,
|
||||||
@@ -540,6 +552,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
return MessageWidget(
|
return MessageWidget(
|
||||||
showReplyIndicator: false,
|
showReplyIndicator: false,
|
||||||
|
showThreadReplyIndicator: false,
|
||||||
message: message,
|
message: message,
|
||||||
reverse: isMyMessage,
|
reverse: isMyMessage,
|
||||||
showUsername: !isMyMessage,
|
showUsername: !isMyMessage,
|
||||||
@@ -597,47 +610,54 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||||
|
|
||||||
return MessageWidget(
|
return Swipeable(
|
||||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
onSwipeEnd: () => widget.onMessageSwiped(message),
|
||||||
message: message,
|
backgroundIcon: StreamSvgIcon.reply(
|
||||||
reverse: isMyMessage,
|
color: StreamChatTheme.of(context).accentColor,
|
||||||
showReactions: !message.isDeleted,
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: 8.0,
|
|
||||||
right: 8.0,
|
|
||||||
bottom: index == 0 ? 30 : (isNextUser ? 5 : 10),
|
|
||||||
),
|
),
|
||||||
showUsername: !isMyMessage && !isNextUser,
|
child: MessageWidget(
|
||||||
showSendingIndicator: isMyMessage &&
|
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||||
(index == 0 || message.status != MessageSendingStatus.SENT)
|
message: message,
|
||||||
? DisplayWidget.show
|
reverse: isMyMessage,
|
||||||
: DisplayWidget.hide,
|
showReactions: !message.isDeleted,
|
||||||
showTimestamp: !isNextUser || readList?.isNotEmpty == true,
|
padding: EdgeInsets.only(
|
||||||
showEditMessage: isMyMessage,
|
left: 8.0,
|
||||||
showDeleteMessage: isMyMessage,
|
right: 8.0,
|
||||||
borderSide: isMyMessage ? BorderSide.none : null,
|
bottom: index == 0 ? 30 : (isNextUser ? 5 : 10),
|
||||||
onThreadTap: _onThreadTap,
|
),
|
||||||
attachmentBorderRadiusGeometry: BorderRadius.only(
|
showUsername: !isMyMessage && !isNextUser,
|
||||||
topLeft: Radius.circular(16),
|
showSendingIndicator: isMyMessage &&
|
||||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
(index == 0 || message.status != MessageSendingStatus.SENT)
|
||||||
topRight: Radius.circular(16),
|
? DisplayWidget.show
|
||||||
bottomRight: Radius.circular(16),
|
: 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,
|
showReactions: false,
|
||||||
showUsername: false,
|
showUsername: false,
|
||||||
showUserAvatar: showUserAvatar,
|
showUserAvatar: showUserAvatar,
|
||||||
showReplyIndicator: false,
|
showThreadReplyIndicator: false,
|
||||||
showTimestamp: false,
|
showTimestamp: false,
|
||||||
translateUserAvatar: false,
|
translateUserAvatar: false,
|
||||||
showSendingIndicator: DisplayWidget.gone,
|
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_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/reply_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);
|
||||||
|
|
||||||
@@ -46,6 +48,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;
|
||||||
|
|
||||||
@@ -55,6 +58,9 @@ class MessageWidget extends StatefulWidget {
|
|||||||
/// The message
|
/// The message
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
||||||
|
/// The replyMessage
|
||||||
|
final Message replyMessage;
|
||||||
|
|
||||||
/// The message theme
|
/// The message theme
|
||||||
final MessageTheme messageTheme;
|
final MessageTheme messageTheme;
|
||||||
|
|
||||||
@@ -99,6 +105,9 @@ class MessageWidget extends StatefulWidget {
|
|||||||
|
|
||||||
final bool allRead;
|
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
|
/// If true the widget will show the reply indicator
|
||||||
final bool showReplyIndicator;
|
final bool showReplyIndicator;
|
||||||
|
|
||||||
@@ -127,6 +136,7 @@ class MessageWidget extends StatefulWidget {
|
|||||||
Key key,
|
Key key,
|
||||||
@required this.message,
|
@required this.message,
|
||||||
@required this.messageTheme,
|
@required this.messageTheme,
|
||||||
|
this.replyMessage,
|
||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
this.translateUserAvatar = true,
|
this.translateUserAvatar = true,
|
||||||
this.shape,
|
this.shape,
|
||||||
@@ -140,6 +150,8 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.showUserAvatar = DisplayWidget.show,
|
this.showUserAvatar = DisplayWidget.show,
|
||||||
this.showSendingIndicator = DisplayWidget.show,
|
this.showSendingIndicator = DisplayWidget.show,
|
||||||
this.showReplyIndicator = true,
|
this.showReplyIndicator = true,
|
||||||
|
this.showThreadReplyIndicator = true,
|
||||||
|
this.onReplyTap,
|
||||||
this.onThreadTap,
|
this.onThreadTap,
|
||||||
this.showUsername = true,
|
this.showUsername = true,
|
||||||
this.showTimestamp = true,
|
this.showTimestamp = true,
|
||||||
@@ -213,6 +225,8 @@ class MessageWidget extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MessageWidgetState extends State<MessageWidget> {
|
class _MessageWidgetState extends State<MessageWidget> {
|
||||||
|
bool get _hasReplyMessage => widget.replyMessage != null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var leftPadding = widget.showUserAvatar != DisplayWidget.gone
|
var leftPadding = widget.showUserAvatar != DisplayWidget.gone
|
||||||
@@ -230,6 +244,9 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
widget.message.attachments?.any((element) => element.type == 'file') ==
|
widget.message.attachments?.any((element) => element.type == 'file') ==
|
||||||
true;
|
true;
|
||||||
|
|
||||||
|
final isMyMessage =
|
||||||
|
widget.message.user.id == StreamChat.of(context).user.id;
|
||||||
|
|
||||||
return Portal(
|
return Portal(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: widget.padding ?? EdgeInsets.all(8),
|
padding: widget.padding ?? EdgeInsets.all(8),
|
||||||
@@ -306,7 +323,8 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
shape: widget.shape ??
|
shape: widget.shape ??
|
||||||
RoundedRectangleBorder(
|
RoundedRectangleBorder(
|
||||||
side: isOnlyEmoji
|
side: isOnlyEmoji &&
|
||||||
|
!_hasReplyMessage
|
||||||
? BorderSide.none
|
? BorderSide.none
|
||||||
: widget.borderSide ??
|
: widget.borderSide ??
|
||||||
BorderSide(
|
BorderSide(
|
||||||
@@ -333,6 +351,19 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
CrossAxisAlignment.start,
|
CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
if (_hasReplyMessage)
|
||||||
|
ReplyMessageWidget(
|
||||||
|
message:
|
||||||
|
widget.replyMessage,
|
||||||
|
messageTheme: isMyMessage
|
||||||
|
? StreamChatTheme.of(
|
||||||
|
context)
|
||||||
|
.otherMessageTheme
|
||||||
|
: StreamChatTheme.of(
|
||||||
|
context)
|
||||||
|
.ownMessageTheme,
|
||||||
|
reverse: widget.reverse,
|
||||||
|
),
|
||||||
..._parseAttachments(context),
|
..._parseAttachments(context),
|
||||||
if (widget.message.text
|
if (widget.message.text
|
||||||
.trim()
|
.trim()
|
||||||
@@ -367,7 +398,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (widget.showReplyIndicator &&
|
if (widget.showThreadReplyIndicator &&
|
||||||
widget.message.replyCount > 0)
|
widget.message.replyCount > 0)
|
||||||
_buildReplyIndicator(leftPadding),
|
_buildReplyIndicator(leftPadding),
|
||||||
],
|
],
|
||||||
@@ -393,7 +424,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(
|
||||||
@@ -562,14 +593,16 @@ 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,
|
||||||
showEditMessage: widget.showEditMessage &&
|
showEditMessage: widget.showEditMessage &&
|
||||||
widget.message.attachments
|
widget.message.attachments
|
||||||
?.any((element) => element.type == 'giphy') !=
|
?.any((element) => element.type == 'giphy') !=
|
||||||
true,
|
true,
|
||||||
showReactions: widget.showReactions,
|
showReactions: widget.showReactions,
|
||||||
showReply:
|
showReply: widget.showReplyIndicator,
|
||||||
widget.showReplyIndicator && widget.onThreadTap != null,
|
showThreadReply:
|
||||||
|
widget.showThreadReplyIndicator && widget.onThreadTap != null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -878,6 +911,10 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
final isOnlyEmoji =
|
final isOnlyEmoji =
|
||||||
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
|
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||||
|
|
||||||
|
if (_hasReplyMessage) {
|
||||||
|
return widget.messageTheme.messageBackgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
if ((widget.message.status == MessageSendingStatus.FAILED ||
|
if ((widget.message.status == MessageSendingStatus.FAILED ||
|
||||||
widget.message.status == MessageSendingStatus.FAILED_UPDATE ||
|
widget.message.status == MessageSendingStatus.FAILED_UPDATE ||
|
||||||
widget.message.status == MessageSendingStatus.FAILED_DELETE)) {
|
widget.message.status == MessageSendingStatus.FAILED_DELETE)) {
|
||||||
@@ -918,45 +955,4 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
factory StreamSvgIcon.edit({
|
||||||
double size,
|
double size,
|
||||||
Color color,
|
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:stream_chat/stream_chat.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import 'stream_svg_icon.dart';
|
||||||
|
|
||||||
Future<void> launchURL(BuildContext context, String url) async {
|
Future<void> launchURL(BuildContext context, String url) async {
|
||||||
if (await canLaunch(url)) {
|
if (await canLaunch(url)) {
|
||||||
await launch(url);
|
await launch(url);
|
||||||
@@ -47,3 +49,105 @@ Future<bool> showConfirmationDialog(
|
|||||||
/// Get random png with initials
|
/// Get random png with initials
|
||||||
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}';
|
||||||
|
|
||||||
|
/// 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