Merge pull request #128 from GetStream/feature/file-picker

Feature/file picker
This commit is contained in:
Salvatore Giordano
2020-11-16 16:37:03 +01:00
committed by GitHub
14 changed files with 880 additions and 373 deletions
@@ -6,10 +6,14 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:name=".Application"
android:label="ChatSample"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"
>
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
+12
View File
@@ -108,6 +108,8 @@ PODS:
- GoogleUtilities/Logger
- image_picker (0.0.1):
- Flutter
- media_gallery (0.0.1):
- Flutter
- nanopb (1.30906.0):
- nanopb/decode (= 1.30906.0)
- nanopb/encode (= 1.30906.0)
@@ -115,6 +117,8 @@ PODS:
- nanopb/encode (1.30906.0)
- path_provider (0.0.1):
- Flutter
- "permission_handler (5.0.1+1)":
- Flutter
- PromisesObjC (1.2.11)
- Protobuf (3.13.0)
- SDWebImage (5.9.4):
@@ -165,7 +169,9 @@ DEPENDENCIES:
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- image_picker (from `.symlinks/plugins/image_picker/ios`)
- media_gallery (from `.symlinks/plugins/media_gallery/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
- permission_handler (from `.symlinks/plugins/permission_handler/ios`)
- shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
@@ -217,8 +223,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
image_picker:
:path: ".symlinks/plugins/image_picker/ios"
media_gallery:
:path: ".symlinks/plugins/media_gallery/ios"
path_provider:
:path: ".symlinks/plugins/path_provider/ios"
permission_handler:
:path: ".symlinks/plugins/permission_handler/ios"
shared_preferences:
:path: ".symlinks/plugins/shared_preferences/ios"
sqflite:
@@ -254,8 +264,10 @@ SPEC CHECKSUMS:
GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833
GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3
image_picker: 9c3312491f862b28d21ecd8fdf0ee14e601b3f09
media_gallery: 834a04455a476b975c280aa1f24ed2853dfe36de
nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
permission_handler: eac8e15b4a1a3fba55b761d19f3f4e6b005d15b6
PromisesObjC: 8c196f5a328c2cba3e74624585467a557dcb482f
Protobuf: 3dac39b34a08151c6d949560efe3f86134a3f748
SDWebImage: b69257f4ab14e9b6a2ef53e910fdf914d8f757c1
@@ -327,6 +327,7 @@
"${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework",
"${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework",
"${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework",
"${BUILT_PRODUCTS_DIR}/media_gallery/media_gallery.framework",
"${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
"${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework",
"${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework",
@@ -357,6 +358,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_gallery.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework",
+135 -133
View File
@@ -59,165 +59,167 @@ class ChooseUserPage extends StatelessWidget {
(_, value) => 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(),
],
),
),
);
}
+66 -64
View File
@@ -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,
),
),
),
),
),
],
],
),
),
),
),
+1 -1
View File
@@ -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"
+144
View File
@@ -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<String> 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<MediaListView> {
final _media = <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<MediaCollection> 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);
});
}
}
+7 -6
View File
@@ -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,
+471 -151
View File
@@ -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<MessageInput> {
final _imagePicker = ImagePicker();
bool _inputEnabled = true;
bool _messageIsPresent = false;
bool _typingStarted = false;
bool _animateContainer = true;
bool _commandEnabled = false;
OverlayEntry _commandsOverlay, _mentionsOverlay, _emojiOverlay;
Iterable<String> _emojiNames;
@@ -176,6 +180,9 @@ class MessageInputState extends State<MessageInput> {
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<MessageInput> {
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _buildDmCheckbox(),
),
_buildFilePickerSection(),
],
),
),
@@ -210,7 +218,7 @@ class MessageInputState extends State<MessageInput> {
Flex _buildTextField(BuildContext context) {
return Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
if (!_commandEnabled) _buildExpandActionsButton(),
if (widget.actionsLocation == ActionsLocation.left)
@@ -370,25 +378,17 @@ class MessageInputState extends State<MessageInput> {
_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<MessageInput> {
);
}
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<MessageInput> {
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<MessageInput> {
});
}
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<PermissionStatus>(
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<MessageInput> {
_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<MessageInput> {
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<MessageInput> {
_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<MessageInput> {
);
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<MessageInput> {
}
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<MessageInput> {
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<MessageInput> {
_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: <Widget>[
ListTile(
title: Text(
'Add a file',
style: TextStyle(
fontWeight: FontWeight.bold,
context: context,
isScrollControlled: true,
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(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<MessageInput> {
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<MessageInput> {
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<MessageInput> {
setState(() {
_messageIsPresent = false;
_typingStarted = false;
_commandEnabled = false;
});
@@ -1405,42 +1708,9 @@ class MessageInputState extends State<MessageInput> {
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<MessageInput> {
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<MessageInput> {
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<T1, T2> {
/// 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<T1, T2>(items[0] as T1, items[1] as T2);
}
/// Returns a tuple with the first item set to the specified value.
Tuple2<T1, T2> withItem1(T1 v) => Tuple2<T1, T2>(v, item2);
/// Returns a tuple with the second item set to the specified value.
Tuple2<T1, T2> withItem2(T2 v) => Tuple2<T1, T2>(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;
}
+7 -6
View File
@@ -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,
+20 -11
View File
@@ -585,14 +585,22 @@ class _MessageWidgetState extends State<MessageWidget> {
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<MessageWidget> {
? 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<MessageWidget> {
),
),
if (widget.message.attachments
.any((element) => element.ogScrapeUrl != null))
?.any((element) => element.ogScrapeUrl != null) ==
true)
_buildUrlAttachment(),
],
),
@@ -836,7 +844,8 @@ class _MessageWidgetState extends State<MessageWidget> {
}
if (widget.message.attachments
.any((element) => element.ogScrapeUrl != null)) {
?.any((element) => element.ogScrapeUrl != null) ==
true) {
return Color(0xFFE9F2FF);
}
+3
View File
@@ -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:
+4
View File
@@ -0,0 +1,4 @@
<svg width="136" height="136" viewBox="0 0 136 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3333 45.3333C11.3333 35.9445 18.9444 28.3333 28.3333 28.3333H107.667C117.055 28.3333 124.667 35.9445 124.667 45.3333V90.6667C124.667 100.055 117.055 107.667 107.667 107.667H28.3333C18.9444 107.667 11.3333 100.055 11.3333 90.6667V45.3333ZM28.3333 39.6666C25.2036 39.6666 22.6666 42.2037 22.6666 45.3333V90.6666C22.6666 93.7963 25.2036 96.3333 28.3333 96.3333H107.667C110.796 96.3333 113.333 93.7963 113.333 90.6666V45.3333C113.333 42.2037 110.796 39.6666 107.667 39.6666H28.3333Z" fill="#D6D6D6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M90.61 51.0003C92.3115 50.9833 93.9306 51.7318 95.0199 53.039L123.353 87.039C125.357 89.4432 125.032 93.0164 122.628 95.0199C120.223 97.0234 116.65 96.6986 114.647 94.2944L90.7563 65.6258L72.4249 88.5399C70.5129 90.9299 67.0486 91.3698 64.6 89.5333L45.8651 75.4821L26.6736 94.6736C24.4606 96.8866 20.8727 96.8866 18.6597 94.6736C16.4468 92.4606 16.4468 88.8727 18.6597 86.6597L41.3264 63.9931C43.3204 61.999 46.4773 61.7747 48.7333 63.4667L67.0061 77.1713L86.2417 53.1267C87.3047 51.798 88.9085 51.0173 90.61 51.0003Z" fill="#D6D6D6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.525 8.15C20.23 7.967 19.862 7.951 19.552 8.106L17 9.382V8C17 6.896 16.104 6 15 6H6C4.896 6 4 6.896 4 8V16C4 17.104 4.896 18 6 18H15C16.104 18 17 17.104 17 16V14.619L19.553 15.894C19.693 15.965 19.848 16 20 16C20.183 16 20.365 15.949 20.525 15.851C20.82 15.668 21 15.347 21 15V9C21 8.653 20.82 8.332 20.525 8.15Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 444 B