Merge pull request #128 from GetStream/feature/file-picker
Feature/file picker
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 |
@@ -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 |
Reference in New Issue
Block a user