diff --git a/packages/stream_chat_v1/lib/chat_info_screen.dart b/packages/stream_chat_v1/lib/chat_info_screen.dart index 2ba2839..eca16bb 100644 --- a/packages/stream_chat_v1/lib/chat_info_screen.dart +++ b/packages/stream_chat_v1/lib/chat_info_screen.dart @@ -6,6 +6,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'channel_file_display_screen.dart'; import 'channel_media_display_screen.dart'; +import 'pinned_messages_screen.dart'; import 'main.dart'; import 'routes/routes.dart'; @@ -199,6 +200,65 @@ class _ChatInfoScreenState extends State { // ), // onTap: () {}, // ), + OptionListTile( + title: 'Pinned Messages', + tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 22.0), + child: StreamSvgIcon.pin( + size: 24.0, + color: + StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + ), + ), + trailing: StreamSvgIcon.right( + color: StreamChatTheme.of(context).colorTheme.grey, + ), + onTap: () { + final channel = StreamChannel.of(context).channel; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: MessageSearchBloc( + child: PinnedMessagesScreen( + messageTheme: widget.messageTheme, + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + onShowMessage: (m, c) async { + final client = StreamChat.of(context).client; + final message = m; + final channel = client.channel( + c.type, + id: c.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + ), + ), + ), + ), + ); + }, + ), OptionListTile( title: 'Photos & Videos', tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, diff --git a/packages/stream_chat_v1/lib/group_info_screen.dart b/packages/stream_chat_v1/lib/group_info_screen.dart index 3e964b2..6c45757 100644 --- a/packages/stream_chat_v1/lib/group_info_screen.dart +++ b/packages/stream_chat_v1/lib/group_info_screen.dart @@ -10,6 +10,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'channel_file_display_screen.dart'; import 'channel_media_display_screen.dart'; +import 'pinned_messages_screen.dart'; import 'chat_info_screen.dart'; import 'main.dart'; import 'routes/routes.dart'; @@ -482,6 +483,65 @@ class _GroupInfoScreenState extends State { onTap: () {}, ); }), + OptionListTile( + title: 'Pinned Messages', + tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: StreamSvgIcon.pin( + size: 24.0, + color: + StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + ), + ), + trailing: StreamSvgIcon.right( + color: StreamChatTheme.of(context).colorTheme.grey, + ), + onTap: () { + final channel = StreamChannel.of(context).channel; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: MessageSearchBloc( + child: PinnedMessagesScreen( + messageTheme: widget.messageTheme, + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + onShowMessage: (m, c) async { + final client = StreamChat.of(context).client; + final message = m; + final channel = client.channel( + c.type, + id: c.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + ), + ), + ), + ), + ); + }, + ), OptionListTile( tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, separatorColor: StreamChatTheme.of(context).colorTheme.greyGainsboro, diff --git a/packages/stream_chat_v1/lib/main.dart b/packages/stream_chat_v1/lib/main.dart index ecd9957..c4d16b1 100644 --- a/packages/stream_chat_v1/lib/main.dart +++ b/packages/stream_chat_v1/lib/main.dart @@ -898,6 +898,7 @@ class _ChannelPageState extends State { ), ); }, + pinPermissions: ['owner', 'admin', 'member'], ), Positioned( bottom: 0, @@ -990,6 +991,7 @@ class _ThreadPageState extends State { initialAlignment: widget.initialAlignment, onMessageSwiped: _reply, onReplyTap: _reply, + pinPermissions: ['owner', 'admin', 'member'], ), ), if (widget.parent!.type != 'deleted') diff --git a/packages/stream_chat_v1/lib/pinned_messages_screen.dart b/packages/stream_chat_v1/lib/pinned_messages_screen.dart new file mode 100644 index 0000000..82dd532 --- /dev/null +++ b/packages/stream_chat_v1/lib/pinned_messages_screen.dart @@ -0,0 +1,224 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:video_player/video_player.dart'; + +class PinnedMessagesScreen 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; + + final MessageTheme messageTheme; + + const PinnedMessagesScreen({ + required this.messageTheme, + this.sortOptions, + this.paginationParams, + this.emptyBuilder, + this.onShowMessage, + }); + + @override + _PinnedMessagesScreenState createState() => _PinnedMessagesScreenState(); +} + +class _PinnedMessagesScreenState extends State { + Map controllerCache = {}; + + @override + void initState() { + super.initState(); + final messageSearchBloc = MessageSearchBloc.of(context); + messageSearchBloc.search( + filter: Filter.in_( + 'cid', + [StreamChannel.of(context).channel.cid!], + ), + messageFilter: Filter.equal( + 'pinned', + true, + ), + 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( + 'Pinned Messages', + 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.pin( + size: 136.0, + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + ), + SizedBox(height: 16.0), + Text( + 'No pinned items', + style: TextStyle( + fontSize: 17.0, + color: StreamChatTheme.of(context).colorTheme.black, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8.0), + RichText( + textAlign: TextAlign.center, + text: TextSpan(children: [ + TextSpan( + text: 'Long-press an important message and\nchoose ', + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5), + ), + ), + TextSpan( + text: 'Pin to conversation', + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.bold, + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5), + ), + ), + ]), + ), + ], + ), + ); + } + + var data = snapshot.data ?? []; + + return LazyLoadScrollView( + onEndOfPage: () => messageSearchBloc.search( + filter: Filter.in_( + 'cid', + [StreamChannel.of(context).channel.cid!], + ), + messageFilter: Filter.equal( + 'pinned', + true, + ), + sort: widget.sortOptions, + pagination: widget.paginationParams!.copyWith( + offset: messageSearchBloc.messageResponses?.length ?? 0, + ), + ), + child: ListView.builder( + itemBuilder: (context, position) { + var user = data[position].message.user!; + var attachments = data[position].message.attachments; + var text = data[position].message.text ?? ''; + + return ListTile( + leading: UserAvatar( + user: user, + constraints: BoxConstraints( + maxWidth: 40.0, + minHeight: 40.0, + ), + borderRadius: BorderRadius.circular(28), + ), + title: Text( + user.name, + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.black, + fontWeight: FontWeight.bold), + ), + subtitle: Text( + text != '' + ? text + : (attachments.isNotEmpty + ? '${attachments.length} attachment${attachments.length > 1 ? 's' : ''}' + : ''), + ), + onTap: () { + widget.onShowMessage?.call(data[position].message, + StreamChannel.of(context).channel); + }, + ); + }, + itemCount: snapshot.data!.length, + ), + ); + }, + stream: messageSearchBloc.messagesStream, + ); + } + + @override + void dispose() { + super.dispose(); + for (var c in controllerCache.values) { + c!.dispose(); + } + } +}