diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 6fb8664b..0492c6da 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -6,8 +6,8 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
-
-
+
+
{
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0._JHWzo92fpTWZMZriJHXqOng6ShYVmWrdaIaPwEPKBg':
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0.lCz-idDgaZ-xszjnuB_hTfeIOhTFmJtTB2fEjhwrcCI':
User(
id: 'vishal',
extraData: {
'name': 'Vishal',
},
),
- 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A':
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.1AvvK2TvEsfcxAlfOK2iUMFACucnk6N-Q8f5bbfnjCM':
User(
id: 'super-band-9',
extraData: {
'name': 'John Doe',
},
),
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FsdmF0b3JlIn0.GMEKyEhONmFnqtkf1TR1A3oUOSIWhjfQv5RpI906dAM':
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FsdmF0b3JlIn0.uQ-pz8e1_C-Df2rIFpkkvWf7g4M0aW2Zkr7pHVhmU1Y':
User(
id: 'salvatore',
extraData: {
'name': 'Salvatore',
},
),
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidG9tbWFzbyJ9.GTalMeZHaBdpIM5w-KWrVIDSy-ODkHTRkf1GZbWAveM':
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidG9tbWFzbyJ9.feLvMytd5_3-eETKrRdwtO3jcA0HxD8woFUbFU7gQCg':
User(
id: 'tommaso',
extraData: {
'name': 'Tommaso',
},
),
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiamFhcCJ9.xpGE2lu4OoYS7-2yy6PbF0gJnxOaxeGO4EU6xo4EdMU':
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiamFhcCJ9.VLkLs6KH-jRRkPvhYMa4_lqN0R6WiK7JhJVmYvxcKz0':
User(
id: 'jaap',
extraData: {
diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart
index 8301f339..80b42183 100644
--- a/lib/src/channel_list_view.dart
+++ b/lib/src/channel_list_view.dart
@@ -485,6 +485,11 @@ class _ChannelListViewState extends State
onTap: () {
onTap(channel, widget.channelWidget);
},
+ onLongPress: widget.onChannelLongPress != null
+ ? () {
+ widget.onChannelLongPress(channel);
+ }
+ : null,
),
),
),
diff --git a/lib/src/file_attachment.dart b/lib/src/file_attachment.dart
index 05318862..fff13bbe 100644
--- a/lib/src/file_attachment.dart
+++ b/lib/src/file_attachment.dart
@@ -1,32 +1,178 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
+import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart';
class FileAttachment extends StatelessWidget {
final Attachment attachment;
final Size size;
+ final Widget trailing;
const FileAttachment({
Key key,
@required this.attachment,
this.size,
+ this.trailing,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
- child: InkWell(
- onTap: () {
- launchURL(context, attachment.assetUrl);
- },
- child: Container(
- width: size?.width ?? 100,
- height: size?.height ?? 100,
- child: Center(
- child: Icon(Icons.attach_file),
- ),
+ child: Container(
+ width: size?.width ?? 100,
+ height: 56.0,
+ margin: trailing != null ? EdgeInsets.only(top: 4.0) : null,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: trailing != null ? BorderRadius.circular(16.0) : null,
+ border: trailing != null
+ ? Border.fromBorderSide(BorderSide(color: Color(0xFFE6E6E6)))
+ : null,
),
+ child: Row(
+ children: [
+ Container(
+ child: _getFileTypeImage(attachment.extraData['mime_type']),
+ height: 40.0,
+ width: 33.33,
+ margin: EdgeInsets.all(8.0),
+ ),
+ SizedBox(
+ width: 6.0,
+ ),
+ Expanded(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ attachment?.title ?? 'File',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 14.0,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ SizedBox(
+ height: 3.0,
+ ),
+ Text(
+ '${attachment.extraData['file_size'] ?? 'N/A'} bytes',
+ style: TextStyle(
+ color: Colors.black.withOpacity(0.5),
+ fontSize: 14.0,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Column(
+ children: [
+ trailing ??
+ IconButton(
+ icon: StreamSvgIcon.cloud_download(
+ color: Colors.black,
+ ),
+ onPressed: () {
+ launchURL(context, attachment.assetUrl);
+ },
+ ),
+ ],
+ ),
+ ],
+ ),
+
+ // ListTile(
+ // dense: true,
+ // leading: Container(
+ // child: _getFileTypeImage(attachment.extraData['mime_type']),
+ // height: 40.0,
+ // width: 33.33,
+ // ),
+ // title: Text(
+ // attachment?.title ?? 'File',
+ // style: TextStyle(
+ // fontWeight: FontWeight.bold,
+ // ),
+ // maxLines: 3,
+ // ),
+ // subtitle: Text(
+ // '${attachment.extraData['file_size'] ?? 'N/A'} bytes',
+ // style: TextStyle(
+ // color: Colors.black.withOpacity(0.5),
+ // ),
+ // ),
+ // trailing: trailing ??
+ // IconButton(
+ // icon: StreamSvgIcon.cloud_download(
+ // color: Colors.black,
+ // ),
+ // onPressed: () {
+ // launchURL(context, attachment.assetUrl);
+ // },
+ // ),
+ // ),
),
);
}
+
+ StreamSvgIcon _getFileTypeImage(String type) {
+ switch (type) {
+ case '7z':
+ return StreamSvgIcon.filetype_7z();
+ break;
+ case 'csv':
+ return StreamSvgIcon.filetype_csv();
+ break;
+ case 'doc':
+ return StreamSvgIcon.filetype_doc();
+ break;
+ case 'docx':
+ return StreamSvgIcon.filetype_docx();
+ break;
+ case 'html':
+ return StreamSvgIcon.filetype_html();
+ break;
+ case 'md':
+ return StreamSvgIcon.filetype_md();
+ break;
+ case 'odt':
+ return StreamSvgIcon.filetype_odt();
+ break;
+ case 'pdf':
+ return StreamSvgIcon.filetype_pdf();
+ break;
+ case 'ppt':
+ return StreamSvgIcon.filetype_ppt();
+ break;
+ case 'pptx':
+ return StreamSvgIcon.filetype_pptx();
+ break;
+ case 'rar':
+ return StreamSvgIcon.filetype_rar();
+ break;
+ case 'rtf':
+ return StreamSvgIcon.filetype_rtf();
+ break;
+ case 'tar':
+ return StreamSvgIcon.filetype_tar();
+ break;
+ case 'txt':
+ return StreamSvgIcon.filetype_txt();
+ break;
+ case 'xls':
+ return StreamSvgIcon.filetype_xls();
+ break;
+ case 'xlsx':
+ return StreamSvgIcon.filetype_xlsx();
+ break;
+ case 'zip':
+ return StreamSvgIcon.filetype_zip();
+ break;
+ default:
+ return StreamSvgIcon.filetype_Generic();
+ break;
+ }
+ }
}
diff --git a/lib/src/full_screen_image.dart b/lib/src/full_screen_image.dart
deleted file mode 100644
index 76953b1a..00000000
--- a/lib/src/full_screen_image.dart
+++ /dev/null
@@ -1,35 +0,0 @@
-import 'package:cached_network_image/cached_network_image.dart';
-import 'package:flutter/material.dart';
-import 'package:photo_view/photo_view.dart';
-
-/// A full screen image widget
-class FullScreenImage extends StatelessWidget {
- /// The url of the image
- final String url;
-
- /// Instantiate a new FullScreenImage
- const FullScreenImage({
- Key key,
- @required this.url,
- }) : super(key: key);
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- appBar: AppBar(
- backgroundColor: Colors.black,
- iconTheme: IconThemeData(
- color: Colors.white,
- ),
- ),
- body: PhotoView(
- imageProvider: CachedNetworkImageProvider(url),
- maxScale: PhotoViewComputedScale.covered,
- minScale: PhotoViewComputedScale.contained,
- heroAttributes: PhotoViewHeroAttributes(
- tag: url,
- ),
- ),
- );
- }
-}
diff --git a/lib/src/full_screen_media.dart b/lib/src/full_screen_media.dart
new file mode 100644
index 00000000..7e3f3d7d
--- /dev/null
+++ b/lib/src/full_screen_media.dart
@@ -0,0 +1,259 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:chewie/chewie.dart';
+import 'package:flutter/material.dart';
+import 'package:jiffy/jiffy.dart';
+import 'package:photo_view/photo_view.dart';
+import 'package:stream_chat_flutter/src/image_footer.dart';
+import 'package:stream_chat_flutter/src/image_header.dart';
+import 'package:video_player/video_player.dart';
+
+import '../stream_chat_flutter.dart';
+
+/// A full screen image widget
+class FullScreenMedia extends StatefulWidget {
+ /// The url of the image
+ final List mediaAttachments;
+ final Message message;
+
+ final int startIndex;
+ final String userName;
+ final DateTime sentAt;
+
+ /// Instantiate a new FullScreenImage
+ const FullScreenMedia({
+ Key key,
+ @required this.mediaAttachments,
+ this.message,
+ this.startIndex = 0,
+ this.userName = '',
+ this.sentAt,
+ }) : super(key: key);
+
+ @override
+ _FullScreenMediaState createState() => _FullScreenMediaState();
+}
+
+class _FullScreenMediaState extends State
+ with SingleTickerProviderStateMixin {
+ bool _optionsShown = true;
+
+ AnimationController _controller;
+ PageController _pageController;
+
+ int _currentPage;
+
+ List videoPackages = [];
+
+ @override
+ void initState() {
+ super.initState();
+ _controller =
+ AnimationController(vsync: this, duration: Duration(milliseconds: 300));
+ _pageController = PageController(initialPage: widget.startIndex);
+ _currentPage = widget.startIndex;
+ widget.mediaAttachments
+ .where((element) => element.type == 'video')
+ .toList()
+ .forEach((element) {
+ videoPackages.add(VideoPackage(context, element, () {
+ setState(() {});
+ }));
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var videoAttachments = widget.mediaAttachments
+ .where((element) => element.type == 'video')
+ .toList();
+
+ return Scaffold(
+ resizeToAvoidBottomInset: false,
+ body: Stack(
+ children: [
+ AnimatedBuilder(
+ animation: _controller,
+ builder: (context, snapshot) {
+ return PageView.builder(
+ controller: _pageController,
+ onPageChanged: (val) {
+ setState(() {
+ _currentPage = val;
+ });
+ },
+ itemBuilder: (context, position) {
+ if (widget.mediaAttachments[position].type == 'image' ||
+ widget.mediaAttachments[position].type == 'giphy') {
+ return PhotoView(
+ imageProvider: CachedNetworkImageProvider(
+ widget.mediaAttachments[position].imageUrl ??
+ widget.mediaAttachments[position].assetUrl ??
+ widget.mediaAttachments[position].thumbUrl),
+ maxScale: PhotoViewComputedScale.covered,
+ minScale: PhotoViewComputedScale.contained,
+ heroAttributes: PhotoViewHeroAttributes(
+ tag: widget.mediaAttachments,
+ ),
+ backgroundDecoration: BoxDecoration(
+ color: ColorTween(
+ begin: StreamChatTheme.of(context)
+ .channelTheme
+ .channelHeaderTheme
+ .color,
+ end: Colors.black)
+ .lerp(_controller.value),
+ ),
+ onTapUp: (a, b, c) {
+ setState(() {
+ _optionsShown = !_optionsShown;
+ });
+ if (_controller.isCompleted) {
+ _controller.reverse();
+ } else {
+ _controller.forward();
+ }
+ },
+ );
+ } else if (widget.mediaAttachments[position].type ==
+ 'video') {
+ var controllerPackage = videoPackages[videoAttachments
+ .indexOf(widget.mediaAttachments[position])];
+
+ if (!controllerPackage.initialised) {
+ return Center(
+ child: CircularProgressIndicator(),
+ );
+ }
+ return InkWell(
+ onTap: () {
+ setState(() {
+ _optionsShown = !_optionsShown;
+ });
+ if (_controller.isCompleted) {
+ _controller.reverse();
+ } else {
+ _controller.forward();
+ }
+ },
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 50.0,
+ ),
+ child: Chewie(
+ controller: controllerPackage.chewieController,
+ ),
+ ),
+ );
+ }
+ return Container();
+ },
+ itemCount: widget.mediaAttachments.length,
+ );
+ }),
+ AnimatedOpacity(
+ opacity: _optionsShown ? 1.0 : 0.0,
+ duration: Duration(milliseconds: 300),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ ImageHeader(
+ userName: widget.userName,
+ sentAt: widget.message.createdAt == null
+ ? ''
+ : 'Sent ${getDay(widget.message.createdAt)} at ${Jiffy(widget.sentAt.toLocal()).format('HH:mm')}',
+ onBackPressed: () {
+ Navigator.of(context).pop();
+ },
+ message: widget.message,
+ urls: widget.mediaAttachments,
+ currentIndex: _currentPage,
+ ),
+ ImageFooter(
+ currentPage: _currentPage,
+ totalPages: widget.mediaAttachments.length,
+ mediaAttachments: widget.mediaAttachments,
+ message: widget.message,
+ videoPackages: videoPackages,
+ mediaSelectedCallBack: (val) {
+ setState(() {
+ _currentPage = val;
+ _pageController.animateToPage(val,
+ duration: Duration(milliseconds: 300),
+ curve: Curves.easeInOut);
+ Navigator.pop(context);
+ });
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ String getDay(DateTime dateTime) {
+ var now = DateTime.now();
+
+ if (DateTime(dateTime.year, dateTime.month, dateTime.day) ==
+ DateTime(now.year, now.month, now.day)) {
+ return 'today';
+ } else if (DateTime(now.year, now.month, now.day)
+ .difference(dateTime)
+ .inHours <
+ 24) {
+ return 'yesterday';
+ } else {
+ return 'on ${Jiffy(dateTime).format("MMM do")}';
+ }
+ }
+
+ @override
+ void dispose() {
+ videoPackages.forEach((element) {
+ element.dispose();
+ });
+ super.dispose();
+ }
+}
+
+class VideoPackage {
+ VideoPlayerController _videoPlayerController;
+ ChewieController _chewieController;
+ bool initialised = false;
+ VoidCallback onInit;
+ BuildContext context;
+
+ ///
+ VideoPackage(this.context, Attachment attachment, this.onInit) {
+ _videoPlayerController = VideoPlayerController.network(attachment.assetUrl);
+ _videoPlayerController.initialize().whenComplete(() {
+ initialised = true;
+ _chewieController = ChewieController(
+ videoPlayerController: _videoPlayerController,
+ autoInitialize: false,
+ aspectRatio: _videoPlayerController.value.aspectRatio,
+ );
+ onInit();
+ });
+
+ VoidCallback errorListener;
+ errorListener = () {
+ if (_videoPlayerController.value.hasError) {
+ Navigator.pop(context);
+ launchURL(context, attachment.titleLink);
+ }
+ _videoPlayerController.removeListener(errorListener);
+ };
+ _videoPlayerController.addListener(errorListener);
+ }
+
+ get videoPlayer => _videoPlayerController;
+
+ get chewieController => _chewieController;
+
+ void dispose() {
+ _videoPlayerController.dispose();
+ _chewieController.dispose();
+ }
+}
diff --git a/lib/src/full_screen_video.dart b/lib/src/full_screen_video.dart
deleted file mode 100644
index 98729243..00000000
--- a/lib/src/full_screen_video.dart
+++ /dev/null
@@ -1,84 +0,0 @@
-import 'package:chewie/chewie.dart';
-import 'package:flutter/material.dart';
-import 'package:stream_chat_flutter/stream_chat_flutter.dart';
-import 'package:video_player/video_player.dart';
-
-import 'utils.dart';
-
-class FullScreenVideo extends StatefulWidget {
- final Attachment attachment;
-
- FullScreenVideo({
- Key key,
- @required this.attachment,
- }) : super(key: key);
-
- @override
- _FullScreenVideoState createState() => _FullScreenVideoState();
-}
-
-class _FullScreenVideoState extends State {
- ChewieController _chewieController;
- VideoPlayerController _videoPlayerController;
- bool initialized = false;
- final GlobalKey _scaffoldKey = GlobalKey();
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- appBar: AppBar(
- backgroundColor: Colors.black,
- iconTheme: IconThemeData(
- color: Colors.white,
- ),
- ),
- body: Builder(
- key: _scaffoldKey,
- builder: (context) {
- if (!initialized) {
- return Center(
- child: CircularProgressIndicator(),
- );
- }
- return Chewie(
- controller: _chewieController,
- );
- },
- ),
- );
- }
-
- @override
- void initState() {
- super.initState();
- _videoPlayerController =
- VideoPlayerController.network(widget.attachment.assetUrl);
- _videoPlayerController.initialize().whenComplete(() {
- setState(() {
- initialized = true;
- _chewieController = ChewieController(
- videoPlayerController: _videoPlayerController,
- autoInitialize: false,
- aspectRatio: _videoPlayerController.value.aspectRatio,
- );
- });
- });
-
- VoidCallback errorListener;
- errorListener = () {
- if (_videoPlayerController.value.hasError) {
- Navigator.pop(context);
- launchURL(_scaffoldKey.currentContext, widget.attachment.titleLink);
- }
- _videoPlayerController.removeListener(errorListener);
- };
- _videoPlayerController.addListener(errorListener);
- }
-
- @override
- void dispose() {
- _videoPlayerController?.dispose();
- _chewieController?.dispose();
- super.dispose();
- }
-}
diff --git a/lib/src/giphy_attachment.dart b/lib/src/giphy_attachment.dart
index 7abf7057..3e29b2f1 100644
--- a/lib/src/giphy_attachment.dart
+++ b/lib/src/giphy_attachment.dart
@@ -5,7 +5,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import '../stream_chat_flutter.dart';
import 'attachment_error.dart';
-import 'full_screen_image.dart';
+import 'full_screen_media.dart';
class GiphyAttachment extends StatelessWidget {
final Attachment attachment;
@@ -64,64 +64,75 @@ class GiphyAttachment extends StatelessWidget {
child: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
- return FullScreenImage(
- url: attachment.imageUrl ??
- attachment.assetUrl ??
- attachment.thumbUrl,
+ final channel = StreamChannel.of(context).channel;
+
+ return StreamChannel(
+ channel: channel,
+ child: FullScreenMedia(
+ mediaAttachments: [
+ attachment,
+ ],
+ userName: message.user.name,
+ sentAt: message.createdAt,
+ message: message,
+ ),
);
}));
},
- child: CachedNetworkImage(
- height: size?.height,
- width: size?.width,
- placeholder: (_, __) {
- return Container(
- width: size?.width,
- height: size?.height,
- child: Center(
- child: CircularProgressIndicator(),
- ),
- );
- },
- imageUrl: attachment.thumbUrl ??
- attachment.imageUrl ??
- attachment.assetUrl,
- errorWidget: (context, url, error) => AttachmentError(
- attachment: attachment,
- size: size,
+ child: ClipRRect(
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(8),
+ topRight: Radius.circular(8),
+ ),
+ child: CachedNetworkImage(
+ height: size?.height,
+ width: size?.width,
+ placeholder: (_, __) {
+ return Container(
+ width: size?.width,
+ height: size?.height,
+ child: Center(
+ child: CircularProgressIndicator(),
+ ),
+ );
+ },
+ imageUrl: attachment.thumbUrl ??
+ attachment.imageUrl ??
+ attachment.assetUrl,
+ errorWidget: (context, url, error) => AttachmentError(
+ attachment: attachment,
+ size: size,
+ ),
+ fit: BoxFit.cover,
),
- fit: BoxFit.cover,
),
),
),
Positioned(
- left: 0,
- top: 0,
- child: Container(
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.only(
- bottomRight: Radius.circular(16.0),
- )),
+ bottom: 16,
+ left: 16,
+ child: Material(
+ color: Colors.black.withOpacity(.5),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
child: Padding(
- padding: const EdgeInsets.only(
- left: 8.0,
- right: 8.0,
- top: 8.0,
- bottom: 4.0,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8.0,
+ vertical: 4.0,
),
child: Row(
children: [
StreamSvgIcon.lightning(
- color: StreamChatTheme.of(context).accentColor,
- size: 16.0,
+ color: Colors.white,
+ size: 16,
),
Text(
'GIPHY',
style: TextStyle(
- color: StreamChatTheme.of(context).accentColor,
+ color: Colors.white,
fontWeight: FontWeight.bold,
- fontSize: 11.0,
+ fontSize: 11,
),
),
],
@@ -278,10 +289,18 @@ class GiphyAttachment extends StatelessWidget {
child: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
- return FullScreenImage(
- url: attachment.imageUrl ??
- attachment.assetUrl ??
- attachment.thumbUrl,
+ var channel = StreamChannel.of(context).channel;
+
+ return StreamChannel(
+ channel: channel,
+ child: FullScreenMedia(
+ mediaAttachments: [
+ attachment,
+ ],
+ userName: message.user.name,
+ sentAt: message.createdAt,
+ message: message,
+ ),
);
}));
},
diff --git a/lib/src/image_actions_modal.dart b/lib/src/image_actions_modal.dart
new file mode 100644
index 00000000..fc81635c
--- /dev/null
+++ b/lib/src/image_actions_modal.dart
@@ -0,0 +1,222 @@
+import 'dart:typed_data';
+import 'dart:ui';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:dio/dio.dart';
+import 'package:image_gallery_saver/image_gallery_saver.dart';
+import '../stream_chat_flutter.dart';
+import 'package:path_provider/path_provider.dart';
+
+class ImageActionsModal extends StatelessWidget {
+ final Message message;
+ final String userName;
+ final String sentAt;
+ final List urls;
+ final currentIndex;
+
+ ImageActionsModal(
+ {this.message, this.userName, this.sentAt, this.urls, this.currentIndex});
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ behavior: HitTestBehavior.translucent,
+ onTap: () {
+ Navigator.pop(context);
+ },
+ child: Stack(
+ children: [
+ Positioned.fill(
+ child: Container(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: [
+ Colors.black.withOpacity(0.8),
+ Colors.transparent,
+ ],
+ stops: [0.0, 0.4],
+ )),
+ )),
+ _buildPage(context),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildPage(context) {
+ return Material(
+ color: Colors.transparent,
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Container(
+ width: 40.0,
+ ),
+ ),
+ Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: Text(
+ userName,
+ style: TextStyle(
+ fontSize: 16.0,
+ fontWeight: FontWeight.w700,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ Text(
+ sentAt,
+ style: StreamChatTheme.of(context)
+ .channelPreviewTheme
+ .subtitle
+ .copyWith(color: Colors.white.withOpacity(0.5)),
+ ),
+ ],
+ ),
+ IconButton(
+ icon: StreamSvgIcon.close(
+ size: 24.0,
+ color: Colors.white,
+ ),
+ onPressed: () {
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ ),
+ Align(
+ alignment: Alignment.centerRight,
+ child: Container(
+ width: MediaQuery.of(context).size.width / 1.8,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Material(
+ clipBehavior: Clip.hardEdge,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ color: Color(0xffe5e5e5),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: ListTile.divideTiles(
+ context: context,
+ tiles: [
+ _buildButton(
+ context,
+ 'Reply',
+ StreamSvgIcon.Icon_curve_line_left_up(
+ size: 24.0,
+ color: Colors.black.withOpacity(0.5),
+ ),
+ () {}),
+ _buildButton(
+ context,
+ 'Show in Chat',
+ StreamSvgIcon.eye(
+ size: 24.0,
+ color: Colors.black,
+ ), () {
+ Navigator.pop(context);
+ Navigator.pop(context);
+ }),
+ _buildButton(
+ context,
+ 'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}',
+ StreamSvgIcon.Icon_save(
+ size: 24.0,
+ color: Colors.black.withOpacity(0.5),
+ ), () async {
+ var url = urls[currentIndex].imageUrl ??
+ urls[currentIndex].assetUrl ??
+ urls[currentIndex].thumbUrl;
+
+ if (urls[currentIndex].type == 'video') {
+ await _saveVideo(url);
+ Navigator.pop(context);
+ } else {
+ await _saveImage(url);
+ Navigator.pop(context);
+ }
+ }),
+ if (StreamChat.of(context).user.id == message.user.id)
+ _buildButton(
+ context,
+ 'Delete',
+ StreamSvgIcon.delete(
+ size: 24.0,
+ color: Color(0xffFF3742),
+ ),
+ () {
+ Navigator.pop(context);
+ Navigator.pop(context);
+ StreamChat.of(context).client.deleteMessage(
+ message,
+ StreamChannel.of(context).channel.cid,
+ );
+ },
+ color: Color(0xffFF3742),
+ ),
+ ],
+ ).toList(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildButton(
+ context, String title, StreamSvgIcon icon, VoidCallback onTap,
+ {Color color}) {
+ var titleStyle = TextStyle(
+ fontSize: 14.5,
+ color: Colors.black,
+ );
+
+ return Material(
+ child: InkWell(
+ onTap: onTap,
+ child: ListTile(
+ dense: true,
+ title: Text(
+ title,
+ style:
+ color == null ? titleStyle : titleStyle.copyWith(color: color),
+ ),
+ leading: icon,
+ ),
+ ),
+ );
+ }
+
+ Future _saveImage(String url) async {
+ var response = await Dio()
+ .get(url, options: Options(responseType: ResponseType.bytes));
+ final result = await ImageGallerySaver.saveImage(
+ Uint8List.fromList(response.data),
+ quality: 60,
+ name: "${DateTime.now().millisecondsSinceEpoch}");
+ return result;
+ }
+
+ Future _saveVideo(String url) async {
+ var appDocDir = await getTemporaryDirectory();
+ var savePath =
+ appDocDir.path + "/${DateTime.now().millisecondsSinceEpoch}.mp4";
+ await Dio().download(url, savePath);
+ final result = await ImageGallerySaver.saveFile(savePath);
+ print(result);
+ }
+}
diff --git a/lib/src/image_attachment.dart b/lib/src/image_attachment.dart
index ba11252a..2345b2b8 100644
--- a/lib/src/image_attachment.dart
+++ b/lib/src/image_attachment.dart
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import '../stream_chat_flutter.dart';
import 'attachment_error.dart';
import 'attachment_title.dart';
-import 'full_screen_image.dart';
+import 'full_screen_media.dart';
import 'utils.dart';
class ImageAttachment extends StatelessWidget {
@@ -43,10 +43,18 @@ class ImageAttachment extends StatelessWidget {
context,
MaterialPageRoute(
builder: (_) {
- return FullScreenImage(
- url: attachment.imageUrl ??
- attachment.assetUrl ??
- attachment.thumbUrl,
+ final channel = StreamChannel.of(context).channel;
+
+ return StreamChannel(
+ channel: channel,
+ child: FullScreenMedia(
+ mediaAttachments: [
+ attachment,
+ ],
+ userName: message.user.name,
+ sentAt: message.createdAt,
+ message: message,
+ ),
);
},
),
diff --git a/lib/src/image_footer.dart b/lib/src/image_footer.dart
new file mode 100644
index 00000000..3c37a66a
--- /dev/null
+++ b/lib/src/image_footer.dart
@@ -0,0 +1,683 @@
+import 'dart:async';
+import 'dart:io';
+import 'dart:math';
+import 'dart:typed_data';
+
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:chewie/chewie.dart';
+import 'package:esys_flutter_share/esys_flutter_share.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:dio/dio.dart';
+import 'package:image_gallery_saver/image_gallery_saver.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:stream_chat/stream_chat.dart';
+import 'package:stream_chat_flutter/src/stream_chat.dart';
+import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
+import 'package:stream_chat_flutter/stream_chat_flutter.dart';
+
+import 'stream_channel.dart';
+
+class ImageFooter extends StatefulWidget {
+ /// Callback to call when pressing the back button.
+ /// By default it calls [Navigator.pop]
+ final VoidCallback onBackPressed;
+
+ /// Callback to call when the header is tapped.
+ final VoidCallback onTitleTap;
+
+ /// Callback to call when the image is tapped.
+ final VoidCallback onImageTap;
+
+ final int currentPage;
+ final int totalPages;
+
+ final List mediaAttachments;
+ final Message message;
+
+ final List videoPackages;
+ final ValueChanged mediaSelectedCallBack;
+
+ /// Creates a channel header
+ ImageFooter({
+ Key key,
+ this.onBackPressed,
+ this.onTitleTap,
+ this.onImageTap,
+ this.currentPage = 0,
+ this.totalPages = 0,
+ this.mediaAttachments,
+ this.message,
+ this.videoPackages,
+ this.mediaSelectedCallBack,
+ }) : preferredSize = Size.fromHeight(kToolbarHeight),
+ super(key: key);
+
+ @override
+ _ImageFooterState createState() => _ImageFooterState();
+
+ @override
+ final Size preferredSize;
+}
+
+class _ImageFooterState extends State {
+ bool _userSearchMode = false;
+ TextEditingController _searchController;
+ TextEditingController _messageController = TextEditingController();
+
+ String _userNameQuery;
+ bool _isSearchActive = false;
+
+ Set _selectedUsers = {};
+ bool _loading = false;
+
+ Timer _debounce;
+
+ Function modalSetStateCallback;
+
+ void _userNameListener() {
+ if (_debounce?.isActive ?? false) _debounce.cancel();
+ _debounce = Timer(const Duration(milliseconds: 350), () {
+ if (mounted && modalSetStateCallback != null) {
+ modalSetStateCallback(() {
+ _userNameQuery = _searchController.text;
+ _isSearchActive = _userNameQuery.isNotEmpty;
+ });
+ }
+ });
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _searchController = TextEditingController()..addListener(_userNameListener);
+ }
+
+ @override
+ void dispose() {
+ _searchController?.clear();
+ _searchController?.removeListener(_userNameListener);
+ _searchController?.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SafeArea(
+ child: Container(
+ color:
+ StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
+ padding: EdgeInsets.symmetric(vertical: 8.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ IconButton(
+ icon: StreamSvgIcon.icon_SHARE(
+ size: 24.0,
+ color: Colors.black,
+ ),
+ onPressed: () {
+ _buildShareModal(context);
+ },
+ ),
+ InkWell(
+ onTap: widget.onTitleTap,
+ child: Container(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ '${widget.currentPage + 1} of ${widget.totalPages}',
+ style: TextStyle(
+ fontSize: 16.0,
+ fontWeight: FontWeight.w700,
+ color: Colors.black,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ IconButton(
+ icon: StreamSvgIcon.Icon_grid(
+ color: Colors.black,
+ ),
+ onPressed: () {
+ _buildPhotosModal(context);
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildPhotosModal(context) {
+ var videoAttachments = widget.mediaAttachments
+ .where((element) => element.type == 'video')
+ .toList();
+
+ showModalBottomSheet(
+ context: context,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16.0),
+ ),
+ builder: (context) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.only(
+ topRight: Radius.circular(16.0),
+ topLeft: Radius.circular(16.0),
+ )),
+ child: Stack(
+ children: [
+ Center(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Text(
+ 'Photos',
+ style: TextStyle(
+ fontSize: 16.0,
+ fontWeight: FontWeight.w700,
+ color: Colors.black,
+ ),
+ ),
+ ),
+ ),
+ Align(
+ alignment: Alignment.centerRight,
+ child: IconButton(
+ icon: StreamSvgIcon.close(
+ color: Colors.black,
+ ),
+ onPressed: () {
+ Navigator.pop(context);
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ Container(
+ color: Colors.white,
+ child: GridView.builder(
+ shrinkWrap: true,
+ physics: NeverScrollableScrollPhysics(),
+ itemBuilder: (context, position) {
+ if (widget.mediaAttachments[position].type == 'video') {
+ var controllerPackage = widget.videoPackages[
+ videoAttachments
+ .indexOf(widget.mediaAttachments[position])];
+
+ return InkWell(
+ onTap: () {
+ widget.mediaSelectedCallBack(position);
+ },
+ child: FittedBox(
+ fit: BoxFit.cover,
+ child: Chewie(
+ controller: controllerPackage.chewieController,
+ ),
+ ),
+ );
+ } else {
+ return InkWell(
+ onTap: () {
+ widget.mediaSelectedCallBack(position);
+ },
+ child: Padding(
+ padding: const EdgeInsets.all(1.0),
+ child: AspectRatio(
+ child: CachedNetworkImage(
+ imageUrl: widget
+ .mediaAttachments[position].imageUrl ??
+ widget.mediaAttachments[position].assetUrl ??
+ widget.mediaAttachments[position].thumbUrl,
+ fit: BoxFit.cover,
+ ),
+ aspectRatio: 1.0,
+ ),
+ ),
+ );
+ }
+ },
+ itemCount: widget.mediaAttachments.length,
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 3),
+ ),
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ Widget _buildShareModal(context) {
+ showDialog(
+ context: context,
+ builder: (context) {
+ return StatefulBuilder(builder: (context, modalSetState) {
+ modalSetStateCallback = modalSetState;
+ return Padding(
+ padding: EdgeInsets.only(
+ top: _userSearchMode
+ ? 16.0
+ : MediaQuery.of(context).size.height / 2,
+ left: 8.0,
+ right: 8.0),
+ child: Material(
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(16.0),
+ topRight: Radius.circular(16.0),
+ ),
+ clipBehavior: Clip.antiAlias,
+ child: Scaffold(
+ body: UsersBloc(
+ child: Column(
+ children: [
+ _buildTextInputSection(modalSetState),
+ Expanded(
+ child: UserListView(
+ selectedUsers: _selectedUsers,
+ onUserTap: (user, _) {
+ _searchController.clear();
+ if (!_selectedUsers.contains(user)) {
+ modalSetState(() {
+ _selectedUsers.add(user);
+ });
+ } else {
+ modalSetState(() {
+ _selectedUsers.remove(user);
+ });
+ }
+ },
+ crossAxisCount: 4,
+ pagination: PaginationParams(
+ limit: 25,
+ ),
+ filter: {
+ if (_searchController.text.isNotEmpty)
+ 'name': {
+ r'$autocomplete': _userNameQuery,
+ },
+ 'id': {
+ r'$ne': StreamChat.of(context).user.id,
+ },
+ },
+ sort: [
+ SortOption(
+ 'name',
+ direction: 1,
+ ),
+ ],
+ emptyBuilder: (_) {
+ return LayoutBuilder(
+ builder: (context, viewportConstraints) {
+ return SingleChildScrollView(
+ physics: AlwaysScrollableScrollPhysics(),
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: viewportConstraints.maxHeight,
+ ),
+ child: Center(
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(24),
+ child: StreamSvgIcon.search(
+ size: 96,
+ color: Colors.grey,
+ ),
+ ),
+ Text(
+ 'No user matches these keywords...'),
+ ],
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ },
+ ),
+ ),
+ if (_selectedUsers.isNotEmpty)
+ _buildShareTextInputSection(modalSetState),
+ if (!_userSearchMode && _selectedUsers.isEmpty)
+ Align(
+ alignment: Alignment.bottomCenter,
+ child: Container(
+ color: Colors.white,
+ height: 48.0,
+ child: Material(
+ child: InkWell(
+ onTap: () async {
+ var url = widget
+ .mediaAttachments[widget.currentPage]
+ .imageUrl ??
+ widget
+ .mediaAttachments[widget.currentPage]
+ .assetUrl ??
+ widget
+ .mediaAttachments[widget.currentPage]
+ .thumbUrl;
+
+ if (widget
+ .mediaAttachments[widget.currentPage]
+ .type ==
+ 'video') {
+ await _saveVideo(url);
+ Navigator.pop(context);
+ } else {
+ await _saveImage(url);
+ Navigator.pop(context);
+ }
+ },
+ child: SizedBox.expand(
+ child: Center(
+ child: Text(
+ 'Save to Photos',
+ style: TextStyle(
+ color: StreamChatTheme.of(context)
+ .accentColor,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ });
+ },
+ );
+ }
+
+ Widget _buildTextInputSection(modalSetState) {
+ if (_userSearchMode) {
+ return Column(
+ children: [
+ SizedBox(
+ height: 16.0,
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ SizedBox(
+ width: 16.0,
+ ),
+ Expanded(
+ child: TextField(
+ controller: _searchController,
+ cursorColor: Colors.black,
+ autofocus: true,
+ decoration: InputDecoration(
+ isDense: true,
+ prefixIconConstraints:
+ BoxConstraints.tight(Size(36.0, 44.0)),
+ prefixIcon: Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 2.0, horizontal: 6.0),
+ child: StreamSvgIcon.search(
+ color: Colors.black,
+ ),
+ ),
+ hintText: 'Search',
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(32.0),
+ borderSide:
+ BorderSide(color: Colors.black.withOpacity(0.08)),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(32.0),
+ borderSide:
+ BorderSide(color: Colors.black.withOpacity(0.08)),
+ ),
+ contentPadding: EdgeInsets.zero,
+ ),
+ ),
+ ),
+ SizedBox(
+ width: 8.0,
+ ),
+ IconButton(
+ icon: StreamSvgIcon.close_small(
+ color: Colors.black.withOpacity(0.5),
+ ),
+ onPressed: () {
+ modalSetState(() {
+ _userSearchMode = false;
+ });
+ setState(() {});
+ },
+ )
+ ],
+ ),
+ ],
+ );
+ } else {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 6.0),
+ child: IconButton(
+ icon: StreamSvgIcon.search(
+ color: Colors.black,
+ ),
+ iconSize: 24.0,
+ onPressed: () {
+ modalSetState(() {
+ _userSearchMode = true;
+ });
+ },
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Text(
+ 'Select a Chat to Share',
+ style: TextStyle(
+ fontSize: 16.0,
+ fontWeight: FontWeight.w700,
+ color: Colors.black,
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 6.0),
+ child: IconButton(
+ icon: StreamSvgIcon.share_arrow(
+ color: Colors.black,
+ ),
+ onPressed: () async {
+ var url =
+ widget.mediaAttachments[widget.currentPage].imageUrl ??
+ widget.mediaAttachments[widget.currentPage].assetUrl ??
+ widget.mediaAttachments[widget.currentPage].thumbUrl;
+ var type =
+ widget.mediaAttachments[widget.currentPage].type == 'image'
+ ? 'jpg'
+ : url?.split('?')?.first?.split('.')?.last ?? 'jpg';
+ var request = await HttpClient().getUrl(Uri.parse(url));
+ var response = await request.close();
+ var bytes = await consolidateHttpClientResponseBytes(response);
+ await Share.file('File', 'image.$type', bytes, 'image/$type');
+ },
+ ),
+ ),
+ ],
+ );
+ }
+ }
+
+ Widget _buildShareTextInputSection(modalSetState) {
+ return Align(
+ alignment: Alignment.bottomCenter,
+ child: Container(
+ color: Colors.white,
+ height: 56.0,
+ child: _loading
+ ? Center(
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: CircularProgressIndicator(),
+ ),
+ )
+ : Row(
+ children: [
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: TextField(
+ controller: _messageController,
+ onChanged: (val) {
+ modalSetState(() {});
+ },
+ onTap: () {
+ modalSetState(() {});
+ setState(() {});
+ },
+ decoration: InputDecoration(
+ isDense: true,
+ prefixText: ' ',
+ hintText: 'Add a comment',
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(32.0),
+ borderSide: BorderSide(
+ color: Colors.black.withOpacity(0.16),
+ ),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(32.0),
+ borderSide: BorderSide(
+ color: Colors.black.withOpacity(0.16),
+ )),
+ contentPadding: EdgeInsets.symmetric(vertical: 12.0),
+ ),
+ ),
+ ),
+ ),
+ IconTheme(
+ data: StreamChatTheme.of(context)
+ .channelTheme
+ .messageInputButtonIconTheme,
+ child: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: InkWell(
+ onTap: () async {
+ modalSetState(() {
+ _loading = true;
+ });
+ await sendMessage();
+ modalSetState(() {
+ _loading = false;
+ });
+ },
+ child: Transform.rotate(
+ angle: -pi / 2,
+ child: StreamSvgIcon.Icon_send_message(
+ color: StreamChatTheme.of(context).accentColor,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ /// Sends the current message
+ Future sendMessage() async {
+ var text = _messageController.text.trim();
+
+ final attachments = widget.message.attachments;
+
+ _messageController.clear();
+
+ final client = StreamChat.of(context).client;
+
+ for (var user in _selectedUsers) {
+ var c = client.channel('messaging', extraData: {
+ 'members': [
+ user.id,
+ StreamChat.of(context).user.id,
+ ],
+ });
+
+ await c.watch();
+
+ final message = Message(
+ text: text,
+ attachments: [attachments[widget.currentPage]],
+ );
+
+ await c.sendMessage(message);
+ }
+
+ _selectedUsers.clear();
+ Navigator.pop(context);
+ }
+
+ Future _saveImage(String url) async {
+ var response = await Dio()
+ .get(url, options: Options(responseType: ResponseType.bytes));
+ final result = await ImageGallerySaver.saveImage(
+ Uint8List.fromList(response.data),
+ quality: 60,
+ name: "${DateTime.now().millisecondsSinceEpoch}");
+ return result;
+ }
+
+ Future _saveVideo(String url) async {
+ var appDocDir = await getTemporaryDirectory();
+ var savePath =
+ appDocDir.path + "/${DateTime.now().millisecondsSinceEpoch}.mp4";
+ await Dio().download(url, savePath);
+ final result = await ImageGallerySaver.saveFile(savePath);
+ print(result);
+ }
+}
+
+/// Used for clipping textfield prefix icon
+class IconClipper extends CustomClipper {
+ @override
+ Path getClip(Size size) {
+ var leftX = size.width / 5;
+ var rightX = 4 * size.width / 5;
+ var topY = size.height / 5;
+ var bottomY = 4 * size.height / 5;
+
+ final path = Path();
+ path.moveTo(leftX, topY);
+ path.lineTo(leftX, bottomY);
+ path.lineTo(rightX, bottomY);
+ path.lineTo(rightX, topY);
+ path.lineTo(leftX, topY);
+ path.lineTo(0.0, 0.0);
+ path.close();
+ return path;
+ }
+
+ @override
+ bool shouldReclip(CustomClipper oldClipper) {
+ return false;
+ }
+}
diff --git a/lib/src/image_group.dart b/lib/src/image_group.dart
index 1a7144d8..44c15623 100644
--- a/lib/src/image_group.dart
+++ b/lib/src/image_group.dart
@@ -3,7 +3,8 @@ import 'package:carousel_slider/carousel_options.dart';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
-import 'package:stream_chat_flutter/src/full_screen_image.dart';
+import 'package:stream_chat_flutter/src/full_screen_media.dart';
+import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ImageGroup extends StatelessWidget {
const ImageGroup({
@@ -107,37 +108,21 @@ class ImageGroup extends StatelessWidget {
BuildContext context, [
int index,
]) {
+ final channel = StreamChannel.of(context).channel;
+
Navigator.push(
context,
MaterialPageRoute(
- builder: (context) {
- return Scaffold(
- appBar: AppBar(
- backgroundColor: Colors.transparent,
- iconTheme: IconThemeData(
- color: Colors.white,
- ),
- ),
- backgroundColor: Colors.black,
- body: SizedBox.expand(
- child: CarouselSlider(
- items: images
- .map((image) => FullScreenImage(
- url: image.imageUrl ??
- image.thumbUrl ??
- image.assetUrl,
- ))
- .toList(),
- options: CarouselOptions(
- initialPage: index ?? 0,
- enableInfiniteScroll: false,
- viewportFraction: 0.95,
- height: MediaQuery.of(context).size.height,
- ),
- ),
- ),
- );
- },
+ builder: (context) => StreamChannel(
+ channel: channel,
+ child: FullScreenMedia(
+ mediaAttachments: images,
+ startIndex: index,
+ userName: message.user.name,
+ sentAt: message.createdAt,
+ message: message,
+ ),
+ ),
),
);
}
diff --git a/lib/src/image_header.dart b/lib/src/image_header.dart
new file mode 100644
index 00000000..62c3f8ec
--- /dev/null
+++ b/lib/src/image_header.dart
@@ -0,0 +1,122 @@
+import 'package:flutter/material.dart';
+import 'package:stream_chat/stream_chat.dart';
+import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
+import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
+import 'image_actions_modal.dart';
+import 'stream_channel.dart';
+
+class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
+ /// True if this header shows the leading back button
+ final bool showBackButton;
+
+ /// Callback to call when pressing the back button.
+ /// By default it calls [Navigator.pop]
+ final VoidCallback onBackPressed;
+
+ /// Callback to call when the header is tapped.
+ final VoidCallback onTitleTap;
+
+ /// Callback to call when the image is tapped.
+ final VoidCallback onImageTap;
+
+ final Message message;
+
+ final String userName;
+ final String sentAt;
+
+ final List urls;
+ final currentIndex;
+
+ /// Creates a channel header
+ ImageHeader({
+ Key key,
+ this.message,
+ this.urls,
+ this.currentIndex,
+ this.showBackButton = true,
+ this.onBackPressed,
+ this.onTitleTap,
+ this.onImageTap,
+ this.userName = '',
+ this.sentAt = '',
+ }) : preferredSize = Size.fromHeight(kToolbarHeight),
+ super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return AppBar(
+ brightness: Theme.of(context).brightness,
+ elevation: 1,
+ leading: showBackButton
+ ? IconButton(
+ icon: StreamSvgIcon.close(
+ color: Colors.black,
+ size: 24.0,
+ ),
+ onPressed: onBackPressed,
+ )
+ : SizedBox(),
+ backgroundColor:
+ StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
+ actions: [
+ IconButton(
+ icon: StreamSvgIcon.Icon_menu_point_v(
+ color: Colors.black,
+ ),
+ onPressed: () {
+ _showMessageActionModalBottomSheet(context);
+ },
+ ),
+ ],
+ centerTitle: true,
+ title: InkWell(
+ onTap: onTitleTap,
+ child: Container(
+ height: preferredSize.height,
+ width: preferredSize.width,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ userName,
+ style: TextStyle(
+ fontSize: 16.0,
+ fontWeight: FontWeight.w700,
+ color: Colors.black,
+ ),
+ ),
+ Text(
+ sentAt,
+ style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ @override
+ final Size preferredSize;
+
+ void _showMessageActionModalBottomSheet(BuildContext context) {
+ final channel = StreamChannel.of(context).channel;
+
+ showDialog(
+ context: context,
+ builder: (context) {
+ return StreamChannel(
+ channel: channel,
+ child: ImageActionsModal(
+ userName: userName,
+ sentAt: sentAt,
+ message: message,
+ urls: urls,
+ currentIndex: currentIndex,
+ ),
+ );
+ });
+ }
+}
diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart
index 1dbc45df..4e30afe1 100644
--- a/lib/src/message_input.dart
+++ b/lib/src/message_input.dart
@@ -658,6 +658,34 @@ class MessageInputState extends State {
}
Widget _buildFilePickerSection() {
+ var _attachmentContainsFile =
+ _attachments.any((element) => element?.attachment?.type == 'file');
+
+ Color _getIconColor(int index) {
+ switch (index) {
+ case 0:
+ return _attachmentContainsFile && _attachments.isNotEmpty
+ ? Colors.black.withOpacity(0.2)
+ : Colors.black.withOpacity(0.5);
+ break;
+ case 1:
+ return !_attachmentContainsFile && _attachments.isNotEmpty
+ ? Colors.black.withOpacity(0.2)
+ : Colors.black.withOpacity(0.5);
+ break;
+ case 2:
+ return _attachmentContainsFile && _attachments.isNotEmpty
+ ? Colors.black.withOpacity(0.2)
+ : Colors.black.withOpacity(0.5);
+ break;
+ case 3:
+ return _attachmentContainsFile && _attachments.isNotEmpty
+ ? Colors.black.withOpacity(0.2)
+ : Colors.black.withOpacity(0.5);
+ break;
+ }
+ }
+
return AnimatedContainer(
duration: _animateContainer ? Duration(milliseconds: 300) : Duration.zero,
height: _openFilePickerSection ? _filePickerSize : 0,
@@ -671,49 +699,49 @@ class MessageInputState extends State {
IconButton(
iconSize: 24,
icon: StreamSvgIcon.pictures(
- color: _filePickerIndex == 0
- ? StreamChatTheme.of(context).accentColor
- : Colors.black.withOpacity(0.5),
+ color: _getIconColor(0),
),
- onPressed: () {
- setState(() {
- _filePickerIndex = 0;
- });
- },
+ onPressed: _attachmentContainsFile && _attachments.isNotEmpty
+ ? null
+ : () {
+ setState(() {
+ _filePickerIndex = 0;
+ });
+ },
),
IconButton(
iconSize: 32,
icon: StreamSvgIcon.files(
- color: _filePickerIndex == 1
- ? StreamChatTheme.of(context).accentColor
- : Colors.black.withOpacity(0.5),
+ color: _getIconColor(1),
),
- onPressed: () {
- pickFile(DefaultAttachmentTypes.file, false);
- },
+ onPressed: !_attachmentContainsFile && _attachments.isNotEmpty
+ ? null
+ : () {
+ pickFile(DefaultAttachmentTypes.file, false);
+ },
),
IconButton(
iconSize: 24,
icon: StreamSvgIcon.camera(
- color: _filePickerIndex == 2
- ? StreamChatTheme.of(context).accentColor
- : Colors.black.withOpacity(0.5),
+ color: _getIconColor(2),
),
- onPressed: () {
- pickFile(DefaultAttachmentTypes.image, true);
- },
+ onPressed: _attachmentContainsFile && _attachments.isNotEmpty
+ ? null
+ : () {
+ pickFile(DefaultAttachmentTypes.image, true);
+ },
),
IconButton(
padding: const EdgeInsets.all(0),
iconSize: 24,
icon: StreamSvgIcon.record(
- color: _filePickerIndex == 3
- ? StreamChatTheme.of(context).accentColor
- : Colors.black.withOpacity(0.5),
+ color: _getIconColor(3),
),
- onPressed: () {
- pickFile(DefaultAttachmentTypes.video, true);
- },
+ onPressed: _attachmentContainsFile && _attachments.isNotEmpty
+ ? null
+ : () {
+ pickFile(DefaultAttachmentTypes.video, true);
+ },
),
],
),
@@ -770,6 +798,9 @@ class MessageInputState extends State {
}
Widget _buildPickerSection() {
+ var _attachmentContainsFile =
+ _attachments.any((element) => element.attachment.type == 'file');
+
switch (_filePickerIndex) {
case 0:
return FutureBuilder(
@@ -782,19 +813,22 @@ class MessageInputState extends State {
}
if (snapshot.data) {
- return MediaListView(
- selectedIds: _attachments.map((e) => e.id).toList(),
- onSelect: (media) async {
- if (!_attachments
- .any((element) => element.id == media.id)) {
- _addAttachment(media);
- } else {
- setState(() {
- _attachments
- .removeWhere((element) => element.id == media.id);
- });
- }
- },
+ return IgnorePointer(
+ ignoring: _attachmentContainsFile,
+ child: MediaListView(
+ selectedIds: _attachments.map((e) => e.id).toList(),
+ onSelect: (media) async {
+ if (!_attachments
+ .any((element) => element.id == media.id)) {
+ _addAttachment(media);
+ } else {
+ setState(() {
+ _attachments
+ .removeWhere((element) => element.id == media.id);
+ });
+ }
+ },
+ ),
);
}
@@ -850,7 +884,7 @@ class MessageInputState extends State {
);
if (file.size > _kMaxAttachmentSize) {
- if (medium.type == AssetType.video) {
+ if (medium?.type == AssetType.video) {
final mediaInfo = await CompressVideoService.compressVideo(file.path);
if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) {
@@ -890,7 +924,7 @@ class MessageInputState extends State {
..file = file
..attachment = Attachment(
localUri: file.path != null ? Uri.parse(file.path) : null,
- type: medium.type == AssetType.image ? 'image' : 'video',
+ type: medium?.type == AssetType.image ? 'image' : 'video',
);
});
@@ -1053,7 +1087,7 @@ class MessageInputState extends State {
offset: rejoin.length,
),
);
-
+ _debounce.cancel();
_mentionsOverlay?.remove();
_mentionsOverlay = null;
},
@@ -1201,44 +1235,98 @@ class MessageInputState extends State {
Widget _buildAttachments() {
return _attachments.isEmpty
? Container()
- : LimitedBox(
- maxHeight: 104.0,
- child: ListView(
- scrollDirection: Axis.horizontal,
- children: _attachments
- .map(
- (attachment) => Padding(
- padding: const EdgeInsets.all(8.0),
- child: ClipRRect(
- borderRadius: BorderRadius.circular(10),
- child: Stack(
- children: [
- AspectRatio(
- aspectRatio: 1.0,
- child: Container(
- height: 104,
- width: 104,
- child: _buildAttachment(attachment),
- ),
- ),
- _buildRemoveButton(attachment),
- attachment.uploaded
- ? SizedBox()
- : Positioned.fill(
- child: Center(
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: CircularProgressIndicator(),
+ : Column(
+ children: [
+ if (_attachments.any((e) => e.attachment?.type == 'file'))
+ LimitedBox(
+ maxHeight: 73.0,
+ child: ListView(
+ scrollDirection: Axis.horizontal,
+ children: _attachments
+ .where((e) => e.attachment?.type == 'file')
+ .map(
+ (e) => Padding(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 8.0),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(10),
+ clipBehavior: Clip.antiAlias,
+ child: FileAttachment(
+ attachment: e.attachment,
+ size: Size(
+ MediaQuery.of(context).size.width * 0.55,
+ MediaQuery.of(context).size.height * 0.3,
+ ),
+ trailing: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: InkWell(
+ child: CircleAvatar(
+ backgroundColor:
+ Colors.black.withOpacity(0.2),
+ maxRadius: 12.0,
+ child: StreamSvgIcon.close(
+ color: Colors.white,
),
),
+ onTap: () {
+ setState(() {
+ _attachments.remove(e);
+ });
+ },
),
- ],
- ),
- ),
- ),
- )
- .toList(),
- ),
+ ),
+ ),
+ ),
+ ),
+ )
+ .toList(),
+ ),
+ ),
+ if (_attachments.any((e) => e.attachment?.type != 'file'))
+ LimitedBox(
+ maxHeight: 104.0,
+ child: ListView(
+ scrollDirection: Axis.horizontal,
+ children: _attachments
+ .where((e) => e.attachment?.type != 'file')
+ .map(
+ (attachment) => Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(10),
+ clipBehavior: Clip.antiAlias,
+ child: Stack(
+ children: [
+ AspectRatio(
+ aspectRatio: 1.0,
+ child: Container(
+ height: 104,
+ width: 104,
+ child: _buildAttachment(attachment),
+ ),
+ ),
+ _buildRemoveButton(attachment),
+ attachment.uploaded
+ ? SizedBox()
+ : Positioned.fill(
+ child: Center(
+ child: Padding(
+ padding:
+ const EdgeInsets.all(16.0),
+ child:
+ CircularProgressIndicator(),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ )
+ .toList(),
+ ),
+ ),
+ ],
);
}
@@ -1275,9 +1363,9 @@ class MessageInputState extends State {
Widget _buildAttachment(_SendingAttachment attachment) {
if (widget.attachmentThumbnailBuilders
- ?.containsKey(attachment.attachment.type) ==
+ ?.containsKey(attachment.attachment?.type) ==
true) {
- return widget.attachmentThumbnailBuilders[attachment.attachment.type](
+ return widget.attachmentThumbnailBuilders[attachment.attachment?.type](
context,
attachment,
);
@@ -1287,7 +1375,7 @@ class MessageInputState extends State {
return SizedBox();
}
- switch (attachment.attachment.type) {
+ switch (attachment.attachment?.type) {
case 'image':
case 'giphy':
return attachment.file != null
@@ -1562,12 +1650,24 @@ class MessageInputState extends State {
attachmentType = mimeType.type;
}
+ Map extraDataMap = {};
+
+ if (mimeType?.subtype != null) {
+ extraDataMap['mime_type'] = mimeType.subtype.toLowerCase();
+ }
+
+ if (file.size != null) {
+ extraDataMap['file_size'] = file.size;
+ }
+
final channel = StreamChannel.of(context).channel;
final attachment = _SendingAttachment(
file: file,
attachment: Attachment(
localUri: file.path != null ? Uri.parse(file.path) : null,
type: attachmentType,
+ extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
+ title: file.name ?? 'File',
),
);
diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart
index 8a4ac9c3..b4e8de74 100644
--- a/lib/src/message_list_view.dart
+++ b/lib/src/message_list_view.dart
@@ -617,7 +617,12 @@ class _MessageListViewState extends State {
showDeleteMessage: isMyMessage,
borderSide: isMyMessage ? BorderSide.none : null,
onThreadTap: _onThreadTap,
- attachmentBorderRadiusGeometry: BorderRadius.circular(16),
+ attachmentBorderRadiusGeometry: BorderRadius.only(
+ topLeft: Radius.circular(16),
+ bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
+ topRight: Radius.circular(16),
+ bottomRight: Radius.circular(16),
+ ),
attachmentPadding: const EdgeInsets.all(2),
borderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16),
diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart
index 88a93d6f..f761f756 100644
--- a/lib/src/message_widget.dart
+++ b/lib/src/message_widget.dart
@@ -180,14 +180,13 @@ class MessageWidget extends StatefulWidget {
MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3,
),
+ message: message,
);
},
'giphy': (context, message, attachment) {
return GiphyAttachment(
attachment: attachment,
- messageTheme: messageTheme.copyWith(
- messageBackgroundColor: Colors.white,
- ),
+ messageTheme: messageTheme,
message: message,
size: Size(
MediaQuery.of(context).size.width * 0.8,
@@ -218,8 +217,8 @@ class _MessageWidgetState extends State {
? widget.messageTheme.avatarTheme.constraints.maxWidth + 16.0
: 6.0;
- final isGiphy =
- widget.message.attachments?.any((element) => element.type == 'giphy') ==
+ final hasFiles =
+ widget.message.attachments?.any((element) => element.type == 'file') ==
true;
return Portal(
@@ -294,18 +293,46 @@ class _MessageWidgetState extends State {
messageTheme: widget.messageTheme,
),
)
- : Column(
- crossAxisAlignment:
- CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- ..._parseAttachments(context),
- if (widget.message.text
- .trim()
- .isNotEmpty &&
- !isGiphy)
- _buildTextBubble(context),
- ],
+ : Material(
+ clipBehavior: Clip.antiAlias,
+ shape: widget.shape ??
+ RoundedRectangleBorder(
+ side: isOnlyEmoji
+ ? BorderSide.none
+ : widget.borderSide ??
+ BorderSide(
+ color: Theme.of(context)
+ .brightness ==
+ Brightness
+ .dark
+ ? Colors.white
+ .withAlpha(24)
+ : Colors.black
+ .withAlpha(
+ 24),
+ ),
+ borderRadius: widget
+ .borderRadiusGeometry ??
+ BorderRadius.zero,
+ ),
+ color: _getBackgroundColor(),
+ child: Padding(
+ padding: EdgeInsets.all(
+ hasFiles ? 2.0 : 0.0),
+ child: Column(
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ..._parseAttachments(context),
+ if (widget.message.text
+ .trim()
+ .isNotEmpty &&
+ !isGiphy)
+ _buildTextBubble(context),
+ ],
+ ),
+ ),
),
),
if (widget.showReactionPickerIndicator)
@@ -448,6 +475,10 @@ class _MessageWidgetState extends State {
);
}
+ bool get isGiphy =>
+ widget.message.attachments?.any((element) => element.type == 'giphy') ==
+ true;
+
Widget _buildReadIndicator() {
var padding = 0.0;
return Stack(
@@ -628,46 +659,39 @@ class _MessageWidgetState extends State {
[];
}
- Padding wrapAttachmentWidget(
+ Widget wrapAttachmentWidget(
BuildContext context,
Widget attachmentWidget, {
Attachment attachment,
}) {
final attachmentShape =
widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context);
- return Padding(
- padding: EdgeInsets.only(
- bottom: 4,
- ),
- child: GestureDetector(
- onTap: () => retryMessage(context),
- onLongPress: () => onLongPress(context),
- child: Material(
- color: attachment?.type == 'giphy'
- ? Colors.white
- : _getBackgroundColor(),
- clipBehavior: Clip.hardEdge,
- shape: attachmentShape,
- child: Padding(
- padding: widget.attachmentPadding,
- child: Material(
- clipBehavior: Clip.hardEdge,
- shape: attachmentShape,
- type: MaterialType.transparency,
- child: Transform(
- transform: Matrix4.rotationY(widget.reverse ? pi : 0),
- alignment: Alignment.center,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- getFailedMessageWidget(
- context,
- padding: const EdgeInsets.all(8.0),
- ),
- attachmentWidget,
- ],
- ),
+ return GestureDetector(
+ onTap: () => retryMessage(context),
+ onLongPress: () => onLongPress(context),
+ child: Material(
+ color: _getBackgroundColor(),
+ clipBehavior: Clip.antiAlias,
+ shape: attachmentShape,
+ child: Padding(
+ padding: widget.attachmentPadding,
+ child: Material(
+ clipBehavior: Clip.hardEdge,
+ shape: attachmentShape,
+ type: MaterialType.transparency,
+ child: Transform(
+ transform: Matrix4.rotationY(widget.reverse ? pi : 0),
+ alignment: Alignment.center,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ getFailedMessageWidget(
+ context,
+ padding: const EdgeInsets.all(8.0),
+ ),
+ attachmentWidget,
+ ],
),
),
),
@@ -797,30 +821,7 @@ class _MessageWidgetState extends State {
return SizedBox();
}
- Widget _wrapTextInBubble({
- BuildContext context,
- Widget child,
- }) {
- return Material(
- shape: widget.shape ??
- RoundedRectangleBorder(
- side: widget.borderSide ??
- BorderSide(
- color: Theme.of(context).brightness == Brightness.dark
- ? Colors.white.withAlpha(24)
- : Colors.black.withAlpha(24),
- ),
- borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
- ),
- color: _getBackgroundColor(),
- child: child,
- );
- }
-
Widget _buildTextBubble(BuildContext context) {
- final isOnlyEmoji =
- widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
-
Widget child = Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
@@ -857,13 +858,6 @@ class _MessageWidgetState extends State {
],
),
);
-
- if (!isOnlyEmoji) {
- child = _wrapTextInBubble(
- context: context,
- child: child,
- );
- }
return GestureDetector(
onTap: () => retryMessage(context),
onLongPress: () => onLongPress(context),
@@ -871,6 +865,10 @@ class _MessageWidgetState extends State {
);
}
+ bool get isOnlyEmoji =>
+ widget.message.text.characters.isNotEmpty &&
+ widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
+
Color _getBackgroundColor() {
if ((widget.message.status == MessageSendingStatus.FAILED ||
widget.message.status == MessageSendingStatus.FAILED_UPDATE ||
@@ -884,6 +882,14 @@ class _MessageWidgetState extends State {
return Color(0xFFE9F2FF);
}
+ if (isOnlyEmoji) {
+ return Colors.transparent;
+ }
+
+ if (isGiphy) {
+ return Colors.transparent;
+ }
+
return widget.messageTheme.messageBackgroundColor;
}
diff --git a/lib/src/stream_svg_icon.dart b/lib/src/stream_svg_icon.dart
index 9a87ce9d..42fed46e 100644
--- a/lib/src/stream_svg_icon.dart
+++ b/lib/src/stream_svg_icon.dart
@@ -338,6 +338,30 @@ class StreamSvgIcon extends StatelessWidget {
);
}
+ factory StreamSvgIcon.download({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'Icon_download.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.cloud_download({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'Icon_cloud_download.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
factory StreamSvgIcon.copy({
double size,
Color color,
@@ -397,4 +421,304 @@ class StreamSvgIcon extends StatelessWidget {
height: size,
);
}
+
+ factory StreamSvgIcon.Icon_curve_line_left_up({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'Icon_curve_line_left_up.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.icon_SHARE({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'icon_SHARE.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.Icon_grid({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'Icon_grid.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.Icon_send_message({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'Icon_send_message.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.Icon_menu_point_v({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'Icon_menu_point_v.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.Icon_save({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'Icon_save.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.share_arrow({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'share_arrow.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_7z({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_7z.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_csv({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_CSV.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_doc({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_DOC.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_docx({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_DOCX.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_Generic({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_Generic.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_html({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_html.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_md({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_MD.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_odt({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_ODT.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_pdf({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_PDF.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_ppt({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_PPT.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_pptx({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_PPTX.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_rar({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_RAR.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_rtf({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_RTF.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_tar({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_TAR.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_txt({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_TXT.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_xls({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_XLS.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_xlsx({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_XLSX.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
+
+ factory StreamSvgIcon.filetype_zip({
+ double size,
+ Color color,
+ }) {
+ return StreamSvgIcon(
+ assetName: 'filetype_ZIP.svg',
+ color: color,
+ width: size,
+ height: size,
+ );
+ }
}
diff --git a/lib/src/video_attachment.dart b/lib/src/video_attachment.dart
index b0d1f60d..21ede87a 100644
--- a/lib/src/video_attachment.dart
+++ b/lib/src/video_attachment.dart
@@ -1,7 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
-import 'package:stream_chat_flutter/src/full_screen_video.dart';
+import 'package:stream_chat_flutter/src/full_screen_media.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
@@ -13,11 +13,13 @@ class VideoAttachment extends StatefulWidget {
final Attachment attachment;
final MessageTheme messageTheme;
final Size size;
+ final Message message;
VideoAttachment({
Key key,
@required this.attachment,
@required this.messageTheme,
+ this.message,
this.size,
}) : super(key: key);
@@ -81,11 +83,19 @@ class _VideoAttachmentState extends State {
return GestureDetector(
onTap: () {
+ final channel = StreamChannel.of(context).channel;
+
Navigator.push(
context,
MaterialPageRoute(
- builder: (_) => FullScreenVideo(
- attachment: widget.attachment,
+ builder: (_) => StreamChannel(
+ channel: channel,
+ child: FullScreenMedia(
+ mediaAttachments: [widget.attachment],
+ userName: widget.message.user.name,
+ sentAt: widget.message.createdAt,
+ message: widget.message,
+ ),
),
),
);
@@ -100,7 +110,7 @@ class _VideoAttachmentState extends State {
children: [
Expanded(
child: FittedBox(
- fit: BoxFit.cover,
+ fit: BoxFit.none,
child: Stack(
children: [
Chewie(
diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart
index 1cbe21e4..5dd46fa3 100644
--- a/lib/stream_chat_flutter.dart
+++ b/lib/stream_chat_flutter.dart
@@ -11,7 +11,9 @@ export 'src/channels_bloc.dart';
export 'src/date_divider.dart';
export 'src/deleted_message.dart';
export 'src/file_attachment.dart';
-export 'src/full_screen_video.dart';
+export 'src/full_screen_media.dart';
+export 'src/image_header.dart';
+export 'src/image_footer.dart';
export 'src/giphy_attachment.dart';
export 'src/image_attachment.dart';
export 'src/message_input.dart';
diff --git a/lib/svgs/Icon_menu_point_v.svg b/lib/svgs/Icon_menu_point_v.svg
new file mode 100644
index 00000000..f7b61163
--- /dev/null
+++ b/lib/svgs/Icon_menu_point_v.svg
@@ -0,0 +1,5 @@
+
diff --git a/lib/svgs/Icon_save.svg b/lib/svgs/Icon_save.svg
new file mode 100644
index 00000000..14bedc6f
--- /dev/null
+++ b/lib/svgs/Icon_save.svg
@@ -0,0 +1,4 @@
+
diff --git a/lib/svgs/Icon_send_message.svg b/lib/svgs/Icon_send_message.svg
new file mode 100644
index 00000000..0d504a40
--- /dev/null
+++ b/lib/svgs/Icon_send_message.svg
@@ -0,0 +1,3 @@
+
diff --git a/pubspec.yaml b/pubspec.yaml
index 75195a30..b41f58c6 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -38,6 +38,8 @@ dependencies:
flutter_slidable: ^0.5.4
carousel_slider: ^2.2.1
clipboard: ^0.1.2+8
+ image_gallery_saver: ^1.6.6
+ esys_flutter_share: ^1.0.2
photo_manager: ^0.5.8
transparent_image: ^1.0.0
ezanimation: ^0.4.1