Merge branch 'feature/new-ui' of github.com:GetStream/stream-chat-flutter into feat/two-way-pagination
This commit is contained in:
@@ -481,6 +481,11 @@ class _ChannelListViewState extends State<ChannelListView>
|
||||
onTap: () {
|
||||
onTap(channel, widget.channelWidget);
|
||||
},
|
||||
onLongPress: widget.onChannelLongPress != null
|
||||
? () {
|
||||
widget.onChannelLongPress(channel);
|
||||
}
|
||||
: 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Attachment> 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<FullScreenMedia>
|
||||
with SingleTickerProviderStateMixin {
|
||||
bool _optionsShown = true;
|
||||
|
||||
AnimationController _controller;
|
||||
PageController _pageController;
|
||||
|
||||
int _currentPage;
|
||||
|
||||
List<VideoPackage> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<FullScreenVideo> {
|
||||
ChewieController _chewieController;
|
||||
VideoPlayerController _videoPlayerController;
|
||||
bool initialized = false;
|
||||
final GlobalKey<ScaffoldState> _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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -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<Attachment> 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<void> _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<void> _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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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<Attachment> mediaAttachments;
|
||||
final Message message;
|
||||
|
||||
final List<VideoPackage> videoPackages;
|
||||
final ValueChanged<int> 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<ImageFooter> {
|
||||
bool _userSearchMode = false;
|
||||
TextEditingController _searchController;
|
||||
TextEditingController _messageController = TextEditingController();
|
||||
|
||||
String _userNameQuery;
|
||||
bool _isSearchActive = false;
|
||||
|
||||
Set<User> _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: <Widget>[
|
||||
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<void> _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<void> _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<Path> {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
+14
-29
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<Attachment> 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: <Widget>[
|
||||
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: <Widget>[
|
||||
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,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<VideoAttachment> {
|
||||
|
||||
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<VideoAttachment> {
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
fit: BoxFit.none,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Chewie(
|
||||
|
||||
Reference in New Issue
Block a user