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. -->
-
-
+
+
onTap: () {
onTap(channel, widget.channelWidget);
},
+ onLongPress: widget.onChannelLongPress != null
+ ? () {
+ widget.onChannelLongPress(channel);
+ }
+ : null,
),
),
),
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..6ab38c83 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,10 +64,18 @@ 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,
+ ),
);
}));
},
@@ -278,10 +286,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..7c39fb4c
--- /dev/null
+++ b/lib/src/image_footer.dart
@@ -0,0 +1,682 @@
+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(
+ 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_widget.dart b/lib/src/message_widget.dart
index 88a93d6f..64dab4ac 100644
--- a/lib/src/message_widget.dart
+++ b/lib/src/message_widget.dart
@@ -180,6 +180,7 @@ class MessageWidget extends StatefulWidget {
MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3,
),
+ message: message,
);
},
'giphy': (context, message, attachment) {
diff --git a/lib/src/stream_svg_icon.dart b/lib/src/stream_svg_icon.dart
index 9a87ce9d..e5ef7ec9 100644
--- a/lib/src/stream_svg_icon.dart
+++ b/lib/src/stream_svg_icon.dart
@@ -397,4 +397,88 @@ 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,
+ );
+ }
}
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 1d2a299e..a226f9c3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -41,6 +41,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