Merge pull request #3 from GetStream/feature/editMessage

Edit message and commands
This commit is contained in:
Salvatore Giordano
2020-03-04 10:41:23 +01:00
committed by GitHub
9 changed files with 896 additions and 394 deletions
+1
View File
@@ -0,0 +1 @@
include ':app'
+34
View File
@@ -1,10 +1,22 @@
PODS: PODS:
- file_picker (0.0.1):
- Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_plugin_android_lifecycle (0.0.1):
- Flutter
- FMDB (2.7.5): - FMDB (2.7.5):
- FMDB/standard (= 2.7.5) - FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5) - FMDB/standard (2.7.5)
- image_picker (0.0.1):
- Flutter
- keyboard_visibility (0.5.0):
- Flutter
- Reachability
- path_provider (0.0.1): - path_provider (0.0.1):
- Flutter - Flutter
- path_provider_macos (0.0.1):
- Flutter
- Reachability (3.2)
- screen (0.0.1): - screen (0.0.1):
- Flutter - Flutter
- sqflite (0.0.1): - sqflite (0.0.1):
@@ -22,8 +34,13 @@ PODS:
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_plugin_android_lifecycle (from `.symlinks/plugins/flutter_plugin_android_lifecycle/ios`)
- image_picker (from `.symlinks/plugins/image_picker/ios`)
- keyboard_visibility (from `.symlinks/plugins/keyboard_visibility/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`)
- path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`)
- screen (from `.symlinks/plugins/screen/ios`) - screen (from `.symlinks/plugins/screen/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher (from `.symlinks/plugins/url_launcher/ios`) - url_launcher (from `.symlinks/plugins/url_launcher/ios`)
@@ -35,12 +52,23 @@ DEPENDENCIES:
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- FMDB - FMDB
- Reachability
EXTERNAL SOURCES: EXTERNAL SOURCES:
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_plugin_android_lifecycle:
:path: ".symlinks/plugins/flutter_plugin_android_lifecycle/ios"
image_picker:
:path: ".symlinks/plugins/image_picker/ios"
keyboard_visibility:
:path: ".symlinks/plugins/keyboard_visibility/ios"
path_provider: path_provider:
:path: ".symlinks/plugins/path_provider/ios" :path: ".symlinks/plugins/path_provider/ios"
path_provider_macos:
:path: ".symlinks/plugins/path_provider_macos/ios"
screen: screen:
:path: ".symlinks/plugins/screen/ios" :path: ".symlinks/plugins/screen/ios"
sqflite: sqflite:
@@ -57,9 +85,15 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/video_player_web/ios" :path: ".symlinks/plugins/video_player_web/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
file_picker: 408623be2125b79a4539cf703be3d4b3abe5e245
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
flutter_plugin_android_lifecycle: 47de533a02850f070f5696a623995e93eddcdb9b
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_picker: e3eacd46b94694dde7cf2705955cece853aa1a8f
keyboard_visibility: 96a24de806fe6823c3ad956c01ba2ec6d056616f
path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d
path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
screen: abd91ca7bf3426e1cc3646d27e9b2358d6bf07b0 screen: abd91ca7bf3426e1cc3646d27e9b2358d6bf07b0
sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0 sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0
url_launcher: a1c0cc845906122c4784c542523d8cacbded5626 url_launcher: a1c0cc845906122c4784c542523d8cacbded5626
+1 -1
View File
@@ -89,7 +89,7 @@ class ChannelPreview extends StatelessWidget {
initialData: channel.state.typingEvents, initialData: channel.state.typingEvents,
builder: (context, snapshot) { builder: (context, snapshot) {
final typings = snapshot.data; final typings = snapshot.data;
final opacity = channel.state.unreadCount > .0 ? 1.0 : 0.5; final opacity = channel.state.unreadCount > 0 ? 1.0 : 0.5;
return typings.isNotEmpty return typings.isNotEmpty
? _buildTypings(typings, context, opacity) ? _buildTypings(typings, context, opacity)
: _buildLastMessage(context, opacity); : _buildLastMessage(context, opacity);
+298 -67
View File
@@ -4,14 +4,18 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:keyboard_visibility/keyboard_visibility.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import '../stream_chat_flutter.dart';
import 'stream_channel.dart'; import 'stream_channel.dart';
/// Inactive state
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input_paint.png)
/// Focused state
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input2.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input2.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input2_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input2_paint.png)
/// ///
@@ -55,8 +59,12 @@ class MessageInput extends StatefulWidget {
Key key, Key key,
this.onMessageSent, this.onMessageSent,
this.parentMessage, this.parentMessage,
this.editMessage,
}) : super(key: key); }) : super(key: key);
/// Message to edit
final Message editMessage;
/// Function called after sending the message /// Function called after sending the message
final void Function(Message) onMessageSent; final void Function(Message) onMessageSent;
@@ -68,10 +76,13 @@ class MessageInput extends StatefulWidget {
} }
class _MessageInputState extends State<MessageInput> { class _MessageInputState extends State<MessageInput> {
final _textController = TextEditingController(); final List<_SendingAttachment> _attachments = [];
final _focusNode = FocusNode();
TextEditingController _textController;
bool _messageIsPresent = false; bool _messageIsPresent = false;
bool _typingStarted = false; bool _typingStarted = false;
final List<_SendingAttachment> _attachments = []; OverlayEntry _commandsOverlay;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -81,45 +92,45 @@ class _MessageInputState extends State<MessageInput> {
child: Stack( child: Stack(
overflow: Overflow.visible, overflow: Overflow.visible,
children: <Widget>[ children: <Widget>[
Positioned.fill( _buildBorder(context),
child: Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
gradient: _typingStarted
? StreamChatTheme.of(context).channelTheme.inputGradient
: null,
),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.0),
),
child: Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context)
.channelTheme
.inputBackground,
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: _typingStarted
? Colors.transparent
: Colors.black.withOpacity(.2)),
),
),
),
),
),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildAttachments(), _buildAttachments(),
Flex( _buildTextField(context),
],
),
],
),
),
);
}
Flex _buildTextField(BuildContext context) {
return Flex(
direction: Axis.horizontal, direction: Axis.horizontal,
children: <Widget>[ children: <Widget>[
_buildAttachmentButton(), _buildAttachmentButton(),
Expanded( _buildTextInput(context),
_animateSendButton(context),
],
);
}
AnimatedCrossFade _animateSendButton(BuildContext context) {
return AnimatedCrossFade(
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: _buildSendButton(context),
secondChild: SizedBox(),
duration: Duration(milliseconds: 300),
alignment: Alignment.center,
);
}
Expanded _buildTextInput(BuildContext context) {
return Expanded(
child: TextField( child: TextField(
minLines: null, minLines: null,
maxLines: null, maxLines: null,
@@ -127,11 +138,21 @@ class _MessageInputState extends State<MessageInput> {
_sendMessage(context); _sendMessage(context);
}, },
controller: _textController, controller: _textController,
focusNode: _focusNode,
onChanged: (s) { onChanged: (s) {
StreamChannel.of(context).channel.keyStroke(); StreamChannel.of(context).channel.keyStroke();
setState(() { setState(() {
_messageIsPresent = s.trim().isNotEmpty; _messageIsPresent = s.trim().isNotEmpty;
}); });
_commandsOverlay?.remove();
_commandsOverlay = null;
if (s.startsWith('/')) {
_commandsOverlay = _buildOverlayEntry();
Overlay.of(context).insert(_commandsOverlay);
}
}, },
onTap: () { onTap: () {
setState(() { setState(() {
@@ -146,27 +167,135 @@ class _MessageInputState extends State<MessageInput> {
border: InputBorder.none, border: InputBorder.none,
), ),
), ),
);
}
Positioned _buildBorder(BuildContext context) {
return Positioned.fill(
child: Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
gradient: _getGradient(context),
), ),
AnimatedCrossFade( child: Container(
crossFadeState: decoration: BoxDecoration(
(_messageIsPresent || _attachments.isNotEmpty) color: Colors.white,
? CrossFadeState.showFirst borderRadius: BorderRadius.circular(10.0),
: CrossFadeState.showSecond,
firstChild: _buildSendButton(context),
secondChild: SizedBox(),
duration: Duration(milliseconds: 300),
alignment: Alignment.center,
), ),
], child: Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context).channelTheme.inputBackground,
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: _typingStarted
? Colors.transparent
: Colors.black.withOpacity(.2)),
), ),
],
), ),
],
), ),
), ),
); );
} }
OverlayEntry _buildOverlayEntry() {
final text = _textController.text;
final commands = StreamChannel.of(context)
.channel
.config
.commands
.where((c) => c.name.startsWith(text.replaceFirst('/', '')))
.toList();
RenderBox renderBox = context.findRenderObject();
final size = renderBox.size;
return OverlayEntry(builder: (context) {
return Positioned(
bottom: size.height + MediaQuery.of(context).viewInsets.bottom,
left: 0,
right: 0,
child: Material(
color: StreamChatTheme.of(context).primaryColor,
child: Container(
constraints: BoxConstraints.loose(Size.fromHeight(400)),
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
spreadRadius: -8,
blurRadius: 5.0,
offset: Offset(0, -4),
),
],
color: StreamChatTheme.of(context).primaryColor,
),
child: Flex(
mainAxisSize: MainAxisSize.min,
direction: Axis.vertical,
children: <Widget>[
ListView(
padding: const EdgeInsets.all(0),
shrinkWrap: true,
children: commands
.map((c) => ListTile(
title: Text.rich(
TextSpan(
text: '${c.name}',
style: TextStyle(fontWeight: FontWeight.bold),
children: [
TextSpan(
text: ' ${c.args}',
style: TextStyle(
fontWeight: FontWeight.w300,
),
),
],
),
),
subtitle: Text(c.description),
onTap: () {
_setCommand(c);
},
))
.toList(),
),
],
),
),
),
);
});
}
void _setCommand(Command c) {
setState(() {
_textController.value = TextEditingValue(
text: '/${c.name} ',
selection: TextSelection.fromPosition(
TextPosition(
offset: c.name.length + 2,
),
),
);
});
_commandsOverlay?.remove();
_commandsOverlay = null;
}
Gradient _getGradient(BuildContext context) {
if (_typingStarted) {
if (widget.editMessage == null) {
return StreamChatTheme.of(context).channelTheme.inputGradient;
}
return LinearGradient(
colors: [Colors.lightGreen, Colors.green],
);
} else {
return null;
}
}
Widget _buildAttachments() { Widget _buildAttachments() {
return Wrap( return Wrap(
direction: Axis.horizontal, direction: Axis.horizontal,
@@ -234,9 +363,14 @@ class _MessageInputState extends State<MessageInput> {
Widget _buildAttachment(_SendingAttachment attachment) { Widget _buildAttachment(_SendingAttachment attachment) {
switch (attachment.type) { switch (attachment.type) {
case FileType.IMAGE: case FileType.IMAGE:
return Image.file( return attachment.file != null
? Image.file(
attachment.file, attachment.file,
fit: BoxFit.cover, fit: BoxFit.cover,
)
: Image.network(
attachment.url,
fit: BoxFit.cover,
); );
break; break;
case FileType.VIDEO: case FileType.VIDEO:
@@ -262,6 +396,16 @@ class _MessageInputState extends State<MessageInput> {
color: Colors.transparent, color: Colors.transparent,
child: IconButton( child: IconButton(
onPressed: () { onPressed: () {
_showAttachmentModal();
},
icon: Icon(
Icons.add_circle_outline,
),
),
);
}
void _showAttachmentModal() {
showModalBottomSheet( showModalBottomSheet(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@@ -327,12 +471,6 @@ class _MessageInputState extends State<MessageInput> {
], ],
); );
}); });
},
icon: Icon(
Icons.add_circle_outline,
),
),
);
} }
void _pickFile(FileType type, bool camera) async { void _pickFile(FileType type, bool camera) async {
@@ -412,15 +550,42 @@ class _MessageInputState extends State<MessageInput> {
_messageIsPresent = false; _messageIsPresent = false;
_typingStarted = false; _typingStarted = false;
}); });
_commandsOverlay?.remove();
_commandsOverlay = null;
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
StreamChannel.of(context) if (widget.editMessage != null) {
.channel final message = widget.editMessage.copyWith(
.sendMessage(
Message(
parentId: widget.parentMessage?.id, parentId: widget.parentMessage?.id,
text: text, text: text,
attachments: attachments.map((attachment) { attachments: widget.editMessage.attachments
.where((attachment) => attachment.type == 'giphy')
.toList() +
_getAttachments(attachments).toList(),
);
StreamChat.of(context).client.updateMessage(message).then((_) {
if (widget.onMessageSent != null) {
widget.onMessageSent(message);
}
});
} else {
final message = Message(
parentId: widget.parentMessage?.id,
text: text,
attachments: _getAttachments(attachments).toList(),
);
StreamChannel.of(context).channel.sendMessage(message).then((_) {
if (widget.onMessageSent != null) {
widget.onMessageSent(message);
}
});
}
}
Iterable<Attachment> _getAttachments(List<_SendingAttachment> attachments) {
return attachments.map((attachment) {
String type; String type;
switch (attachment.type) { switch (attachment.type) {
case FileType.IMAGE: case FileType.IMAGE:
@@ -433,19 +598,84 @@ class _MessageInputState extends State<MessageInput> {
type = 'file'; type = 'file';
} }
return Attachment( return Attachment(
imageUrl: imageUrl: attachment.type == FileType.IMAGE ? attachment.url : null,
attachment.type == FileType.IMAGE ? attachment.url : null,
assetUrl: attachment.url, assetUrl: attachment.url,
type: type, type: type,
); );
}).toList(), });
), }
)
.then((_) { int _keyboardListener;
if (widget.onMessageSent != null) {
widget.onMessageSent(Message(text: text)); @override
void initState() {
super.initState();
_keyboardListener = KeyboardVisibilityNotification().addNewListener(
onHide: () {
if (_commandsOverlay != null) {
_commandsOverlay.remove();
}
},
onShow: () {
if (_commandsOverlay != null) {
if (_textController.text.startsWith('/')) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_commandsOverlay = _buildOverlayEntry();
Overlay.of(context).insert(_commandsOverlay);
});
}
}
},
);
if (widget.editMessage != null) {
_textController = TextEditingController(text: widget.editMessage.text);
_typingStarted = true;
_messageIsPresent = true;
widget.editMessage.attachments.forEach((attachment) {
if (attachment.type == 'image') {
_attachments.add(_SendingAttachment(
type: FileType.IMAGE,
url: attachment.imageUrl,
uploaded: true,
));
} else if (attachment.type == 'video') {
_attachments.add(_SendingAttachment(
type: FileType.VIDEO,
url: attachment.assetUrl,
uploaded: true,
));
} else if (attachment.type != 'giphy') {
_attachments.add(_SendingAttachment(
type: FileType.ANY,
url: attachment.assetUrl,
uploaded: true,
));
} }
}); });
} else {
_textController = TextEditingController();
}
}
@override
void dispose() {
_commandsOverlay?.remove();
KeyboardVisibilityNotification().removeListener(_keyboardListener);
super.dispose();
}
bool _initialized = false;
@override
void didChangeDependencies() {
if (widget.editMessage != null && !_initialized) {
FocusScope.of(context).requestFocus(_focusNode);
_initialized = true;
}
super.didChangeDependencies();
} }
} }
@@ -453,11 +683,12 @@ class _SendingAttachment {
final File file; final File file;
final FileType type; final FileType type;
String url; String url;
bool uploaded = false; bool uploaded;
_SendingAttachment({ _SendingAttachment({
this.url, this.url,
this.file, this.file,
this.type, this.type,
this.uploaded = false,
}); });
} }
+271 -114
View File
@@ -8,7 +8,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/message_input.dart';
import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart';
import 'package:stream_chat_flutter/src/reaction_picker.dart';
import 'package:stream_chat_flutter/src/stream_channel.dart'; import 'package:stream_chat_flutter/src/stream_channel.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart';
@@ -33,9 +35,13 @@ class MessageWidget extends StatefulWidget {
@required this.message, @required this.message,
@required this.nextMessage, @required this.nextMessage,
this.onThreadTap, this.onThreadTap,
this.onMessageActions,
this.isParent = false, this.isParent = false,
}) : super(key: key); }) : super(key: key);
/// Function called on long press
final Function(BuildContext, Message) onMessageActions;
/// This message /// This message
final Message message; final Message message;
@@ -63,6 +69,7 @@ class _MessageWidgetState extends State<MessageWidget>
MessageTheme messageTheme; MessageTheme messageTheme;
StreamChatState streamChat; StreamChatState streamChat;
StreamChannelState streamChannel; StreamChannelState streamChannel;
bool isMyMessage;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -75,9 +82,10 @@ class _MessageWidgetState extends State<MessageWidget>
final messageUserId = widget.message.user.id; final messageUserId = widget.message.user.id;
final previousUserId = widget.previousMessage?.user?.id; final previousUserId = widget.previousMessage?.user?.id;
final nextUserId = widget.nextMessage?.user?.id; final nextUserId = widget.nextMessage?.user?.id;
final isMyMessage = messageUserId == currentUserId;
final isLastUser = previousUserId == messageUserId; final isLastUser = previousUserId == messageUserId;
final isNextUser = nextUserId == messageUserId; final isNextUser = nextUserId == messageUserId;
isMyMessage = messageUserId == currentUserId;
final alignment = final alignment =
isMyMessage ? Alignment.centerRight : Alignment.centerLeft; isMyMessage ? Alignment.centerRight : Alignment.centerLeft;
@@ -95,11 +103,11 @@ class _MessageWidgetState extends State<MessageWidget>
crossAxisAlignment: crossAxisAlignment:
isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
_buildBubble(context, isMyMessage, isLastUser), _buildBubble(context, isLastUser),
streamChannel.channel.config.replies streamChannel.channel.config.replies
? _buildThreadIndicator(context, isMyMessage) ? _buildThreadIndicator(context)
: SizedBox(), : SizedBox(),
isNextUser ? SizedBox() : _buildTimestamp(isMyMessage, alignment), isNextUser ? SizedBox() : _buildTimestamp(alignment),
], ],
), ),
isNextUser isNextUser
@@ -161,7 +169,7 @@ class _MessageWidgetState extends State<MessageWidget>
); );
} }
Widget _buildThreadIndicator(BuildContext context, bool isMyMessage) { Widget _buildThreadIndicator(BuildContext context) {
var row = [ var row = [
Text( Text(
'Replies: ${widget.message.replyCount}', 'Replies: ${widget.message.replyCount}',
@@ -203,7 +211,6 @@ class _MessageWidgetState extends State<MessageWidget>
Widget _buildBubble( Widget _buildBubble(
BuildContext context, BuildContext context,
bool isMyMessage,
bool isLastUser, bool isLastUser,
) { ) {
var nOfAttachmentWidgets = 0; var nOfAttachmentWidgets = 0;
@@ -216,17 +223,20 @@ class _MessageWidgetState extends State<MessageWidget>
Widget attachmentWidget; Widget attachmentWidget;
if (attachment.type == 'video') { if (attachment.type == 'video') {
attachmentWidget = _buildVideo(attachment, isMyMessage, isLastUser); attachmentWidget = _buildVideo(attachment, isLastUser);
} else if (attachment.type == 'image' || attachment.type == 'giphy') { } else if (attachment.type == 'image' || attachment.type == 'giphy') {
attachmentWidget = _buildImage(isMyMessage, isLastUser, attachment); attachmentWidget = _buildImage(isLastUser, attachment);
} }
if (attachmentWidget != null) { if (attachmentWidget != null) {
final boxDecoration = _buildBoxDecoration(isMyMessage, isLastUser) final boxDecoration = _buildBoxDecoration(isLastUser)
.copyWith(color: Color(0xffebebeb)); .copyWith(color: Color(0xffebebeb));
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 2.0), padding: const EdgeInsets.only(bottom: 2.0),
child: ClipRRect( child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ClipRRect(
borderRadius: boxDecoration.borderRadius, borderRadius: boxDecoration.borderRadius,
child: Container( child: Container(
decoration: boxDecoration, decoration: boxDecoration,
@@ -252,8 +262,8 @@ class _MessageWidgetState extends State<MessageWidget>
Text( Text(
attachment.title, attachment.title,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: style: messageTheme.messageText
messageTheme.messageText.copyWith( .copyWith(
color: Colors.blue, color: Colors.blue,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -278,12 +288,14 @@ class _MessageWidgetState extends State<MessageWidget>
: SizedBox(), : SizedBox(),
], ],
), ),
attachment.type == 'image' && attachment.titleLink != null attachment.type == 'image' &&
attachment.titleLink != null
? Positioned.fill( ? Positioned.fill(
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: () => _launchURL(attachment.titleLink), onTap: () =>
_launchURL(attachment.titleLink),
), ),
), ),
) )
@@ -295,6 +307,51 @@ class _MessageWidgetState extends State<MessageWidget>
), ),
), ),
), ),
if (attachment.actions != null)
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: attachment.actions?.map((action) {
if (action.style == 'primary') {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: FlatButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Text('${action.text}'),
color: action.style == 'primary'
? StreamChatTheme.of(context).accentColor
: null,
textColor: Colors.white,
onPressed: () {
streamChannel.channel
.sendAction(widget.message.id, {
action.name: action.value,
});
},
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: OutlineButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Text('${action.text}'),
color: StreamChatTheme.of(context).accentColor,
onPressed: () {
streamChannel.channel
.sendAction(widget.message.id, {
action.name: action.value,
});
},
),
);
})?.toList(),
),
],
),
); );
} }
@@ -306,15 +363,14 @@ class _MessageWidgetState extends State<MessageWidget>
if (widget.message.text.trim().isNotEmpty) { if (widget.message.text.trim().isNotEmpty) {
column.children.addAll( column.children.addAll(
<Widget>[ <Widget>[
IntrinsicWidth( Column(
child: Column( crossAxisAlignment:
crossAxisAlignment: isMyMessage isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
streamChannel.channel.config.reactions (streamChannel.channel.config.reactions &&
nOfAttachmentWidgets == 0)
? Align( ? Align(
child: _buildReactions(isMyMessage), child: _buildReactions(),
alignment: isMyMessage alignment: isMyMessage
? Alignment.centerLeft ? Alignment.centerLeft
: Alignment.centerRight, : Alignment.centerRight,
@@ -323,15 +379,8 @@ class _MessageWidgetState extends State<MessageWidget>
Stack( Stack(
overflow: Overflow.visible, overflow: Overflow.visible,
children: <Widget>[ children: <Widget>[
widget.message.reactionCounts?.isNotEmpty == true nOfAttachmentWidgets == 0
? Positioned( ? _buildReactionPaint()
left: isMyMessage ? 8 : null,
right: !isMyMessage ? 8 : null,
top: -6,
child: CustomPaint(
painter: ReactionBubblePainter(),
),
)
: SizedBox(), : SizedBox(),
Padding( Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@@ -339,7 +388,7 @@ class _MessageWidgetState extends State<MessageWidget>
left: isMyMessage ? 8.0 : 0.0, left: isMyMessage ? 8.0 : 0.0,
), ),
child: Container( child: Container(
decoration: _buildBoxDecoration(isMyMessage, decoration: _buildBoxDecoration(
isLastUser || nOfAttachmentWidgets > 0), isLastUser || nOfAttachmentWidgets > 0),
padding: EdgeInsets.all(10), padding: EdgeInsets.all(10),
constraints: BoxConstraints.loose(Size.fromWidth(300)), constraints: BoxConstraints.loose(Size.fromWidth(300)),
@@ -354,10 +403,10 @@ class _MessageWidgetState extends State<MessageWidget>
bodyColor: messageTheme.messageText.color, bodyColor: messageTheme.messageText.color,
decoration: decoration:
messageTheme.messageText.decoration, messageTheme.messageText.decoration,
decorationColor: messageTheme decorationColor:
.messageText.decorationColor, messageTheme.messageText.decorationColor,
decorationStyle: messageTheme decorationStyle:
.messageText.decorationStyle, messageTheme.messageText.decorationStyle,
fontFamily: fontFamily:
messageTheme.messageText.fontFamily, messageTheme.messageText.fontFamily,
), ),
@@ -372,19 +421,65 @@ class _MessageWidgetState extends State<MessageWidget>
), ),
], ],
), ),
],
);
}
if (nOfAttachmentWidgets > 0) {
column.children.insert(
0,
streamChannel.channel.config.reactions
? Align(
child: _buildReactions(),
alignment:
isMyMessage ? Alignment.centerLeft : Alignment.centerRight,
)
: SizedBox(),
);
column.children[1] = Stack(
overflow: Overflow.visible,
children: <Widget>[
Padding(
padding: EdgeInsets.only(
right: isMyMessage ? 0.0 : 8.0,
left: isMyMessage ? 8.0 : 0.0,
), ),
child: column.children[1],
),
_buildReactionPaint(),
], ],
); );
} }
return GestureDetector( return GestureDetector(
child: column, child: IntrinsicWidth(child: column),
onLongPress: () { onLongPress: () {
_showMessageBottomSheet(context, isMyMessage); if (widget.message.type == 'ephemeral') {
return;
}
if (widget.onMessageActions != null) {
widget.onMessageActions(context, widget.message);
} else {
_showMessageBottomSheet(context);
}
}); });
} }
void _showMessageBottomSheet(BuildContext context, bool isMyMessage) { Widget _buildReactionPaint() {
return widget.message.reactionCounts?.isNotEmpty == true
? Positioned(
left: isMyMessage ? 8 : null,
right: !isMyMessage ? 8 : null,
top: -6,
child: CustomPaint(
painter: _ReactionBubblePainter(),
),
)
: SizedBox();
}
void _showMessageBottomSheet(BuildContext context) {
if (!streamChannel.channel.config.reactions && if (!streamChannel.channel.config.reactions &&
!streamChannel.channel.config.replies) { !streamChannel.channel.config.replies) {
return; return;
@@ -399,11 +494,7 @@ class _MessageWidgetState extends State<MessageWidget>
), ),
), ),
context: context, context: context,
builder: (context) { builder: (_) {
final fontSize = 30.0;
final textStyle = TextStyle(
fontSize: fontSize,
);
return SafeArea( return SafeArea(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -412,49 +503,10 @@ class _MessageWidgetState extends State<MessageWidget>
Container( Container(
color: Colors.black87, color: Colors.black87,
child: streamChannel.channel.config.reactions child: streamChannel.channel.config.reactions
? Row( ? ReactionPicker(
crossAxisAlignment: CrossAxisAlignment.start, channel: StreamChannel.of(context).channel,
mainAxisAlignment: MainAxisAlignment.center, reactionToEmoji: reactionToEmoji,
mainAxisSize: MainAxisSize.min, message: widget.message,
children: reactionToEmoji.keys.map((reactionType) {
final ownReactionIndex = widget.message.ownReactions
.indexWhere((reaction) =>
reaction.type == reactionType);
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
IconButton(
iconSize: fontSize + 10,
icon: Text(
reactionToEmoji[reactionType],
style: textStyle,
),
onPressed: () {
if (ownReactionIndex != -1) {
removeReaction(reactionType);
} else {
sendReaction(reactionType);
}
},
),
ownReactionIndex != -1
? Padding(
padding:
const EdgeInsets.only(bottom: 4.0),
child: Text(
widget
.message
.ownReactions[ownReactionIndex]
.score
.toString(),
style: TextStyle(color: Colors.white),
),
)
: SizedBox(),
],
);
}).toList(),
) )
: SizedBox(), : SizedBox(),
), ),
@@ -478,6 +530,22 @@ class _MessageWidgetState extends State<MessageWidget>
}, },
) )
: SizedBox(), : SizedBox(),
isMyMessage
? FlatButton(
child: Padding(
padding: const EdgeInsets.all(28.0),
child: Text(
'Edit message',
style: Theme.of(context).textTheme.headline,
),
),
onPressed: () async {
Navigator.pop(context);
_showEditBottomSheet(context);
},
)
: SizedBox(),
streamChannel.channel.config.replies streamChannel.channel.config.replies
? FlatButton( ? FlatButton(
child: Padding( child: Padding(
@@ -499,10 +567,102 @@ class _MessageWidgetState extends State<MessageWidget>
}); });
} }
Widget _buildReactions(bool isMyMessage) { void _showEditBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
elevation: 2,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32),
),
),
builder: (context) {
return StreamChannel(
channel: streamChannel.channel,
child: Flex(
direction: Axis.vertical,
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(
top: 16.0,
left: 16.0,
right: 16.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'Edit message',
style: Theme.of(context).textTheme.title,
),
Container(
height: 30,
padding: const EdgeInsets.all(2.0),
child: AspectRatio(
aspectRatio: 1,
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0,
onPressed: () {
Navigator.of(context).pop();
},
fillColor: Colors.black.withOpacity(.1),
padding: EdgeInsets.all(4),
child: Icon(
Icons.close,
size: 15,
color: Colors.black,
),
),
),
),
],
),
),
Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: MessageInput(
editMessage: widget.message,
parentMessage: widget.isParent
? StreamChannel.of(context)
.channel
.state
.messages
.firstWhere((message) =>
message.id == widget.message.parentId)
: null,
onMessageSent: (_) {
Navigator.pop(context);
},
),
),
],
),
);
},
);
}
Widget _buildReactions() {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
_showMessageBottomSheet(context, isMyMessage); if (widget.onMessageActions != null) {
widget.onMessageActions(context, widget.message);
} else {
_showMessageBottomSheet(context);
}
}, },
child: Padding( child: Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
@@ -527,14 +687,16 @@ class _MessageWidgetState extends State<MessageWidget>
} }
Row _buildReactionRow() { Row _buildReactionRow() {
final List<Widget> children = return Row(
widget.message.reactionCounts.keys.map((reactionType) { mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
...widget.message.reactionCounts.keys.map((reactionType) {
return Text( return Text(
reactionToEmoji[reactionType] ?? '?', reactionToEmoji[reactionType] ?? '?',
) as Widget; );
}).toList(); }),
Padding(
children.add(Padding(
padding: const EdgeInsets.only(left: 4.0), padding: const EdgeInsets.only(left: 4.0),
child: Text( child: Text(
widget.message.reactionCounts.values widget.message.reactionCounts.values
@@ -542,11 +704,8 @@ class _MessageWidgetState extends State<MessageWidget>
.toString(), .toString(),
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white),
), ),
)); ),
return Row( ],
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: children,
); );
} }
@@ -559,18 +718,7 @@ class _MessageWidgetState extends State<MessageWidget>
'wow': '😲', 'wow': '😲',
}; };
void sendReaction(String reactionType) {
streamChannel.channel.sendReaction(widget.message.id, reactionType);
Navigator.of(context).pop();
}
void removeReaction(String reactionType) {
streamChannel.channel.deleteReaction(widget.message.id, reactionType);
Navigator.of(context).pop();
}
Widget _buildImage( Widget _buildImage(
bool isMyMessage,
bool isLastUser, bool isLastUser,
Attachment attachment, Attachment attachment,
) { ) {
@@ -578,12 +726,21 @@ class _MessageWidgetState extends State<MessageWidget>
imageUrl: imageUrl:
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl, attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl,
fit: BoxFit.cover, fit: BoxFit.cover,
placeholderFadeInDuration: Duration(milliseconds: 300),
placeholder: (_, __) {
return SizedBox(
height: 200,
width: 300,
child: Center(
child: CircularProgressIndicator(),
),
);
},
); );
} }
Widget _buildVideo( Widget _buildVideo(
Attachment attachment, Attachment attachment,
bool isMyMessage,
bool isLastUser, bool isLastUser,
) { ) {
VideoPlayerController videoController; VideoPlayerController videoController;
@@ -653,7 +810,7 @@ class _MessageWidgetState extends State<MessageWidget>
super.dispose(); super.dispose();
} }
Widget _buildTimestamp(bool isMyMessage, Alignment alignment) { Widget _buildTimestamp(Alignment alignment) {
return Padding( return Padding(
padding: const EdgeInsets.only(top: 5.0), padding: const EdgeInsets.only(top: 5.0),
child: Text( child: Text(
@@ -663,7 +820,7 @@ class _MessageWidgetState extends State<MessageWidget>
); );
} }
BoxDecoration _buildBoxDecoration(bool isMyMessage, bool isLastUser) { BoxDecoration _buildBoxDecoration(bool isLastUser) {
return BoxDecoration( return BoxDecoration(
border: isMyMessage ? null : Border.all(color: Colors.black.withAlpha(8)), border: isMyMessage ? null : Border.all(color: Colors.black.withAlpha(8)),
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
@@ -682,7 +839,7 @@ class _MessageWidgetState extends State<MessageWidget>
} }
} }
class ReactionBubblePainter extends CustomPainter { class _ReactionBubblePainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.black; final paint = Paint()..color = Colors.black;
+78
View File
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import '../stream_chat_flutter.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker_paint.png)
///
/// It shows a reaction picker
///
/// Usually you don't use this widget as it's one of the default widgets used by [MessageWidget.onMessageActions].
class ReactionPicker extends StatelessWidget {
const ReactionPicker({
Key key,
@required this.reactionToEmoji,
@required this.message,
@required this.channel,
this.size = 40,
}) : super(key: key);
final Map<String, String> reactionToEmoji;
final Message message;
final double size;
final Channel channel;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: reactionToEmoji.keys.map((reactionType) {
final ownReactionIndex = message.ownReactions
.indexWhere((reaction) => reaction.type == reactionType);
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
IconButton(
iconSize: size,
icon: Text(
reactionToEmoji[reactionType],
style: TextStyle(
fontSize: size - 10,
),
),
onPressed: () {
if (ownReactionIndex != -1) {
removeReaction(context, reactionType);
} else {
sendReaction(context, reactionType);
}
},
),
ownReactionIndex != -1
? Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
message.ownReactions[ownReactionIndex].score.toString(),
style: TextStyle(color: Colors.white),
),
)
: SizedBox(),
],
);
}).toList(),
);
}
void sendReaction(BuildContext context, String reactionType) {
channel.sendReaction(message.id, reactionType);
Navigator.of(context).pop();
}
void removeReaction(BuildContext context, String reactionType) {
channel.deleteReaction(message.id, reactionType);
Navigator.of(context).pop();
}
}
+3 -2
View File
@@ -4,7 +4,7 @@ description: Stream Chat official Flutter SDK. Build your own chat experience us
version: 0.1.6+4 version: 0.1.6+4
environment: environment:
sdk: ">=2.1.0 <3.0.0" sdk: ">=2.2.2 <3.0.0"
dependencies: dependencies:
flutter: flutter:
@@ -20,7 +20,8 @@ dependencies:
chewie: ^0.9.8+1 chewie: ^0.9.8+1
file_picker: ^1.4.3+2 file_picker: ^1.4.3+2
image_picker: ^0.6.3+4 image_picker: ^0.6.3+4
stream_chat: ^0.1.12 keyboard_visibility: ^0.5.6
stream_chat: ^0.1.13
dev_dependencies: dev_dependencies:
pedantic: ^1.8.0+1 pedantic: ^1.8.0+1
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB