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:
- file_picker (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_plugin_android_lifecycle (0.0.1):
- Flutter
- FMDB (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):
- Flutter
- path_provider_macos (0.0.1):
- Flutter
- Reachability (3.2)
- screen (0.0.1):
- Flutter
- sqflite (0.0.1):
@@ -22,8 +34,13 @@ PODS:
- Flutter
DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- 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_macos (from `.symlinks/plugins/path_provider_macos/ios`)
- screen (from `.symlinks/plugins/screen/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
@@ -35,12 +52,23 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- FMDB
- Reachability
EXTERNAL SOURCES:
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
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: ".symlinks/plugins/path_provider/ios"
path_provider_macos:
:path: ".symlinks/plugins/path_provider_macos/ios"
screen:
:path: ".symlinks/plugins/screen/ios"
sqflite:
@@ -57,9 +85,15 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/video_player_web/ios"
SPEC CHECKSUMS:
file_picker: 408623be2125b79a4539cf703be3d4b3abe5e245
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
flutter_plugin_android_lifecycle: 47de533a02850f070f5696a623995e93eddcdb9b
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_picker: e3eacd46b94694dde7cf2705955cece853aa1a8f
keyboard_visibility: 96a24de806fe6823c3ad956c01ba2ec6d056616f
path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d
path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
screen: abd91ca7bf3426e1cc3646d27e9b2358d6bf07b0
sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0
url_launcher: a1c0cc845906122c4784c542523d8cacbded5626
+1 -1
View File
@@ -89,7 +89,7 @@ class ChannelPreview extends StatelessWidget {
initialData: channel.state.typingEvents,
builder: (context, snapshot) {
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
? _buildTypings(typings, context, opacity)
: _buildLastMessage(context, opacity);
+407 -176
View File
@@ -4,14 +4,18 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.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_flutter/src/message_list_view.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import '../stream_chat_flutter.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_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_paint.png)
///
@@ -55,8 +59,12 @@ class MessageInput extends StatefulWidget {
Key key,
this.onMessageSent,
this.parentMessage,
this.editMessage,
}) : super(key: key);
/// Message to edit
final Message editMessage;
/// Function called after sending the message
final void Function(Message) onMessageSent;
@@ -68,10 +76,13 @@ class MessageInput extends StatefulWidget {
}
class _MessageInputState extends State<MessageInput> {
final _textController = TextEditingController();
final List<_SendingAttachment> _attachments = [];
final _focusNode = FocusNode();
TextEditingController _textController;
bool _messageIsPresent = false;
bool _typingStarted = false;
final List<_SendingAttachment> _attachments = [];
OverlayEntry _commandsOverlay;
@override
Widget build(BuildContext context) {
@@ -81,84 +92,12 @@ class _MessageInputState extends State<MessageInput> {
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned.fill(
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)),
),
),
),
),
),
_buildBorder(context),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildAttachments(),
Flex(
direction: Axis.horizontal,
children: <Widget>[
_buildAttachmentButton(),
Expanded(
child: TextField(
minLines: null,
maxLines: null,
onSubmitted: (_) {
_sendMessage(context);
},
controller: _textController,
onChanged: (s) {
StreamChannel.of(context).channel.keyStroke();
setState(() {
_messageIsPresent = s.trim().isNotEmpty;
});
},
onTap: () {
setState(() {
_typingStarted = true;
});
},
style: Theme.of(context).textTheme.body1,
autofocus: false,
decoration: InputDecoration(
hintText: 'Write a message',
prefixText: ' ',
border: InputBorder.none,
),
),
),
AnimatedCrossFade(
crossFadeState:
(_messageIsPresent || _attachments.isNotEmpty)
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: _buildSendButton(context),
secondChild: SizedBox(),
duration: Duration(milliseconds: 300),
alignment: Alignment.center,
),
],
),
_buildTextField(context),
],
),
],
@@ -167,6 +106,196 @@ class _MessageInputState extends State<MessageInput> {
);
}
Flex _buildTextField(BuildContext context) {
return Flex(
direction: Axis.horizontal,
children: <Widget>[
_buildAttachmentButton(),
_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(
minLines: null,
maxLines: null,
onSubmitted: (_) {
_sendMessage(context);
},
controller: _textController,
focusNode: _focusNode,
onChanged: (s) {
StreamChannel.of(context).channel.keyStroke();
setState(() {
_messageIsPresent = s.trim().isNotEmpty;
});
_commandsOverlay?.remove();
_commandsOverlay = null;
if (s.startsWith('/')) {
_commandsOverlay = _buildOverlayEntry();
Overlay.of(context).insert(_commandsOverlay);
}
},
onTap: () {
setState(() {
_typingStarted = true;
});
},
style: Theme.of(context).textTheme.body1,
autofocus: false,
decoration: InputDecoration(
hintText: 'Write a message',
prefixText: ' ',
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),
),
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)),
),
),
),
),
);
}
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() {
return Wrap(
direction: Axis.horizontal,
@@ -234,10 +363,15 @@ class _MessageInputState extends State<MessageInput> {
Widget _buildAttachment(_SendingAttachment attachment) {
switch (attachment.type) {
case FileType.IMAGE:
return Image.file(
attachment.file,
fit: BoxFit.cover,
);
return attachment.file != null
? Image.file(
attachment.file,
fit: BoxFit.cover,
)
: Image.network(
attachment.url,
fit: BoxFit.cover,
);
break;
case FileType.VIDEO:
return Container(
@@ -262,71 +396,7 @@ class _MessageInputState extends State<MessageInput> {
color: Colors.transparent,
child: IconButton(
onPressed: () {
showModalBottomSheet(
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32),
),
),
context: context,
builder: (_) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
title: Text(
'Add a file',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
ListTile(
leading: Icon(Icons.image),
title: Text('Upload a photo'),
onTap: () {
_pickFile(FileType.IMAGE, false);
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Icons.video_library),
title: Text('Upload a video'),
onTap: () {
_pickFile(FileType.VIDEO, false);
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Icons.camera_alt),
title: Text('Photo from camera'),
onTap: () {
ImagePicker.pickImage(source: ImageSource.camera);
_pickFile(FileType.IMAGE, true);
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Icons.videocam),
title: Text('Video from camera'),
onTap: () {
_pickFile(FileType.VIDEO, true);
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text('Upload a file'),
onTap: () {
_pickFile(FileType.ANY, false);
Navigator.pop(context);
},
),
],
);
});
_showAttachmentModal();
},
icon: Icon(
Icons.add_circle_outline,
@@ -335,6 +405,74 @@ class _MessageInputState extends State<MessageInput> {
);
}
void _showAttachmentModal() {
showModalBottomSheet(
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32),
),
),
context: context,
builder: (_) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
title: Text(
'Add a file',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
ListTile(
leading: Icon(Icons.image),
title: Text('Upload a photo'),
onTap: () {
_pickFile(FileType.IMAGE, false);
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Icons.video_library),
title: Text('Upload a video'),
onTap: () {
_pickFile(FileType.VIDEO, false);
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Icons.camera_alt),
title: Text('Photo from camera'),
onTap: () {
ImagePicker.pickImage(source: ImageSource.camera);
_pickFile(FileType.IMAGE, true);
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Icons.videocam),
title: Text('Video from camera'),
onTap: () {
_pickFile(FileType.VIDEO, true);
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text('Upload a file'),
onTap: () {
_pickFile(FileType.ANY, false);
Navigator.pop(context);
},
),
],
);
});
}
void _pickFile(FileType type, bool camera) async {
File file;
@@ -412,52 +550,145 @@ class _MessageInputState extends State<MessageInput> {
_messageIsPresent = false;
_typingStarted = false;
});
_commandsOverlay?.remove();
_commandsOverlay = null;
FocusScope.of(context).unfocus();
StreamChannel.of(context)
.channel
.sendMessage(
Message(
parentId: widget.parentMessage?.id,
text: text,
attachments: attachments.map((attachment) {
String type;
switch (attachment.type) {
case FileType.IMAGE:
type = 'image';
break;
case FileType.VIDEO:
type = 'video';
break;
default:
type = 'file';
}
return Attachment(
imageUrl:
attachment.type == FileType.IMAGE ? attachment.url : null,
assetUrl: attachment.url,
type: type,
);
}).toList(),
),
)
.then((_) {
if (widget.onMessageSent != null) {
widget.onMessageSent(Message(text: text));
if (widget.editMessage != null) {
final message = widget.editMessage.copyWith(
parentId: widget.parentMessage?.id,
text: text,
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;
switch (attachment.type) {
case FileType.IMAGE:
type = 'image';
break;
case FileType.VIDEO:
type = 'video';
break;
default:
type = 'file';
}
return Attachment(
imageUrl: attachment.type == FileType.IMAGE ? attachment.url : null,
assetUrl: attachment.url,
type: type,
);
});
}
int _keyboardListener;
@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();
}
}
class _SendingAttachment {
final File file;
final FileType type;
String url;
bool uploaded = false;
bool uploaded;
_SendingAttachment({
this.url,
this.file,
this.type,
this.uploaded = false,
});
}
+372 -215
View File
@@ -8,7 +8,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_markdown/flutter_markdown.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/reaction_picker.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/user_avatar.dart';
@@ -33,9 +35,13 @@ class MessageWidget extends StatefulWidget {
@required this.message,
@required this.nextMessage,
this.onThreadTap,
this.onMessageActions,
this.isParent = false,
}) : super(key: key);
/// Function called on long press
final Function(BuildContext, Message) onMessageActions;
/// This message
final Message message;
@@ -63,6 +69,7 @@ class _MessageWidgetState extends State<MessageWidget>
MessageTheme messageTheme;
StreamChatState streamChat;
StreamChannelState streamChannel;
bool isMyMessage;
@override
Widget build(BuildContext context) {
@@ -75,9 +82,10 @@ class _MessageWidgetState extends State<MessageWidget>
final messageUserId = widget.message.user.id;
final previousUserId = widget.previousMessage?.user?.id;
final nextUserId = widget.nextMessage?.user?.id;
final isMyMessage = messageUserId == currentUserId;
final isLastUser = previousUserId == messageUserId;
final isNextUser = nextUserId == messageUserId;
isMyMessage = messageUserId == currentUserId;
final alignment =
isMyMessage ? Alignment.centerRight : Alignment.centerLeft;
@@ -95,11 +103,11 @@ class _MessageWidgetState extends State<MessageWidget>
crossAxisAlignment:
isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: <Widget>[
_buildBubble(context, isMyMessage, isLastUser),
_buildBubble(context, isLastUser),
streamChannel.channel.config.replies
? _buildThreadIndicator(context, isMyMessage)
? _buildThreadIndicator(context)
: SizedBox(),
isNextUser ? SizedBox() : _buildTimestamp(isMyMessage, alignment),
isNextUser ? SizedBox() : _buildTimestamp(alignment),
],
),
isNextUser
@@ -161,7 +169,7 @@ class _MessageWidgetState extends State<MessageWidget>
);
}
Widget _buildThreadIndicator(BuildContext context, bool isMyMessage) {
Widget _buildThreadIndicator(BuildContext context) {
var row = [
Text(
'Replies: ${widget.message.replyCount}',
@@ -203,7 +211,6 @@ class _MessageWidgetState extends State<MessageWidget>
Widget _buildBubble(
BuildContext context,
bool isMyMessage,
bool isLastUser,
) {
var nOfAttachmentWidgets = 0;
@@ -216,84 +223,134 @@ class _MessageWidgetState extends State<MessageWidget>
Widget attachmentWidget;
if (attachment.type == 'video') {
attachmentWidget = _buildVideo(attachment, isMyMessage, isLastUser);
attachmentWidget = _buildVideo(attachment, isLastUser);
} else if (attachment.type == 'image' || attachment.type == 'giphy') {
attachmentWidget = _buildImage(isMyMessage, isLastUser, attachment);
attachmentWidget = _buildImage(isLastUser, attachment);
}
if (attachmentWidget != null) {
final boxDecoration = _buildBoxDecoration(isMyMessage, isLastUser)
final boxDecoration = _buildBoxDecoration(isLastUser)
.copyWith(color: Color(0xffebebeb));
return Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: ClipRRect(
borderRadius: boxDecoration.borderRadius,
child: Container(
decoration: boxDecoration,
constraints: BoxConstraints.loose(Size.fromWidth(300)),
child: Stack(
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ClipRRect(
borderRadius: boxDecoration.borderRadius,
child: Container(
decoration: boxDecoration,
constraints: BoxConstraints.loose(Size.fromWidth(300)),
child: Stack(
children: <Widget>[
attachmentWidget,
attachment.title != null
? Container(
constraints:
BoxConstraints.loose(Size(300, 500)),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
Text(
attachment.title,
overflow: TextOverflow.ellipsis,
style:
messageTheme.messageText.copyWith(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
attachmentWidget,
attachment.title != null
? Container(
constraints:
BoxConstraints.loose(Size(300, 500)),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
Text(
attachment.title,
overflow: TextOverflow.ellipsis,
style: messageTheme.messageText
.copyWith(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
Text(
Uri.parse(attachment.thumbUrl)
.authority
.split('.')
.reversed
.take(2)
.toList()
.reversed
.join('.'),
overflow: TextOverflow.ellipsis,
style: messageTheme.createdAt,
),
],
),
Text(
Uri.parse(attachment.thumbUrl)
.authority
.split('.')
.reversed
.take(2)
.toList()
.reversed
.join('.'),
overflow: TextOverflow.ellipsis,
style: messageTheme.createdAt,
),
],
),
color: Color(0xffebebeb),
)
: SizedBox(),
],
),
attachment.type == 'image' &&
attachment.titleLink != null
? Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () =>
_launchURL(attachment.titleLink),
),
),
color: Color(0xffebebeb),
)
: SizedBox(),
],
),
attachment.type == 'image' && attachment.titleLink != null
? Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _launchURL(attachment.titleLink),
),
margin: EdgeInsets.only(
top: nOfAttachmentWidgets > 1 ? 5 : 0,
),
),
),
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),
),
)
: SizedBox(),
],
),
margin: EdgeInsets.only(
top: nOfAttachmentWidgets > 1 ? 5 : 0,
),
),
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,85 +363,123 @@ class _MessageWidgetState extends State<MessageWidget>
if (widget.message.text.trim().isNotEmpty) {
column.children.addAll(
<Widget>[
IntrinsicWidth(
child: Column(
crossAxisAlignment: isMyMessage
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: <Widget>[
streamChannel.channel.config.reactions
? Align(
child: _buildReactions(isMyMessage),
alignment: isMyMessage
? Alignment.centerLeft
: Alignment.centerRight,
)
: SizedBox(),
Stack(
overflow: Overflow.visible,
children: <Widget>[
widget.message.reactionCounts?.isNotEmpty == true
? Positioned(
left: isMyMessage ? 8 : null,
right: !isMyMessage ? 8 : null,
top: -6,
child: CustomPaint(
painter: ReactionBubblePainter(),
),
)
: SizedBox(),
Padding(
padding: EdgeInsets.only(
right: isMyMessage ? 0.0 : 8.0,
left: isMyMessage ? 8.0 : 0.0,
),
child: Container(
decoration: _buildBoxDecoration(isMyMessage,
isLastUser || nOfAttachmentWidgets > 0),
padding: EdgeInsets.all(10),
constraints: BoxConstraints.loose(Size.fromWidth(300)),
child: MarkdownBody(
data: '${widget.message.text}',
onTapLink: (link) {
_launchURL(link);
},
styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context).copyWith(
textTheme: Theme.of(context).textTheme.apply(
bodyColor: messageTheme.messageText.color,
decoration:
messageTheme.messageText.decoration,
decorationColor: messageTheme
.messageText.decorationColor,
decorationStyle: messageTheme
.messageText.decorationStyle,
fontFamily:
messageTheme.messageText.fontFamily,
),
),
).copyWith(
p: messageTheme.messageText,
Column(
crossAxisAlignment:
isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: <Widget>[
(streamChannel.channel.config.reactions &&
nOfAttachmentWidgets == 0)
? Align(
child: _buildReactions(),
alignment: isMyMessage
? Alignment.centerLeft
: Alignment.centerRight,
)
: SizedBox(),
Stack(
overflow: Overflow.visible,
children: <Widget>[
nOfAttachmentWidgets == 0
? _buildReactionPaint()
: SizedBox(),
Padding(
padding: EdgeInsets.only(
right: isMyMessage ? 0.0 : 8.0,
left: isMyMessage ? 8.0 : 0.0,
),
child: Container(
decoration: _buildBoxDecoration(
isLastUser || nOfAttachmentWidgets > 0),
padding: EdgeInsets.all(10),
constraints: BoxConstraints.loose(Size.fromWidth(300)),
child: MarkdownBody(
data: '${widget.message.text}',
onTapLink: (link) {
_launchURL(link);
},
styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context).copyWith(
textTheme: Theme.of(context).textTheme.apply(
bodyColor: messageTheme.messageText.color,
decoration:
messageTheme.messageText.decoration,
decorationColor:
messageTheme.messageText.decorationColor,
decorationStyle:
messageTheme.messageText.decorationStyle,
fontFamily:
messageTheme.messageText.fontFamily,
),
),
).copyWith(
p: messageTheme.messageText,
),
),
),
],
),
],
),
),
],
),
],
),
],
);
}
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(
child: column,
child: IntrinsicWidth(child: column),
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 &&
!streamChannel.channel.config.replies) {
return;
@@ -399,11 +494,7 @@ class _MessageWidgetState extends State<MessageWidget>
),
),
context: context,
builder: (context) {
final fontSize = 30.0;
final textStyle = TextStyle(
fontSize: fontSize,
);
builder: (_) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -412,49 +503,10 @@ class _MessageWidgetState extends State<MessageWidget>
Container(
color: Colors.black87,
child: streamChannel.channel.config.reactions
? Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
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(),
? ReactionPicker(
channel: StreamChannel.of(context).channel,
reactionToEmoji: reactionToEmoji,
message: widget.message,
)
: SizedBox(),
),
@@ -478,6 +530,22 @@ class _MessageWidgetState extends State<MessageWidget>
},
)
: 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
? FlatButton(
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(
onTap: () {
_showMessageBottomSheet(context, isMyMessage);
if (widget.onMessageActions != null) {
widget.onMessageActions(context, widget.message);
} else {
_showMessageBottomSheet(context);
}
},
child: Padding(
padding: EdgeInsets.symmetric(
@@ -527,26 +687,25 @@ class _MessageWidgetState extends State<MessageWidget>
}
Row _buildReactionRow() {
final List<Widget> children =
widget.message.reactionCounts.keys.map((reactionType) {
return Text(
reactionToEmoji[reactionType] ?? '?',
) as Widget;
}).toList();
children.add(Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
widget.message.reactionCounts.values
.fold(0, (t, v) => v + t)
.toString(),
style: TextStyle(color: Colors.white),
),
));
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: children,
children: [
...widget.message.reactionCounts.keys.map((reactionType) {
return Text(
reactionToEmoji[reactionType] ?? '?',
);
}),
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
widget.message.reactionCounts.values
.fold(0, (t, v) => v + t)
.toString(),
style: TextStyle(color: Colors.white),
),
),
],
);
}
@@ -559,18 +718,7 @@ class _MessageWidgetState extends State<MessageWidget>
'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(
bool isMyMessage,
bool isLastUser,
Attachment attachment,
) {
@@ -578,12 +726,21 @@ class _MessageWidgetState extends State<MessageWidget>
imageUrl:
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl,
fit: BoxFit.cover,
placeholderFadeInDuration: Duration(milliseconds: 300),
placeholder: (_, __) {
return SizedBox(
height: 200,
width: 300,
child: Center(
child: CircularProgressIndicator(),
),
);
},
);
}
Widget _buildVideo(
Attachment attachment,
bool isMyMessage,
bool isLastUser,
) {
VideoPlayerController videoController;
@@ -653,7 +810,7 @@ class _MessageWidgetState extends State<MessageWidget>
super.dispose();
}
Widget _buildTimestamp(bool isMyMessage, Alignment alignment) {
Widget _buildTimestamp(Alignment alignment) {
return Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Text(
@@ -663,7 +820,7 @@ class _MessageWidgetState extends State<MessageWidget>
);
}
BoxDecoration _buildBoxDecoration(bool isMyMessage, bool isLastUser) {
BoxDecoration _buildBoxDecoration(bool isLastUser) {
return BoxDecoration(
border: isMyMessage ? null : Border.all(color: Colors.black.withAlpha(8)),
borderRadius: BorderRadius.only(
@@ -682,7 +839,7 @@ class _MessageWidgetState extends State<MessageWidget>
}
}
class ReactionBubblePainter extends CustomPainter {
class _ReactionBubblePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
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
environment:
sdk: ">=2.1.0 <3.0.0"
sdk: ">=2.2.2 <3.0.0"
dependencies:
flutter:
@@ -20,7 +20,8 @@ dependencies:
chewie: ^0.9.8+1
file_picker: ^1.4.3+2
image_picker: ^0.6.3+4
stream_chat: ^0.1.12
keyboard_visibility: ^0.5.6
stream_chat: ^0.1.13
dev_dependencies:
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