diff --git a/packages/stream_chat_v1/lib/channel_file_display_screen.dart b/packages/stream_chat_v1/lib/channel_file_display_screen.dart new file mode 100644 index 0000000..2779ba3 --- /dev/null +++ b/packages/stream_chat_v1/lib/channel_file_display_screen.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class ChannelFileDisplayScreen extends StatefulWidget { + /// The sorting used for the channels matching the filters. + /// Sorting is based on field and direction, multiple sorting options can be provided. + /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. + /// Direction can be ascending or descending. + final List sortOptions; + + /// Pagination parameters + /// limit: the number of users to return (max is 30) + /// offset: the offset (max is 1000) + /// message_limit: how many messages should be included to each channel + final PaginationParams paginationParams; + + /// The builder used when the file list is empty. + final WidgetBuilder emptyBuilder; + + const ChannelFileDisplayScreen({ + this.sortOptions, + this.paginationParams, + this.emptyBuilder, + }); + + @override + _ChannelFileDisplayScreenState createState() => + _ChannelFileDisplayScreenState(); +} + +class _ChannelFileDisplayScreenState extends State { + @override + void initState() { + super.initState(); + final messageSearchBloc = MessageSearchBloc.of(context); + messageSearchBloc.search( + filter: { + 'cid': { + r'$in': [StreamChannel.of(context).channel.cid] + } + }, + messageFilter: { + 'attachments.type': { + r'$in': ['file'], + }, + }, + sort: widget.sortOptions, + pagination: widget.paginationParams, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.white, + appBar: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + centerTitle: true, + title: Text( + 'Files', + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.black, + fontSize: 16.0), + ), + leading: Center( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + width: 24.0, + height: 24.0, + child: StreamSvgIcon.left( + color: StreamChatTheme.of(context).colorTheme.black, + size: 24.0, + ), + ), + ), + ), + backgroundColor: StreamChatTheme.of(context).colorTheme.white, + ), + body: _buildMediaGrid(), + ); + } + + Widget _buildMediaGrid() { + final messageSearchBloc = MessageSearchBloc.of(context); + + return StreamBuilder>( + builder: (context, snapshot) { + if (snapshot.data == null) { + return Center( + child: const CircularProgressIndicator(), + ); + } + + if (snapshot.data.isEmpty) { + if (widget.emptyBuilder != null) { + return widget.emptyBuilder(context); + } + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.files( + size: 136.0, + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + ), + SizedBox(height: 16.0), + Text( + 'No Files', + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context).colorTheme.black, + ), + ), + SizedBox(height: 8.0), + Text( + 'Files sent in this chat will appear here', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5), + ), + ), + ], + ), + ); + } + + final media = {}; + + for (var item in snapshot.data) { + item.message.attachments.where((e) => e.type == 'file').forEach((e) { + media[e] = item.message; + }); + } + + return LazyLoadScrollView( + onEndOfPage: () => messageSearchBloc.loadMore( + filter: { + 'cid': { + r'$in': [StreamChannel.of(context).channel.cid] + } + }, + messageFilter: { + 'attachments.type': { + r'$in': ['file'] + }, + }, + sort: widget.sortOptions, + pagination: widget.paginationParams.copyWith( + offset: messageSearchBloc.messageResponses?.length ?? 0, + ), + ), + child: ListView.builder( + itemBuilder: (context, position) { + return Padding( + padding: const EdgeInsets.all(1.0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: FileAttachment( + message: media.values.toList()[position], + attachment: media.keys.toList()[position], + ), + ), + ); + }, + itemCount: media.length, + ), + ); + }, + stream: messageSearchBloc.messagesStream, + ); + } +} diff --git a/packages/stream_chat_v1/lib/channel_media_display_screen.dart b/packages/stream_chat_v1/lib/channel_media_display_screen.dart new file mode 100644 index 0000000..e09b122 --- /dev/null +++ b/packages/stream_chat_v1/lib/channel_media_display_screen.dart @@ -0,0 +1,249 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:video_player/video_player.dart'; + +class ChannelMediaDisplayScreen extends StatefulWidget { + /// The sorting used for the channels matching the filters. + /// Sorting is based on field and direction, multiple sorting options can be provided. + /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. + /// Direction can be ascending or descending. + final List sortOptions; + + /// Pagination parameters + /// limit: the number of users to return (max is 30) + /// offset: the offset (max is 1000) + /// message_limit: how many messages should be included to each channel + final PaginationParams paginationParams; + + /// The builder used when the file list is empty. + final WidgetBuilder emptyBuilder; + + final ShowMessageCallback onShowMessage; + + const ChannelMediaDisplayScreen({ + this.sortOptions, + this.paginationParams, + this.emptyBuilder, + this.onShowMessage, + }); + + @override + _ChannelMediaDisplayScreenState createState() => + _ChannelMediaDisplayScreenState(); +} + +class _ChannelMediaDisplayScreenState extends State { + Map controllerCache = {}; + + @override + void initState() { + super.initState(); + final messageSearchBloc = MessageSearchBloc.of(context); + messageSearchBloc.search( + filter: { + 'cid': { + r'$in': [StreamChannel.of(context).channel.cid], + } + }, + messageFilter: { + 'attachments.type': { + r'$in': ['image', 'video'] + }, + }, + sort: widget.sortOptions, + pagination: widget.paginationParams, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.white, + appBar: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + centerTitle: true, + title: Text( + 'Photos & Videos', + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.black, + fontSize: 16.0, + ), + ), + leading: Center( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + width: 24.0, + height: 24.0, + child: StreamSvgIcon.left( + color: StreamChatTheme.of(context).colorTheme.black, + size: 24.0, + ), + ), + ), + ), + backgroundColor: StreamChatTheme.of(context).colorTheme.white, + ), + body: _buildMediaGrid(), + ); + } + + Widget _buildMediaGrid() { + final messageSearchBloc = MessageSearchBloc.of(context); + + return StreamBuilder>( + builder: (context, snapshot) { + if (snapshot.data == null) { + return Center( + child: const CircularProgressIndicator(), + ); + } + + if (snapshot.data.isEmpty) { + if (widget.emptyBuilder != null) { + return widget.emptyBuilder(context); + } + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.pictures( + size: 136.0, + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + ), + SizedBox(height: 16.0), + Text( + 'No Media', + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context).colorTheme.black, + ), + ), + SizedBox(height: 8.0), + Text( + 'Photos or video sent in this chat will \nappear here', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5), + ), + ), + ], + ), + ); + } + + final media = <_AssetPackage>[]; + + for (var item in snapshot.data) { + item.message.attachments + .where((e) => + (e.type == 'image' || e.type == 'video') && + e.ogScrapeUrl == null) + .forEach((e) { + VideoPlayerController controller; + if (e.type == 'video') { + var cachedController = controllerCache[e.assetUrl]; + + if (cachedController == null) { + controller = VideoPlayerController.network(e.assetUrl); + controller.initialize(); + controllerCache[e.assetUrl] = controller; + } else { + controller = cachedController; + } + } + media.add(_AssetPackage(e, item.message, controller)); + }); + } + + return LazyLoadScrollView( + onEndOfPage: () => messageSearchBloc.loadMore( + filter: { + 'cid': { + r'$in': [StreamChannel.of(context).channel.cid] + } + }, + messageFilter: { + 'attachments.type': { + r'$in': ['image', 'video'] + }, + }, + sort: widget.sortOptions, + pagination: widget.paginationParams.copyWith( + offset: messageSearchBloc.messageResponses?.length ?? 0, + ), + ), + child: GridView.builder( + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), + itemBuilder: (context, position) { + var channel = StreamChannel.of(context).channel; + return Padding( + padding: const EdgeInsets.all(1.0), + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: FullScreenMedia( + mediaAttachments: + media.map((e) => e.attachment).toList(), + startIndex: position, + message: media[position].message, + sentAt: media[position].message.createdAt, + userName: media[position].message.user.name, + onShowMessage: widget.onShowMessage, + ), + ), + ), + ); + }, + child: media[position].attachment.type == 'image' + ? IgnorePointer( + child: ImageAttachment( + attachment: media[position].attachment, + message: media[position].message, + showTitle: false, + size: Size( + MediaQuery.of(context).size.width * 0.8, + MediaQuery.of(context).size.height * 0.3, + ), + ), + ) + : VideoPlayer(media[position].videoPlayer), + ), + ); + }, + itemCount: media.length, + ), + ); + }, + stream: messageSearchBloc.messagesStream, + ); + } + + @override + void dispose() { + super.dispose(); + for (var c in controllerCache.values) { + c.dispose(); + } + } +} + +class _AssetPackage { + Attachment attachment; + Message message; + VideoPlayerController videoPlayer; + + _AssetPackage(this.attachment, this.message, this.videoPlayer); +} diff --git a/packages/stream_chat_v1/lib/chat_info_screen.dart b/packages/stream_chat_v1/lib/chat_info_screen.dart index 5c8441e..1668132 100644 --- a/packages/stream_chat_v1/lib/chat_info_screen.dart +++ b/packages/stream_chat_v1/lib/chat_info_screen.dart @@ -1,3 +1,5 @@ +import 'package:example/channel_file_display_screen.dart'; +import 'package:example/channel_media_display_screen.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; diff --git a/packages/stream_chat_v1/lib/group_info_screen.dart b/packages/stream_chat_v1/lib/group_info_screen.dart index b76cc9b..cfcdea6 100644 --- a/packages/stream_chat_v1/lib/group_info_screen.dart +++ b/packages/stream_chat_v1/lib/group_info_screen.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:example/channel_file_display_screen.dart'; +import 'package:example/channel_media_display_screen.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart';