diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index a90d828e..6fb8664b 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -6,10 +6,14 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
+
+
+ android:icon="@mipmap/ic_launcher"
+ android:requestLegacyExternalStorage="true"
+ >
value..extraData['image'] = getRandomPicUrl(value));
return Scaffold(
- body: Column(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Padding(
- padding: const EdgeInsets.only(
- top: 34,
- bottom: 20,
- ),
- child: Center(
- child: SvgPicture.asset(
- 'assets/logo.svg',
- height: 40,
+ body: SafeArea(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ top: 34,
+ bottom: 20,
+ ),
+ child: Center(
+ child: SvgPicture.asset(
+ 'assets/logo.svg',
+ height: 40,
+ ),
),
),
- ),
- Padding(
- padding: const EdgeInsets.only(bottom: 13.0),
- child: Text(
- 'Welcome to Stream Chat',
+ Padding(
+ padding: const EdgeInsets.only(bottom: 13.0),
+ child: Text(
+ 'Welcome to Stream Chat',
+ style: TextStyle(
+ fontSize: 22,
+ color: Colors.black,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ Text(
+ 'Select a user to try the Flutter SDK:',
style: TextStyle(
- fontSize: 22,
+ fontSize: 14.5,
color: Colors.black,
- fontWeight: FontWeight.bold,
),
),
- ),
- Text(
- 'Select a user to try the Flutter SDK:',
- style: TextStyle(
- fontSize: 14.5,
- color: Colors.black,
- ),
- ),
- Expanded(
- child: ListView.separated(
- separatorBuilder: (context, i) {
- return Container(
- width: double.infinity,
- color: Colors.black12,
- height: 1,
- );
- },
- itemCount: users.length + 1,
- itemBuilder: (context, i) {
- return [
- ...users.entries.map((entry) {
- final token = entry.key;
- final user = entry.value;
- return ListTile(
- onTap: () async {
- showDialog(
- barrierDismissible: false,
- context: context,
- builder: (context) => Center(
- child: Container(
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(16),
- color: Colors.white,
- ),
- height: 100,
- width: 100,
- child: Center(
- child: CircularProgressIndicator(),
+ Expanded(
+ child: ListView.separated(
+ separatorBuilder: (context, i) {
+ return Container(
+ width: double.infinity,
+ color: Colors.black12,
+ height: 1,
+ );
+ },
+ itemCount: users.length + 1,
+ itemBuilder: (context, i) {
+ return [
+ ...users.entries.map((entry) {
+ final token = entry.key;
+ final user = entry.value;
+ return ListTile(
+ onTap: () async {
+ showDialog(
+ barrierDismissible: false,
+ context: context,
+ builder: (context) => Center(
+ child: Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(16),
+ color: Colors.white,
+ ),
+ height: 100,
+ width: 100,
+ child: Center(
+ child: CircularProgressIndicator(),
+ ),
),
),
+ );
+
+ final secureStorage = FlutterSecureStorage();
+ final client = StreamChat.of(context).client;
+
+ await client.setUser(
+ user,
+ token,
+ );
+
+ secureStorage.write(
+ key: kStreamApiKey,
+ value: kDefaultStreamApiKey,
+ );
+ secureStorage.write(
+ key: kStreamUserId,
+ value: user.id,
+ );
+ secureStorage.write(
+ key: kStreamToken,
+ value: token,
+ );
+
+ if (!kIsWeb) {
+ initNotifications(client);
+ }
+
+ Navigator.pop(context);
+ await Navigator.pushReplacement(
+ context,
+ MaterialPageRoute(
+ builder: (context) {
+ return StreamChat(
+ client: client,
+ child: ChannelListPage(),
+ );
+ },
+ ),
+ );
+ },
+ leading: UserAvatar(
+ user: user,
+ constraints: BoxConstraints.tight(
+ Size.fromRadius(20),
),
- );
-
- final secureStorage = FlutterSecureStorage();
- final client = StreamChat.of(context).client;
-
- await client.setUser(
- user,
- token,
- );
-
- secureStorage.write(
- key: kStreamApiKey,
- value: kDefaultStreamApiKey,
- );
- secureStorage.write(
- key: kStreamUserId,
- value: user.id,
- );
- secureStorage.write(
- key: kStreamToken,
- value: token,
- );
-
- if (!kIsWeb) {
- initNotifications(client);
- }
-
- Navigator.pop(context);
- await Navigator.pushReplacement(
+ ),
+ title: Text(
+ user.name,
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ subtitle: Text('Stream test account'),
+ trailing: SvgPicture.asset(
+ 'assets/icon_arrow_right.svg',
+ height: 24,
+ width: 24,
+ ),
+ );
+ }),
+ ListTile(
+ onTap: () {
+ Navigator.push(
context,
MaterialPageRoute(
- builder: (context) {
- return StreamChat(
- client: client,
- child: ChannelListPage(),
- );
- },
+ builder: (context) => AdvancedOptionsPage(),
),
);
},
- leading: UserAvatar(
- user: user,
- constraints: BoxConstraints.tight(
- Size.fromRadius(20),
+ leading: CircleAvatar(
+ child: Icon(
+ StreamIcons.settings,
+ color: Colors.black,
),
+ backgroundColor:
+ StreamChatTheme.of(context).secondaryColor,
),
title: Text(
- user.name,
+ 'Advanced Options',
style: TextStyle(fontWeight: FontWeight.bold),
),
- subtitle: Text('Stream test account'),
+ subtitle: Text('Custom settings'),
trailing: SvgPicture.asset(
'assets/icon_arrow_right.svg',
height: 24,
width: 24,
+ clipBehavior: Clip.none,
),
- );
- }),
- ListTile(
- onTap: () {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => AdvancedOptionsPage(),
- ),
- );
- },
- leading: CircleAvatar(
- child: Icon(
- StreamIcons.settings,
- color: Colors.black,
- ),
- backgroundColor:
- StreamChatTheme.of(context).secondaryColor,
),
- title: Text(
- 'Advanced Options',
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- subtitle: Text('Custom settings'),
- trailing: SvgPicture.asset(
- 'assets/icon_arrow_right.svg',
- height: 24,
- width: 24,
- clipBehavior: Clip.none,
- ),
- ),
- ][i];
- },
+ ][i];
+ },
+ ),
),
- ),
- StreamVersion(),
- ],
+ StreamVersion(),
+ ],
+ ),
),
);
}
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 1da02072..2e3f43e2 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -64,83 +64,85 @@ class ChannelListPage extends StatelessWidget {
final user = StreamChat.of(context).user;
return Scaffold(
drawer: Drawer(
- child: Padding(
- padding: EdgeInsets.only(
- top: MediaQuery.of(context).viewPadding.top + 8,
- ),
- child: Column(
- children: [
- Padding(
- padding: const EdgeInsets.only(
- bottom: 20.0,
- left: 8,
- ),
- child: Row(
- children: [
- UserAvatar(
- user: user,
- showOnlineStatus: false,
- constraints: BoxConstraints.tight(Size.fromRadius(20)),
- ),
- Padding(
- padding: const EdgeInsets.only(left: 16.0),
- child: Text(
- user.name,
- style: TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.bold,
+ child: SafeArea(
+ child: Padding(
+ padding: EdgeInsets.only(
+ top: MediaQuery.of(context).viewPadding.top + 8,
+ ),
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ bottom: 20.0,
+ left: 8,
+ ),
+ child: Row(
+ children: [
+ UserAvatar(
+ user: user,
+ showOnlineStatus: false,
+ constraints: BoxConstraints.tight(Size.fromRadius(20)),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(left: 16.0),
+ child: Text(
+ user.name,
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
),
),
+ ],
+ ),
+ ),
+ ListTile(
+ leading: Icon(StreamIcons.edit),
+ title: Text(
+ 'New direct message',
+ style: TextStyle(
+ fontSize: 14.5,
),
- ],
- ),
- ),
- ListTile(
- leading: Icon(StreamIcons.edit),
- title: Text(
- 'New direct message',
- style: TextStyle(
- fontSize: 14.5,
),
),
- ),
- ListTile(
- leading: Icon(StreamIcons.group),
- title: Text(
- 'New group',
- style: TextStyle(
- fontSize: 14.5,
+ ListTile(
+ leading: Icon(StreamIcons.group),
+ title: Text(
+ 'New group',
+ style: TextStyle(
+ fontSize: 14.5,
+ ),
),
),
- ),
- Expanded(
- child: Container(
- alignment: Alignment.bottomCenter,
- child: ListTile(
- onTap: () async {
- await StreamChat.of(context).client.disconnect();
+ Expanded(
+ child: Container(
+ alignment: Alignment.bottomCenter,
+ child: ListTile(
+ onTap: () async {
+ await StreamChat.of(context).client.disconnect();
- final secureStorage = FlutterSecureStorage();
- await secureStorage.deleteAll();
- Navigator.pop(context);
- await Navigator.pushReplacement(
- context,
- MaterialPageRoute(
- builder: (context) => ChooseUserPage(),
+ final secureStorage = FlutterSecureStorage();
+ await secureStorage.deleteAll();
+ Navigator.pop(context);
+ await Navigator.pushReplacement(
+ context,
+ MaterialPageRoute(
+ builder: (context) => ChooseUserPage(),
+ ),
+ );
+ },
+ leading: Icon(StreamIcons.user),
+ title: Text(
+ 'Sign out',
+ style: TextStyle(
+ fontSize: 14.5,
),
- );
- },
- leading: Icon(StreamIcons.user),
- title: Text(
- 'Sign out',
- style: TextStyle(
- fontSize: 14.5,
),
),
),
),
- ),
- ],
+ ],
+ ),
),
),
),
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 85fd549e..8fc2dadb 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -1,6 +1,6 @@
name: example
description: A new Flutter project.
-version: 1.0.57+59
+version: 1.0.59+61
environment:
sdk: ">=2.2.2 <3.0.0"
diff --git a/lib/src/media_list_view.dart b/lib/src/media_list_view.dart
new file mode 100644
index 00000000..627a480c
--- /dev/null
+++ b/lib/src/media_list_view.dart
@@ -0,0 +1,144 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:media_gallery/media_gallery.dart';
+import 'package:stream_chat_flutter/src/stream_icons.dart';
+import 'package:transparent_image/transparent_image.dart';
+
+class MediaListView extends StatefulWidget {
+ final List selectedIds;
+ final void Function(Media media) onSelect;
+
+ const MediaListView({
+ Key key,
+ this.selectedIds = const [],
+ this.onSelect,
+ }) : super(key: key);
+ @override
+ _MediaListViewState createState() => _MediaListViewState();
+}
+
+class _MediaListViewState extends State {
+ final _media = [];
+ final ScrollController _scrollController = ScrollController();
+
+ @override
+ Widget build(BuildContext context) {
+ return GridView.builder(
+ itemCount: _media.length,
+ controller: _scrollController,
+ gridDelegate:
+ SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
+ itemBuilder: (
+ context,
+ position,
+ ) {
+ final media = _media.elementAt(position);
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 1.0),
+ child: InkWell(
+ child: Stack(
+ children: [
+ AspectRatio(
+ aspectRatio: 1.0,
+ child: FadeInImage(
+ placeholder: MemoryImage(kTransparentImage),
+ image: MediaThumbnailProvider(
+ media: media,
+ ),
+ fit: BoxFit.cover,
+ ),
+ ),
+ Positioned.fill(
+ child: IgnorePointer(
+ child: AnimatedOpacity(
+ duration: Duration(milliseconds: 300),
+ opacity: widget.selectedIds.any((id) => id == media.id)
+ ? 1.0
+ : 0.0,
+ child: Container(
+ color: Colors.black.withOpacity(0.5),
+ alignment: Alignment.topRight,
+ padding: const EdgeInsets.only(
+ top: 8,
+ right: 8,
+ ),
+ child: CircleAvatar(
+ maxRadius: 12.0,
+ backgroundColor: Colors.white,
+ child: Icon(
+ StreamIcons.check,
+ color: Colors.black,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ if (media.mediaType == MediaType.video)
+ Positioned(
+ left: 8,
+ bottom: 10,
+ child: SvgPicture.asset(
+ 'svgs/video_call_icon.svg',
+ package: 'stream_chat_flutter',
+ ),
+ ),
+ ],
+ ),
+ onTap: () {
+ if (widget.onSelect != null) {
+ widget.onSelect(media);
+ }
+ },
+ ),
+ );
+ },
+ );
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _getMedia();
+ }
+
+ void _getMedia() async {
+ final List collections =
+ await MediaGallery.listMediaCollections(
+ mediaTypes: [
+ MediaType.video,
+ MediaType.image,
+ ],
+ );
+
+ if (collections.isEmpty) {
+ return;
+ }
+ final collection = collections.firstWhere(
+ (element) => element.isAllCollection,
+ orElse: () => collections.first,
+ );
+
+ final videoPage = await collection.getMedias(
+ mediaType: MediaType.video,
+ take: 500,
+ );
+ final imagePage = await collection.getMedias(
+ mediaType: MediaType.image,
+ take: 500,
+ );
+
+ final allItems = [
+ ...videoPage.items,
+ ...imagePage.items,
+ ]..sort((
+ a,
+ b,
+ ) =>
+ b.creationDate.compareTo(a.creationDate));
+
+ setState(() {
+ _media.addAll(allItems);
+ });
+ }
+}
diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart
index 00354a49..16c48e25 100644
--- a/lib/src/message_actions_modal.dart
+++ b/lib/src/message_actions_modal.dart
@@ -45,14 +45,15 @@ class MessageActionsModal extends StatelessWidget {
@override
Widget build(BuildContext context) {
- var size = MediaQuery.of(context).size;
- var user = StreamChat.of(context).user;
+ final size = MediaQuery.of(context).size;
+ final user = StreamChat.of(context).user;
- var roughMaxSize = 2 * size.width / 3;
- var roughSentenceSize =
+ final roughMaxSize = 2 * size.width / 3;
+ final roughSentenceSize =
message.text.length * messageTheme.messageText.fontSize * 1.2;
- var divFactor =
- roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize);
+ final divFactor = message.attachments?.isNotEmpty == true
+ ? 1
+ : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
return GestureDetector(
behavior: HitTestBehavior.translucent,
diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart
index 89891ace..bac36ef7 100644
--- a/lib/src/message_input.dart
+++ b/lib/src/message_input.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:io';
import 'dart:math';
import 'package:emojis/emoji.dart';
@@ -8,10 +9,13 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_svg/flutter_svg.dart';
-import 'package:http_parser/http_parser.dart';
+import 'package:http_parser/http_parser.dart' as httpParser;
import 'package:image_picker/image_picker.dart';
+import 'package:media_gallery/media_gallery.dart';
import 'package:mime/mime.dart';
+import 'package:permission_handler/permission_handler.dart';
import 'package:stream_chat/stream_chat.dart';
+import 'package:stream_chat_flutter/src/media_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/user_avatar.dart';
@@ -168,7 +172,7 @@ class MessageInputState extends State {
final _imagePicker = ImagePicker();
bool _inputEnabled = true;
bool _messageIsPresent = false;
- bool _typingStarted = false;
+ bool _animateContainer = true;
bool _commandEnabled = false;
OverlayEntry _commandsOverlay, _mentionsOverlay, _emojiOverlay;
Iterable _emojiNames;
@@ -176,6 +180,9 @@ class MessageInputState extends State {
Command _chosenCommand;
bool _actionsShrunk = false;
bool _sendAsDm = false;
+ bool _openFilePickerSection = false;
+ int _filePickerIndex = 0;
+ double _filePickerSize = 250.0;
/// The editing controller passed to the input TextField
TextEditingController textEditingController;
@@ -201,6 +208,7 @@ class MessageInputState extends State {
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _buildDmCheckbox(),
),
+ _buildFilePickerSection(),
],
),
),
@@ -210,7 +218,7 @@ class MessageInputState extends State {
Flex _buildTextField(BuildContext context) {
return Flex(
direction: Axis.horizontal,
- crossAxisAlignment: CrossAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (!_commandEnabled) _buildExpandActionsButton(),
if (widget.actionsLocation == ActionsLocation.left)
@@ -370,25 +378,17 @@ class MessageInputState extends State {
_emojiOverlay?.remove();
_emojiOverlay = null;
- _checkCommands(s.trimLeft(), context);
+ _checkCommands(s, context);
_checkMentions(s, context);
_checkEmoji(s, context);
},
- onTap: () {
- setState(() {
- _typingStarted = true;
- });
- },
style: Theme.of(context).textTheme.bodyText2,
autofocus: false,
textAlignVertical: TextAlignVertical.center,
decoration: InputDecoration(
- hintText:
- (_commandEnabled && _chosenCommand.name == 'giphy')
- ? 'Search GIFs'
- : 'Write a message',
+ hintText: _getHint(),
prefixText: _commandEnabled ? null : ' ',
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent)),
@@ -440,6 +440,16 @@ class MessageInputState extends State {
);
}
+ String _getHint() {
+ if (_commandEnabled && _chosenCommand.name == 'giphy') {
+ return 'Search GIFs';
+ }
+ if (_attachments.isNotEmpty) {
+ return 'Add a command or send';
+ }
+ return 'Write a message';
+ }
+
void _checkEmoji(String s, BuildContext context) {
if (textEditingController.selection.isCollapsed &&
(s.isNotEmpty && s[textEditingController.selection.start - 1] == ':' ||
@@ -548,8 +558,10 @@ class MessageInputState extends State {
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
- child: Icon(StreamIcons.lightning,
- color: StreamChatTheme.of(context).accentColor),
+ child: Icon(
+ StreamIcons.lightning,
+ color: StreamChatTheme.of(context).accentColor,
+ ),
),
Text(
'Instant Commands',
@@ -604,6 +616,261 @@ class MessageInputState extends State {
});
}
+ Widget _buildFilePickerSection() {
+ return AnimatedContainer(
+ duration: _animateContainer ? Duration(milliseconds: 300) : Duration.zero,
+ height: _openFilePickerSection ? _filePickerSize : 0,
+ child: Material(
+ color: Color(0xFFF2F2F2),
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [
+ IconButton(
+ icon: Icon(
+ StreamIcons.picture,
+ color: _filePickerIndex == 0
+ ? StreamChatTheme.of(context).accentColor
+ : Colors.black.withOpacity(0.5),
+ ),
+ onPressed: () {
+ setState(() {
+ _filePickerIndex = 0;
+ });
+ },
+ ),
+ IconButton(
+ icon: Icon(
+ StreamIcons.folder,
+ color: _filePickerIndex == 1
+ ? StreamChatTheme.of(context).accentColor
+ : Colors.black.withOpacity(0.5),
+ ),
+ onPressed: () {
+ pickFile(DefaultAttachmentTypes.file, false);
+ },
+ ),
+ IconButton(
+ icon: Icon(
+ StreamIcons.camera,
+ color: _filePickerIndex == 2
+ ? StreamChatTheme.of(context).accentColor
+ : Colors.black.withOpacity(0.5),
+ ),
+ onPressed: () {
+ pickFile(DefaultAttachmentTypes.image, true);
+ },
+ ),
+ ],
+ ),
+ GestureDetector(
+ onVerticalDragUpdate: (update) {
+ setState(() {
+ _animateContainer = false;
+ _filePickerSize = (_filePickerSize - update.delta.dy).clamp(
+ 240.0,
+ MediaQuery.of(context).size.height / 1.7,
+ );
+ });
+ },
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(16.0),
+ topRight: Radius.circular(16.0),
+ ),
+ ),
+ child: Container(
+ width: double.infinity,
+ child: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Container(
+ width: 40.0,
+ height: 4.0,
+ decoration: BoxDecoration(
+ color: Color(0xFFF2F2F2),
+ borderRadius: BorderRadius.circular(4.0),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ if (_openFilePickerSection)
+ Expanded(
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(8.0),
+ ),
+ child: _buildPickerSection(),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildPickerSection() {
+ switch (_filePickerIndex) {
+ case 0:
+ return FutureBuilder(
+ future: Platform.isAndroid
+ ? Permission.storage.status
+ : Permission.photos.status,
+ builder: (context, snapshot) {
+ if (!snapshot.hasData) {
+ return Center(
+ child: CircularProgressIndicator(),
+ );
+ }
+
+ if (snapshot.data.isGranted) {
+ return MediaListView(
+ selectedIds: _attachments.map((e) => e.id).toList(),
+ onSelect: (media) {
+ if (!_attachments
+ .any((element) => element.id == media.id)) {
+ _addAttachment(media);
+ } else {
+ _attachments
+ .removeWhere((element) => element.id == media.id);
+ setState(() {});
+ }
+ },
+ );
+ }
+
+ return InkWell(
+ onTap: () async {
+ var status = await (Platform.isAndroid
+ ? Permission.storage.status
+ : Permission.photos.status);
+ print('status: ${status}');
+ if (status.isPermanentlyDenied || status.isDenied) {
+ if (await openAppSettings()) {
+ setState(() {});
+ }
+ } else {
+ status = await (Platform.isAndroid
+ ? Permission.storage
+ : Permission.photos)
+ .request();
+ if (status.isGranted) {
+ setState(() {});
+ }
+ }
+ },
+ child: Container(
+ color: Color(0xFFF2F2F2),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ SvgPicture.asset(
+ 'svgs/icon_picture_empty_state.svg',
+ package: 'stream_chat_flutter',
+ height: 140,
+ color: StreamChatTheme.of(context).accentColor,
+ ),
+ Center(
+ child: Text(
+ 'Allow access to your gallery',
+ style: TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.bold,
+ color: StreamChatTheme.of(context).accentColor,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ });
+ break;
+ case 1:
+ break;
+ case 2:
+ break;
+ }
+ }
+
+ void _addAttachment(Media medium) async {
+ final mediaFile = await medium.getFile();
+ final thumbBytes = await medium.getThumbnail();
+
+ final file = PlatformFile(
+ path: mediaFile.path,
+ bytes: mediaFile.readAsBytesSync(),
+ );
+
+ final thumbFile = PlatformFile(
+ bytes: thumbBytes,
+ name: '${file.name ?? file.path?.split('/')?.last}_thumbnail.jpeg',
+ );
+
+ setState(() {
+ _inputEnabled = true;
+ });
+
+ if (file == null) {
+ return;
+ }
+
+ final channel = StreamChannel.of(context).channel;
+ final attachment = _SendingAttachment(
+ file: file,
+ thumbFile: thumbFile,
+ attachment: Attachment(
+ localUri: file.path != null ? Uri.parse(file.path) : null,
+ type: medium.mediaType == MediaType.image ? 'image' : 'video',
+ ),
+ id: medium.id,
+ );
+
+ setState(() {
+ _attachments.add(attachment);
+ });
+
+ final thumbUrl = await _uploadImage(
+ thumbFile,
+ channel,
+ );
+
+ final url = await _uploadAttachment(
+ file,
+ medium.mediaType == MediaType.image
+ ? DefaultAttachmentTypes.image
+ : DefaultAttachmentTypes.video,
+ channel);
+
+ final fileType = medium.mediaType == MediaType.image
+ ? DefaultAttachmentTypes.image
+ : DefaultAttachmentTypes.video;
+
+ if (fileType == DefaultAttachmentTypes.image) {
+ attachment.attachment = attachment.attachment.copyWith(
+ imageUrl: url,
+ thumbUrl: thumbUrl,
+ );
+ } else {
+ attachment.attachment = attachment.attachment.copyWith(
+ assetUrl: url,
+ thumbUrl: thumbUrl,
+ );
+ }
+
+ setState(() {
+ attachment.uploaded = true;
+ });
+ }
+
CircleAvatar _buildGiphyIcon() {
if (kIsWeb) {
return CircleAvatar(
@@ -857,24 +1124,11 @@ class MessageInputState extends State {
_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 _attachments.isEmpty
? Container()
: LimitedBox(
- maxHeight: 76.0,
+ maxHeight: 104.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: _attachments
@@ -888,8 +1142,8 @@ class MessageInputState extends State {
AspectRatio(
aspectRatio: 1.0,
child: Container(
- height: 50,
- width: 50,
+ height: 104,
+ width: 104,
child: _buildAttachment(attachment),
),
),
@@ -934,11 +1188,12 @@ class MessageInputState extends State {
_attachments.remove(attachment);
});
},
- fillColor: Colors.white.withOpacity(.5),
+ fillColor: Colors.black.withOpacity(.5),
child: Center(
child: Icon(
- Icons.close,
+ StreamIcons.close,
size: 15,
+ color: Colors.white,
),
),
),
@@ -970,9 +1225,26 @@ class MessageInputState extends State {
);
break;
case 'video':
- return Container(
- child: Icon(Icons.videocam),
- color: Colors.black26,
+ return Stack(
+ children: [
+ Container(
+ child: attachment.thumbFile != null
+ ? Image.memory(
+ attachment.thumbFile.bytes,
+ fit: BoxFit.cover,
+ )
+ : Icon(Icons.videocam),
+ color: Colors.black26,
+ ),
+ Positioned(
+ left: 8,
+ bottom: 10,
+ child: SvgPicture.asset(
+ 'svgs/video_call_icon.svg',
+ package: 'stream_chat_flutter',
+ ),
+ ),
+ ],
);
break;
default:
@@ -984,26 +1256,24 @@ class MessageInputState extends State {
}
Widget _buildCommandButton() {
- return Center(
- child: InkWell(
- child: Padding(
- padding: const EdgeInsets.only(
- left: 4.0, right: 8.0, top: 8.0, bottom: 8.0),
- child: Icon(
- StreamIcons.lightning,
- color: Color(0xFF000000).withAlpha(128),
- ),
+ return InkWell(
+ child: Padding(
+ padding:
+ const EdgeInsets.only(left: 4.0, right: 8.0, top: 8.0, bottom: 8.0),
+ child: Icon(
+ StreamIcons.lightning,
+ color: Color(0xFF000000).withAlpha(128),
),
- onTap: () {
- 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;
+ }
+ },
);
}
@@ -1016,11 +1286,37 @@ class MessageInputState extends State {
EdgeInsets.only(left: 8.0, right: padding, top: 8.0, bottom: 8.0),
child: Icon(
StreamIcons.attach,
- color: Color(0xFF000000).withAlpha(128),
+ color: _openFilePickerSection
+ ? StreamChatTheme.of(context).accentColor
+ : Color(0xFF000000).withAlpha(128),
),
),
- onTap: () {
- showAttachmentModal();
+ onTap: () async {
+ _emojiOverlay?.remove();
+ _emojiOverlay = null;
+ _commandsOverlay?.remove();
+ _commandsOverlay = null;
+ _mentionsOverlay?.remove();
+ _mentionsOverlay = null;
+
+ if (_openFilePickerSection) {
+ setState(() {
+ _animateContainer = true;
+ _openFilePickerSection = false;
+ _filePickerSize = 250.0;
+ });
+ } else {
+ final status = await (Platform.isAndroid
+ ? Permission.storage.status
+ : Permission.photos.status);
+ if (status.isUndetermined) {
+ await (Platform.isAndroid
+ ? Permission.storage
+ : Permission.photos)
+ .request();
+ }
+ showAttachmentModal();
+ }
},
),
);
@@ -1032,73 +1328,79 @@ class MessageInputState extends State {
_focusNode.unfocus();
}
- showModalBottomSheet(
- clipBehavior: Clip.hardEdge,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.only(
- topLeft: Radius.circular(32),
- topRight: Radius.circular(32),
+ if (!kIsWeb) {
+ setState(() {
+ _openFilePickerSection = true;
+ });
+ } else {
+ showModalBottomSheet(
+ clipBehavior: Clip.hardEdge,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(32),
+ topRight: Radius.circular(32),
+ ),
),
- ),
- context: context,
- isScrollControlled: true,
- builder: (_) {
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- ListTile(
- title: Text(
- 'Add a file',
- style: TextStyle(
- fontWeight: FontWeight.bold,
+ context: context,
+ isScrollControlled: true,
+ builder: (_) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ListTile(
+ title: Text(
+ 'Add a file',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ ),
),
),
- ),
- ListTile(
- leading: Icon(Icons.image),
- title: Text('Upload a photo'),
- onTap: () {
- pickFile(DefaultAttachmentTypes.image, false);
- Navigator.pop(context);
- },
- ),
- ListTile(
- leading: Icon(Icons.video_library),
- title: Text('Upload a video'),
- onTap: () {
- pickFile(DefaultAttachmentTypes.video, false);
- Navigator.pop(context);
- },
- ),
- if (!kIsWeb)
ListTile(
- leading: Icon(Icons.camera_alt),
- title: Text('Photo from camera'),
+ leading: Icon(Icons.image),
+ title: Text('Upload a photo'),
onTap: () {
- pickFile(DefaultAttachmentTypes.image, true);
+ pickFile(DefaultAttachmentTypes.image, false);
Navigator.pop(context);
},
),
- if (!kIsWeb)
ListTile(
- leading: Icon(Icons.videocam),
- title: Text('Video from camera'),
+ leading: Icon(Icons.video_library),
+ title: Text('Upload a video'),
onTap: () {
- pickFile(DefaultAttachmentTypes.video, true);
+ pickFile(DefaultAttachmentTypes.video, false);
Navigator.pop(context);
},
),
- ListTile(
- leading: Icon(Icons.insert_drive_file),
- title: Text('Upload a file'),
- onTap: () {
- pickFile(DefaultAttachmentTypes.file, false);
- Navigator.pop(context);
- },
- ),
- ],
- );
- });
+ if (!kIsWeb)
+ ListTile(
+ leading: Icon(Icons.camera_alt),
+ title: Text('Photo from camera'),
+ onTap: () {
+ pickFile(DefaultAttachmentTypes.image, true);
+ Navigator.pop(context);
+ },
+ ),
+ if (!kIsWeb)
+ ListTile(
+ leading: Icon(Icons.videocam),
+ title: Text('Video from camera'),
+ onTap: () {
+ pickFile(DefaultAttachmentTypes.video, true);
+ Navigator.pop(context);
+ },
+ ),
+ ListTile(
+ leading: Icon(Icons.insert_drive_file),
+ title: Text('Upload a file'),
+ onTap: () {
+ pickFile(DefaultAttachmentTypes.file, false);
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ );
+ });
+ }
}
/// Add an attachment to the sending message
@@ -1228,7 +1530,9 @@ class MessageInputState extends State {
MultipartFile.fromBytes(
bytes,
filename: filename,
- contentType: MediaType.parse(lookupMimeType(filename)),
+ contentType: filename != null
+ ? httpParser.MediaType.parse(lookupMimeType(filename))
+ : null,
),
);
return res.file;
@@ -1241,7 +1545,7 @@ class MessageInputState extends State {
MultipartFile.fromBytes(
bytes,
filename: filename,
- contentType: MediaType.parse(lookupMimeType(filename)),
+ contentType: httpParser.MediaType.parse(lookupMimeType(filename)),
),
);
return res.file;
@@ -1332,7 +1636,6 @@ class MessageInputState extends State {
setState(() {
_messageIsPresent = false;
- _typingStarted = false;
_commandEnabled = false;
});
@@ -1405,42 +1708,9 @@ class MessageInputState extends State {
if (!kIsWeb) {
_keyboardListener = KeyboardVisibility.onChange.listen((visible) {
if (visible) {
- if (_commandsOverlay != null) {
- if (textEditingController.text.trimLeft().startsWith('/')) {
- WidgetsBinding.instance.addPostFrameCallback((_) {
- _commandsOverlay = _buildCommandsOverlayEntry();
- Overlay.of(context).insert(_commandsOverlay);
- });
- }
- }
-
- if (_mentionsOverlay != null) {
- if (textEditingController.text.contains('@')) {
- WidgetsBinding.instance.addPostFrameCallback((_) {
- _mentionsOverlay = _buildCommandsOverlayEntry();
- Overlay.of(context).insert(_mentionsOverlay);
- });
- }
- }
-
- if (_emojiOverlay != null) {
- if (textEditingController.text.contains(':')) {
- WidgetsBinding.instance.addPostFrameCallback((_) {
- _emojiOverlay = _buildEmojiOverlay();
- Overlay.of(context).insert(_emojiOverlay);
- });
- }
- }
- } else {
- if (_commandsOverlay != null) {
- _commandsOverlay.remove();
- }
- if (_mentionsOverlay != null) {
- _mentionsOverlay.remove();
- }
- if (_emojiOverlay != null) {
- _emojiOverlay.remove();
- }
+ _checkCommands(textEditingController.text, context);
+ _checkMentions(textEditingController.text, context);
+ _checkEmoji(textEditingController.text, context);
}
});
}
@@ -1450,12 +1720,17 @@ class MessageInputState extends State {
if (widget.editMessage != null || widget.initialMessage != null) {
_parseExistingMessage(widget.editMessage ?? widget.initialMessage);
}
+
+ _focusNode.addListener(() {
+ if (_focusNode.hasFocus) {
+ _openFilePickerSection = false;
+ }
+ });
}
void _parseExistingMessage(Message message) {
textEditingController.text = message.text;
- _typingStarted = true;
_messageIsPresent = true;
message.attachments?.forEach((attachment) {
@@ -1488,13 +1763,17 @@ class MessageInputState extends State {
class _SendingAttachment {
PlatformFile file;
+ PlatformFile thumbFile;
Attachment attachment;
bool uploaded;
+ String id;
_SendingAttachment({
this.file,
+ this.thumbFile,
this.attachment,
this.uploaded = false,
+ this.id,
});
}
@@ -1503,3 +1782,44 @@ extension StringExtension on String {
return "${this[0].toUpperCase()}${this.substring(1)}";
}
}
+
+/// Represents a 2-tuple, or pair.
+class Tuple2 {
+ /// Returns the first item of the tuple
+ final T1 item1;
+
+ /// Returns the second item of the tuple
+ final T2 item2;
+
+ /// Creates a new tuple value with the specified items.
+ const Tuple2(this.item1, this.item2);
+
+ /// Create a new tuple value with the specified list [items].
+ factory Tuple2.fromList(List items) {
+ if (items.length != 2) {
+ throw ArgumentError('items must have length 2');
+ }
+
+ return Tuple2(items[0] as T1, items[1] as T2);
+ }
+
+ /// Returns a tuple with the first item set to the specified value.
+ Tuple2 withItem1(T1 v) => Tuple2(v, item2);
+
+ /// Returns a tuple with the second item set to the specified value.
+ Tuple2 withItem2(T2 v) => Tuple2(item1, v);
+
+ /// Creates a [List] containing the items of this [Tuple2].
+ ///
+ /// The elements are in item order. The list is variable-length
+ /// if [growable] is true.
+ List toList({bool growable = false}) =>
+ List.from([item1, item2], growable: growable);
+
+ @override
+ String toString() => '[$item1, $item2]';
+
+ @override
+ bool operator ==(Object other) =>
+ other is Tuple2 && other.item1 == item1 && other.item2 == item2;
+}
diff --git a/lib/src/message_reactions_modal.dart b/lib/src/message_reactions_modal.dart
index fc3f68a4..d68bd769 100644
--- a/lib/src/message_reactions_modal.dart
+++ b/lib/src/message_reactions_modal.dart
@@ -36,14 +36,15 @@ class MessageReactionsModal extends StatelessWidget {
@override
Widget build(BuildContext context) {
- var size = MediaQuery.of(context).size;
- var user = StreamChat.of(context).user;
+ final size = MediaQuery.of(context).size;
+ final user = StreamChat.of(context).user;
- var roughMaxSize = 2 * size.width / 3;
- var roughSentenceSize =
+ final roughMaxSize = 2 * size.width / 3;
+ final roughSentenceSize =
message.text.length * messageTheme.messageText.fontSize * 1.2;
- var divFactor =
- roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize);
+ final divFactor = message.attachments?.isNotEmpty == true
+ ? 1
+ : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
return GestureDetector(
behavior: HitTestBehavior.translucent,
diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart
index fd6286a2..4f551655 100644
--- a/lib/src/message_widget.dart
+++ b/lib/src/message_widget.dart
@@ -585,14 +585,22 @@ class _MessageWidgetState extends State {
widget.message,
attachment,
);
- return wrapAttachmentWidget(context, attachmentWidget,
- attachment: attachment);
+ return wrapAttachmentWidget(
+ context,
+ attachmentWidget,
+ attachment: attachment,
+ );
})?.toList() ??
[];
}
- Padding wrapAttachmentWidget(BuildContext context, Widget attachmentWidget,
- {Attachment attachment}) {
+ Padding wrapAttachmentWidget(
+ BuildContext context,
+ Widget attachmentWidget, {
+ Attachment attachment,
+ }) {
+ final attachmentShape =
+ widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context);
return Padding(
padding: EdgeInsets.only(
bottom: 4,
@@ -605,13 +613,12 @@ class _MessageWidgetState extends State {
? Colors.white
: _getBackgroundColor(),
clipBehavior: Clip.hardEdge,
- shape: widget.attachmentShape ??
- widget.shape ??
- _getDefaultShape(context),
+ shape: attachmentShape,
child: Padding(
padding: widget.attachmentPadding,
- child: ClipRRect(
- borderRadius: BorderRadius.circular(6),
+ child: Material(
+ clipBehavior: Clip.hardEdge,
+ shape: attachmentShape,
child: Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
@@ -809,7 +816,8 @@ class _MessageWidgetState extends State {
),
),
if (widget.message.attachments
- .any((element) => element.ogScrapeUrl != null))
+ ?.any((element) => element.ogScrapeUrl != null) ==
+ true)
_buildUrlAttachment(),
],
),
@@ -836,7 +844,8 @@ class _MessageWidgetState extends State {
}
if (widget.message.attachments
- .any((element) => element.ogScrapeUrl != null)) {
+ ?.any((element) => element.ogScrapeUrl != null) ==
+ true) {
return Color(0xFFE9F2FF);
}
diff --git a/pubspec.yaml b/pubspec.yaml
index a3a1f55a..ccbc8463 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -35,6 +35,9 @@ dependencies:
flutter_slidable: ^0.5.4
carousel_slider: ^2.2.1
clipboard: ^0.1.2+8
+ media_gallery: ^0.1.5
+ permission_handler: ^5.0.1+1
+ transparent_image: ^1.0.0
flutter:
assets:
diff --git a/svgs/icon_picture_empty_state.svg b/svgs/icon_picture_empty_state.svg
new file mode 100644
index 00000000..135772d0
--- /dev/null
+++ b/svgs/icon_picture_empty_state.svg
@@ -0,0 +1,4 @@
+
diff --git a/svgs/video_call_icon.svg b/svgs/video_call_icon.svg
new file mode 100644
index 00000000..1c3832d6
--- /dev/null
+++ b/svgs/video_call_icon.svg
@@ -0,0 +1,3 @@
+