Merge pull request #74 from GetStream/release/4.0

Release/4.0
This commit is contained in:
Salvatore Giordano
2022-05-03 09:02:14 +02:00
committed by GitHub
23 changed files with 1037 additions and 1136 deletions
@@ -1,9 +1,6 @@
import 'package:collection/collection.dart' show IterableExtension; import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/*Navigator.push( /*Navigator.push(
context, context,
-1
View File
@@ -1,5 +1,4 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:imessage/message_list_view.dart'; import 'package:imessage/message_list_view.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart' import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'
show show
@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 46; objectVersion = 50;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -177,7 +177,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 1140; LastSwiftUpdateCheck = 1140;
LastUpgradeCheck = 1020; LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "The Chromium Authors"; ORGANIZATIONNAME = "The Chromium Authors";
TargetAttributes = { TargetAttributes = {
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {
@@ -245,7 +245,7 @@
"${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework", "${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework",
"${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework", "${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework",
"${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework", "${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework",
"${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework", "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework",
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
@@ -256,16 +256,16 @@
"${BUILT_PRODUCTS_DIR}/image_gallery_saver/image_gallery_saver.framework", "${BUILT_PRODUCTS_DIR}/image_gallery_saver/image_gallery_saver.framework",
"${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework",
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
"${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", "${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework",
"${BUILT_PRODUCTS_DIR}/photo_manager/photo_manager.framework", "${BUILT_PRODUCTS_DIR}/photo_manager/photo_manager.framework",
"${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework", "${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework",
"${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework", "${BUILT_PRODUCTS_DIR}/shared_preferences_ios/shared_preferences_ios.framework",
"${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework",
"${BUILT_PRODUCTS_DIR}/sqlite3/sqlite3.framework", "${BUILT_PRODUCTS_DIR}/sqlite3/sqlite3.framework",
"${BUILT_PRODUCTS_DIR}/sqlite3_flutter_libs/sqlite3_flutter_libs.framework", "${BUILT_PRODUCTS_DIR}/sqlite3_flutter_libs/sqlite3_flutter_libs.framework",
"${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework", "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework",
"${BUILT_PRODUCTS_DIR}/video_compress/video_compress.framework", "${BUILT_PRODUCTS_DIR}/video_compress/video_compress.framework",
"${BUILT_PRODUCTS_DIR}/video_player/video_player.framework", "${BUILT_PRODUCTS_DIR}/video_player_avfoundation/video_player_avfoundation.framework",
"${BUILT_PRODUCTS_DIR}/video_thumbnail/video_thumbnail.framework", "${BUILT_PRODUCTS_DIR}/video_thumbnail/video_thumbnail.framework",
"${BUILT_PRODUCTS_DIR}/wakelock/wakelock.framework", "${BUILT_PRODUCTS_DIR}/wakelock/wakelock.framework",
); );
@@ -285,16 +285,16 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_gallery_saver.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_gallery_saver.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_manager.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_manager.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3_flutter_libs.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3_flutter_libs.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_compress.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_compress.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player_avfoundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_thumbnail.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_thumbnail.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock.framework",
); );
@@ -442,7 +442,10 @@
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
@@ -581,7 +584,10 @@
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
@@ -615,7 +621,10 @@
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1020" LastUpgradeVersion = "1300"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
@@ -3,7 +3,6 @@ import 'package:example/localizations.dart';
import 'package:example/routes/routes.dart'; import 'package:example/routes/routes.dart';
import 'package:example/stream_version.dart'; import 'package:example/stream_version.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -1,52 +1,42 @@
import 'package:example/localizations.dart'; import 'package:example/localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
class ChannelFileDisplayScreen extends StatefulWidget { class ChannelFileDisplayScreen extends StatefulWidget {
/// The sorting used for the channels matching the filters. final StreamMessageThemeData messageTheme;
/// 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<SortOption>? 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({ const ChannelFileDisplayScreen({
this.sortOptions, Key? key,
this.paginationParams = const PaginationParams(limit: 20), required this.messageTheme,
this.emptyBuilder, }) : super(key: key);
});
@override @override
_ChannelFileDisplayScreenState createState() => State<ChannelFileDisplayScreen> createState() =>
_ChannelFileDisplayScreenState(); _ChannelFileDisplayScreenState();
} }
class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> { class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
@override final Map<String?, VideoPlayerController?> controllerCache = {};
void initState() {
super.initState(); late final controller = StreamMessageSearchListController(
final messageSearchBloc = MessageSearchBloc.of(context); client: StreamChat.of(context).client,
messageSearchBloc.search( filter: Filter.in_(
filter: Filter.in_( 'cid',
'cid', [StreamChannel.of(context).channel.cid!],
[StreamChannel.of(context).channel.cid!], ),
messageFilter: Filter.in_(
'attachments.type',
['file'],
),
sort: [
SortOption(
'created_at',
direction: SortOption.ASC,
), ),
messageFilter: Filter.in_( ],
'attachments.type', limit: 20,
['file'], );
),
sort: widget.sortOptions,
pagination: widget.paginationParams,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -58,120 +48,109 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
title: Text( title: Text(
AppLocalizations.of(context).files, AppLocalizations.of(context).files,
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16.0), 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.textHighEmphasis,
size: 24.0,
),
),
), ),
), ),
leading: StreamBackButton(),
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
), ),
body: _buildMediaGrid(), body: ValueListenableBuilder(
); valueListenable: controller,
} builder: (
BuildContext context,
Widget _buildMediaGrid() { PagedValue<String, GetMessageResponse> value,
final messageSearchBloc = MessageSearchBloc.of(context); Widget? child,
) {
return StreamBuilder<List<GetMessageResponse>>( return value.when(
builder: (context, snapshot) { (items, nextPageKey, error) {
if (snapshot.data == null) { if (items.isEmpty) {
return Center( return Center(
child: const CircularProgressIndicator(), child: Column(
); mainAxisAlignment: MainAxisAlignment.center,
} children: [
StreamSvgIcon.files(
if (snapshot.data!.isEmpty) { size: 136.0,
if (widget.emptyBuilder != null) { color: StreamChatTheme.of(context).colorTheme.disabled,
return widget.emptyBuilder!(context); ),
} SizedBox(height: 16.0),
return Center( Text(
child: Column( AppLocalizations.of(context).noFiles,
mainAxisAlignment: MainAxisAlignment.center, style: TextStyle(
children: [ fontSize: 14.0,
StreamSvgIcon.files( color: StreamChatTheme.of(context)
size: 136.0, .colorTheme
color: StreamChatTheme.of(context).colorTheme.disabled, .textHighEmphasis,
), ),
SizedBox(height: 16.0), ),
Text( SizedBox(height: 8.0),
AppLocalizations.of(context).noFiles, Text(
style: TextStyle( AppLocalizations.of(context).filesAppearHere,
fontSize: 14.0, textAlign: TextAlign.center,
color: style: TextStyle(
StreamChatTheme.of(context).colorTheme.textHighEmphasis, fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
],
), ),
), );
SizedBox(height: 8.0), }
Text( final media = <Attachment, Message>{};
AppLocalizations.of(context).filesAppearHere,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
],
),
);
}
final media = <Attachment, Message>{}; for (var item in items) {
item.message.attachments
.where((e) => e.type == 'file')
.forEach((e) {
media[e] = item.message;
});
}
for (var item in snapshot.data!) { return LazyLoadScrollView(
item.message.attachments.where((e) => e.type == 'file').forEach((e) { onEndOfPage: () async {
media[e] = item.message; if (nextPageKey != null) {
}); controller.loadMore(nextPageKey);
} }
},
return LazyLoadScrollView( child: ListView.builder(
onEndOfPage: () => messageSearchBloc.search( itemBuilder: (context, position) {
filter: Filter.in_( return Padding(
'cid', padding: const EdgeInsets.all(1.0),
[StreamChannel.of(context).channel.cid!], child: Padding(
), padding: const EdgeInsets.all(8.0),
messageFilter: Filter.in_( child: StreamFileAttachment(
'attachments.type', message: media.values.toList()[position],
['file'], attachment: media.keys.toList()[position],
), ),
sort: widget.sortOptions, ),
pagination: widget.paginationParams.copyWith( );
offset: messageSearchBloc.messageResponses?.length ?? 0, },
), itemCount: media.length,
),
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, loading: () => Center(
), child: const CircularProgressIndicator(),
); ),
}, error: (_) => Offstage(),
stream: messageSearchBloc.messagesStream, );
},
),
); );
} }
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
void initState() {
controller.doInitialLoad();
super.initState();
}
} }
+194 -85
View File
@@ -6,6 +6,7 @@ import 'package:example/search_text_field.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'channel_page.dart'; import 'channel_page.dart';
import 'chat_info_screen.dart'; import 'chat_info_screen.dart';
@@ -19,9 +20,21 @@ class ChannelList extends StatefulWidget {
class _ChannelList extends State<ChannelList> { class _ChannelList extends State<ChannelList> {
ScrollController _scrollController = ScrollController(); ScrollController _scrollController = ScrollController();
TextEditingController? _controller; late StreamMessageSearchListController _messageSearchListController =
StreamMessageSearchListController(
client: StreamChat.of(context).client,
filter: Filter.in_('members', [StreamChat.of(context).currentUser!.id]),
limit: 5,
searchQuery: '',
sort: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
);
String _channelQuery = ''; TextEditingController? _controller;
bool _isSearchActive = false; bool _isSearchActive = false;
@@ -31,14 +44,25 @@ class _ChannelList extends State<ChannelList> {
if (_debounce?.isActive ?? false) _debounce!.cancel(); if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () { _debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted) { if (mounted) {
_messageSearchListController.searchQuery = _controller!.text;
setState(() { setState(() {
_channelQuery = _controller!.text; _isSearchActive = _controller!.text.isNotEmpty;
_isSearchActive = _channelQuery.isNotEmpty;
}); });
if (_isSearchActive) _messageSearchListController.doInitialLoad();
} }
}); });
} }
late final _channelListController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
presence: true,
limit: 30,
);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -50,12 +74,12 @@ class _ChannelList extends State<ChannelList> {
_controller?.removeListener(_channelQueryListener); _controller?.removeListener(_channelQueryListener);
_controller?.dispose(); _controller?.dispose();
_scrollController.dispose(); _scrollController.dispose();
_channelListController.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = StreamChat.of(context).currentUser;
return WillPopScope( return WillPopScope(
onWillPop: () async { onWillPop: () async {
if (_isSearchActive) { if (_isSearchActive) {
@@ -86,18 +110,8 @@ class _ChannelList extends State<ChannelList> {
), ),
], ],
body: _isSearchActive body: _isSearchActive
? MessageSearchListView( ? StreamMessageSearchListView(
showErrorTile: true, controller: _messageSearchListController,
messageQuery: _channelQuery,
filters: Filter.in_('members', [user!.id]),
sortOptions: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
pullToRefresh: false,
limit: 30,
emptyBuilder: (_) { emptyBuilder: (_) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, viewportConstraints) { builder: (context, viewportConstraints) {
@@ -128,80 +142,175 @@ class _ChannelList extends State<ChannelList> {
}, },
); );
}, },
onItemTap: (messageResponse) async { itemBuilder: (
FocusScope.of(context).requestFocus(FocusNode()); context,
final client = StreamChat.of(context).client; messageResponses,
final message = messageResponse.message; index,
final channel = client.channel( defaultWidget,
messageResponse.channel!.type, ) {
id: messageResponse.channel!.id, return defaultWidget.copyWith(
); onTap: () async {
if (channel.state == null) { final messageResponse = messageResponses[index];
await channel.watch(); FocusScope.of(context).requestFocus(FocusNode());
} final client = StreamChat.of(context).client;
Navigator.pushNamed( final message = messageResponse.message;
context, final channel = client.channel(
Routes.CHANNEL_PAGE, messageResponse.channel!.type,
arguments: ChannelPageArgs( id: messageResponse.channel!.id,
channel: channel, );
initialMessage: message, if (channel.state == null) {
), await channel.watch();
}
Navigator.pushNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
);
},
); );
}, },
) )
: ChannelListView( : SlidableAutoCloseBehavior(
onChannelTap: (channel, _) { closeWhenOpened: true,
Navigator.pushNamed( child: RefreshIndicator(
context, onRefresh: _channelListController.refresh,
Routes.CHANNEL_PAGE, child: StreamChannelListView(
arguments: ChannelPageArgs( controller: _channelListController,
channel: channel, itemBuilder: (context, channels, index, defaultWidget) {
), final chatTheme = StreamChatTheme.of(context);
); final backgroundColor = chatTheme.colorTheme.inputBg;
}, final channel = channels[index];
onStartChatPressed: () { final canDeleteChannel = channel.ownCapabilities
Navigator.pushNamed(context, Routes.NEW_CHAT); .contains(PermissionType.deleteChannel);
}, return Slidable(
swipeToAction: true, groupTag: 'channels-actions',
filter: Filter.in_('members', [user!.id]), endActionPane: ActionPane(
presence: true, extentRatio: canDeleteChannel ? 0.40 : 0.20,
limit: 30, motion: const BehindMotion(),
onViewInfoTap: (channel) { children: [
Navigator.pop(context); CustomSlidableAction(
if (channel.memberCount == 2 && channel.isDistinct) { child: Icon(Icons.more_horiz),
Navigator.push( backgroundColor: backgroundColor,
context, onPressed: (_) {
MaterialPageRoute( showChannelInfoModalBottomSheet(
builder: (context) => StreamChannel( context: context,
channel: channel,
onViewInfoTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
final isOneToOne =
channel.memberCount == 2 &&
channel.isDistinct;
return StreamChannel(
channel: channel,
child: isOneToOne
? ChatInfoScreen(
messageTheme: chatTheme
.ownMessageTheme,
user: channel
.state!.members
.where((m) =>
m.userId !=
channel
.client
.state
.currentUser!
.id)
.first
.user,
)
: GroupInfoScreen(
messageTheme: chatTheme
.ownMessageTheme,
),
);
},
),
);
},
);
},
),
if (canDeleteChannel)
CustomSlidableAction(
backgroundColor: backgroundColor,
child: StreamSvgIcon.delete(
color: chatTheme.colorTheme.accentError,
),
onPressed: (_) async {
final res = await showConfirmationDialog(
context,
title: 'Delete Conversation',
question:
'Are you sure you want to delete this conversation?',
okText: 'Delete',
cancelText: 'Cancel',
icon: StreamSvgIcon.delete(
color: chatTheme.colorTheme.accentError,
),
);
if (res == true) {
await _channelListController
.deleteChannel(channel);
}
},
),
],
),
child: defaultWidget,
);
},
onChannelTap: (channel) {
Navigator.pushNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel, channel: channel,
child: ChatInfoScreen( ),
messageTheme: );
StreamChatTheme.of(context).ownMessageTheme, },
user: channel.state!.members emptyBuilder: (_) {
.where((m) => return Center(
m.userId != child: Padding(
channel.client.state.currentUser!.id) padding: const EdgeInsets.all(8),
.first child: StreamScrollViewEmptyWidget(
.user, emptyIcon: StreamSvgIcon.message(
size: 148,
color: StreamChatTheme.of(context)
.colorTheme
.disabled,
),
emptyTitle: TextButton(
onPressed: () {
Navigator.pushNamed(
context,
Routes.NEW_CHAT,
);
},
child: Text(
'Start a chat',
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.accentPrimary,
),
),
),
), ),
), ),
), );
); },
} else { ),
Navigator.push( ),
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: GroupInfoScreen(
messageTheme:
StreamChatTheme.of(context).ownMessageTheme,
),
),
),
);
}
},
), ),
), ),
), ),
@@ -40,7 +40,7 @@ class _ChannelListPageState extends State<ChannelListPage> {
Positioned( Positioned(
top: -3, top: -3,
right: -16, right: -16,
child: UnreadIndicator(), child: StreamUnreadIndicator(),
), ),
], ],
), ),
@@ -70,7 +70,7 @@ class _ChannelListPageState extends State<ChannelListPage> {
} }
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: ChannelListHeader( appBar: StreamChannelListHeader(
onNewChatButtonTap: () { onNewChatButtonTap: () {
Navigator.pushNamed(context, Routes.NEW_CHAT); Navigator.pushNamed(context, Routes.NEW_CHAT);
}, },
@@ -100,11 +100,7 @@ class _ChannelListPageState extends State<ChannelListPage> {
body: IndexedStack( body: IndexedStack(
index: _currentIndex, index: _currentIndex,
children: [ children: [
ChannelsBloc( ChannelList(),
child: MessageSearchBloc(
child: ChannelList(),
),
),
UserMentionsPage(), UserMentionsPage(),
], ],
), ),
@@ -165,7 +161,7 @@ class LeftDrawer extends StatelessWidget {
), ),
child: Row( child: Row(
children: [ children: [
UserAvatar( StreamUserAvatar(
user: user, user: user,
showOnlineStatus: false, showOnlineStatus: false,
constraints: BoxConstraints.tight(Size.fromRadius(20)), constraints: BoxConstraints.tight(Size.fromRadius(20)),
@@ -1,61 +1,45 @@
import 'package:example/localizations.dart'; import 'package:example/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'channel_page.dart';
class ChannelMediaDisplayScreen extends StatefulWidget { class ChannelMediaDisplayScreen extends StatefulWidget {
/// The sorting used for the channels matching the filters. final StreamMessageThemeData messageTheme;
/// 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<SortOption>? 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 MessageThemeData messageTheme;
const ChannelMediaDisplayScreen({ const ChannelMediaDisplayScreen({
Key? key,
required this.messageTheme, required this.messageTheme,
this.sortOptions, }) : super(key: key);
this.paginationParams = const PaginationParams(limit: 20),
this.emptyBuilder,
this.onShowMessage,
});
@override @override
_ChannelMediaDisplayScreenState createState() => State<ChannelMediaDisplayScreen> createState() =>
_ChannelMediaDisplayScreenState(); _ChannelMediaDisplayScreenState();
} }
class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> { class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
Map<String?, VideoPlayerController?> controllerCache = {}; final Map<String?, VideoPlayerController?> controllerCache = {};
@override late final controller = StreamMessageSearchListController(
void initState() { client: StreamChat.of(context).client,
super.initState(); filter: Filter.in_(
final messageSearchBloc = MessageSearchBloc.of(context); 'cid',
messageSearchBloc.search( [StreamChannel.of(context).channel.cid!],
filter: Filter.in_( ),
'cid', messageFilter: Filter.in_(
[StreamChannel.of(context).channel.cid!], 'attachments.type',
['image', 'video'],
),
sort: [
SortOption(
'created_at',
direction: SortOption.ASC,
), ),
messageFilter: Filter.in_( ],
'attachments.type', limit: 20,
['image', 'video'], );
),
sort: widget.sortOptions,
pagination: widget.paginationParams,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -74,155 +58,169 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
leading: StreamBackButton(), leading: StreamBackButton(),
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
), ),
body: _buildMediaGrid(), body: ValueListenableBuilder(
); valueListenable: controller,
} builder: (BuildContext context,
PagedValue<String, GetMessageResponse> value, Widget? child) {
Widget _buildMediaGrid() { return value.when(
final messageSearchBloc = MessageSearchBloc.of(context); (items, nextPageKey, error) {
if (items.isEmpty) {
return StreamBuilder<List<GetMessageResponse>>( return Center(
builder: (context, snapshot) { child: Column(
if (snapshot.data == null) { mainAxisAlignment: MainAxisAlignment.center,
return Center( children: [
child: const CircularProgressIndicator(), StreamSvgIcon.pictures(
); size: 136.0,
} color: StreamChatTheme.of(context).colorTheme.disabled,
),
if (snapshot.data!.isEmpty) { SizedBox(height: 16.0),
if (widget.emptyBuilder != null) { Text(
return widget.emptyBuilder!(context); AppLocalizations.of(context).noMedia,
} style: TextStyle(
return Center( fontSize: 14.0,
child: Column( color: StreamChatTheme.of(context)
mainAxisAlignment: MainAxisAlignment.center, .colorTheme
children: [ .textHighEmphasis,
StreamSvgIcon.pictures(
size: 136.0,
color: StreamChatTheme.of(context).colorTheme.disabled,
),
SizedBox(height: 16.0),
Text(
AppLocalizations.of(context).noMedia,
style: TextStyle(
fontSize: 14.0,
color:
StreamChatTheme.of(context).colorTheme.textHighEmphasis,
),
),
SizedBox(height: 8.0),
Text(
AppLocalizations.of(context).photosOrVideosWillAppearHere,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.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.search(
filter: Filter.in_(
'cid',
[StreamChannel.of(context).channel.cid!],
),
messageFilter: Filter.in_(
'attachments.type',
['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,
userName: media[position].message.user!.name,
onShowMessage: widget.onShowMessage,
),
), ),
), ),
SizedBox(height: 8.0),
Text(
AppLocalizations.of(context)
.photosOrVideosWillAppearHere,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
],
),
);
}
final media = <_AssetPackage>[];
for (var item in value.asSuccess.items) {
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: () async {
if (nextPageKey != null) {
controller.loadMore(nextPageKey);
}
},
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: StreamFullScreenMedia(
mediaAttachmentPackages: media
.map(
(e) => StreamAttachmentPackage(
attachment: e.attachment,
message: e.message,
),
)
.toList(),
startIndex: position,
userName: media[position].message.user!.name,
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,
),
);
},
),
),
),
);
},
child: media[position].attachment.type == 'image'
? IgnorePointer(
child: StreamImageAttachment(
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,
),
messageTheme: widget.messageTheme,
),
)
: VideoPlayer(media[position].videoPlayer!),
),
); );
}, },
child: media[position].attachment.type == 'image' itemCount: media.length,
? 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,
),
messageTheme: widget.messageTheme,
),
)
: VideoPlayer(media[position].videoPlayer!),
), ),
); );
}, },
itemCount: media.length, loading: () => Center(
), child: const CircularProgressIndicator(),
); ),
}, error: (_) => Offstage(),
stream: messageSearchBloc.messagesStream, );
},
),
); );
} }
@override @override
void dispose() { void dispose() {
controller.dispose();
super.dispose(); super.dispose();
for (var c in controllerCache.values) { }
c!.dispose();
} @override
void initState() {
controller.doInitialLoad();
super.initState();
} }
} }
+8 -14
View File
@@ -34,8 +34,8 @@ class ChannelPage extends StatefulWidget {
} }
class _ChannelPageState extends State<ChannelPage> { class _ChannelPageState extends State<ChannelPage> {
Message? _quotedMessage;
FocusNode? _focusNode; FocusNode? _focusNode;
StreamMessageInputController _messageInputController = StreamMessageInputController();
@override @override
void initState() { void initState() {
@@ -50,7 +50,7 @@ class _ChannelPageState extends State<ChannelPage> {
} }
void _reply(Message message) { void _reply(Message message) {
setState(() => _quotedMessage = message); _messageInputController.quotedMessage = message;
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_focusNode!.requestFocus(); _focusNode!.requestFocus();
}); });
@@ -60,7 +60,7 @@ class _ChannelPageState extends State<ChannelPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: ChannelHeader( appBar: StreamChannelHeader(
showTypingIndicator: false, showTypingIndicator: false,
onImageTap: () async { onImageTap: () async {
var channel = StreamChannel.of(context).channel; var channel = StreamChannel.of(context).channel;
@@ -108,7 +108,7 @@ class _ChannelPageState extends State<ChannelPage> {
Expanded( Expanded(
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
MessageListView( StreamMessageListView(
initialScrollIndex: widget.initialScrollIndex, initialScrollIndex: widget.initialScrollIndex,
initialAlignment: widget.initialAlignment, initialAlignment: widget.initialAlignment,
highlightInitialMessage: widget.highlightInitialMessage, highlightInitialMessage: widget.highlightInitialMessage,
@@ -137,14 +137,13 @@ class _ChannelPageState extends State<ChannelPage> {
); );
}, },
deletedBottomRowBuilder: (context, message) { deletedBottomRowBuilder: (context, message) {
return const VisibleFootnote(); return const StreamVisibleFootnote();
}, },
); );
}, },
threadBuilder: (_, parentMessage) { threadBuilder: (_, parentMessage) {
return ThreadPage(parent: parentMessage!); return ThreadPage(parent: parentMessage!);
}, },
pinPermissions: ['owner', 'admin', 'member'],
), ),
Positioned( Positioned(
bottom: 0, bottom: 0,
@@ -156,8 +155,7 @@ class _ChannelPageState extends State<ChannelPage> {
.colorTheme .colorTheme
.appBg .appBg
.withOpacity(.9), .withOpacity(.9),
child: TypingIndicator( child: StreamTypingIndicator(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
vertical: 4, vertical: 4,
@@ -175,13 +173,9 @@ class _ChannelPageState extends State<ChannelPage> {
], ],
), ),
), ),
MessageInput( StreamMessageInput(
focusNode: _focusNode, focusNode: _focusNode,
quotedMessage: _quotedMessage, messageInputController: _messageInputController,
onQuotedMessageCleared: () {
setState(() => _quotedMessage = null);
_focusNode!.unfocus();
},
), ),
], ],
), ),
@@ -1,22 +1,18 @@
import 'package:collection/collection.dart' show IterableExtension;
import 'package:example/localizations.dart'; import 'package:example/localizations.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_file_display_screen.dart'; import 'channel_file_display_screen.dart';
import 'channel_media_display_screen.dart'; import 'channel_media_display_screen.dart';
import 'channel_page.dart';
import 'pinned_messages_screen.dart'; import 'pinned_messages_screen.dart';
import 'routes/routes.dart';
/// Detail screen for a 1:1 chat correspondence /// Detail screen for a 1:1 chat correspondence
class ChatInfoScreen extends StatefulWidget { class ChatInfoScreen extends StatefulWidget {
/// User in consideration /// User in consideration
final User? user; final User? user;
final MessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
const ChatInfoScreen({ const ChatInfoScreen({
Key? key, Key? key,
@@ -54,13 +50,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
height: 8.0, height: 8.0,
color: StreamChatTheme.of(context).colorTheme.disabled, color: StreamChatTheme.of(context).colorTheme.disabled,
), ),
if ([ if (channel.ownCapabilities.contains(PermissionType.deleteChannel))
'admin',
'owner',
].contains(channel.state!.members
.firstWhereOrNull(
(m) => m.userId == channel.client.state.currentUser!.id)
?.role))
_buildDeleteListTile(), _buildDeleteListTile(),
], ],
), ),
@@ -78,7 +68,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: UserAvatar( child: StreamUserAvatar(
user: widget.user!, user: widget.user!,
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
width: 72.0, width: 72.0,
@@ -95,7 +85,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
SizedBox(height: 7.0), SizedBox(height: 7.0),
_buildConnectedTitleState(), _buildConnectedTitleState(),
SizedBox(height: 15.0), SizedBox(height: 15.0),
OptionListTile( StreamOptionListTile(
title: '@${widget.user!.id}', title: '@${widget.user!.id}',
tileColor: StreamChatTheme.of(context).colorTheme.appBg, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
trailing: Padding( trailing: Padding(
@@ -148,7 +138,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
builder: (context, snapshot) { builder: (context, snapshot) {
mutedBool.value = snapshot.data; mutedBool.value = snapshot.data;
return OptionListTile( return StreamOptionListTile(
tileColor: StreamChatTheme.of(context).colorTheme.appBg, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
title: AppLocalizations.of(context).muteUser, title: AppLocalizations.of(context).muteUser,
titleTextStyle: StreamChatTheme.of(context).textTheme.body, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
@@ -201,7 +191,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
// ), // ),
// onTap: () {}, // onTap: () {},
// ), // ),
OptionListTile( StreamOptionListTile(
title: AppLocalizations.of(context).pinnedMessages, title: AppLocalizations.of(context).pinnedMessages,
tileColor: StreamChatTheme.of(context).colorTheme.appBg, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
@@ -226,43 +216,13 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageSearchBloc( child: PinnedMessagesScreen(),
child: PinnedMessagesScreen(
messageTheme: widget.messageTheme,
sortOptions: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
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.pushNamedAndRemoveUntil(
context,
Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.CHANNEL_LIST_PAGE),
arguments: ChannelPageArgs(
channel: c,
initialMessage: m,
),
);
},
),
),
), ),
), ),
); );
}, },
), ),
OptionListTile( StreamOptionListTile(
title: AppLocalizations.of(context).photosAndVideos, title: AppLocalizations.of(context).photosAndVideos,
tileColor: StreamChatTheme.of(context).colorTheme.appBg, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
@@ -287,42 +247,15 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageSearchBloc( child: ChannelMediaDisplayScreen(
child: ChannelMediaDisplayScreen( messageTheme: widget.messageTheme,
messageTheme: widget.messageTheme,
sortOptions: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
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( StreamOptionListTile(
title: AppLocalizations.of(context).files, title: AppLocalizations.of(context).files,
tileColor: StreamChatTheme.of(context).colorTheme.appBg, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
@@ -347,22 +280,15 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageSearchBloc( child: ChannelFileDisplayScreen(
child: ChannelFileDisplayScreen( messageTheme: widget.messageTheme,
sortOptions: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
),
), ),
), ),
), ),
); );
}, },
), ),
OptionListTile( StreamOptionListTile(
title: AppLocalizations.of(context).sharedGroups, title: AppLocalizations.of(context).sharedGroups,
tileColor: StreamChatTheme.of(context).colorTheme.appBg, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
@@ -392,7 +318,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
} }
Widget _buildDeleteListTile() { Widget _buildDeleteListTile() {
return OptionListTile( return StreamOptionListTile(
title: 'Delete Conversation', title: 'Delete Conversation',
tileColor: StreamChatTheme.of(context).colorTheme.appBg, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body.copyWith( titleTextStyle: StreamChatTheme.of(context).textTheme.body.copyWith(
@@ -633,7 +559,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: ChannelAvatar( child: StreamChannelAvatar(
channel: channel, channel: channel,
constraints: constraints:
BoxConstraints(maxWidth: 40.0, maxHeight: 40.0), BoxConstraints(maxWidth: 40.0, maxHeight: 40.0),
@@ -124,7 +124,7 @@ class ChooseUserPage extends StatelessWidget {
arguments: HomePageArgs(client), arguments: HomePageArgs(client),
); );
}, },
leading: UserAvatar( leading: StreamUserAvatar(
user: user, user: user,
constraints: BoxConstraints.tight( constraints: BoxConstraints.tight(
Size.fromRadius(20), Size.fromRadius(20),
@@ -1,7 +1,6 @@
import 'package:example/localizations.dart'; import 'package:example/localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:uuid/uuid.dart';
import 'channel_page.dart'; import 'channel_page.dart';
import 'routes/routes.dart'; import 'routes/routes.dart';
@@ -155,7 +154,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
), ),
], ],
), ),
body: ConnectionStatusBuilder( body: StreamConnectionStatusBuilder(
statusBuilder: (context, status) { statusBuilder: (context, status) {
String statusString = ''; String statusString = '';
bool showStatus = true; bool showStatus = true;
@@ -172,7 +171,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
statusString = AppLocalizations.of(context).disconnected; statusString = AppLocalizations.of(context).disconnected;
break; break;
} }
return InfoTile( return StreamInfoTile(
showMessage: showStatus, showMessage: showStatus,
tileAnchor: Alignment.topCenter, tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter,
@@ -222,7 +221,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
final user = _selectedUsers[index]; final user = _selectedUsers[index];
return ListTile( return ListTile(
key: ObjectKey(user), key: ObjectKey(user),
leading: UserAvatar( leading: StreamUserAvatar(
user: user, user: user,
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
width: 40, width: 40,
+173 -228
View File
@@ -4,7 +4,6 @@ import 'package:collection/collection.dart' show IterableExtension;
import 'package:example/localizations.dart'; import 'package:example/localizations.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_file_display_screen.dart'; import 'channel_file_display_screen.dart';
@@ -12,10 +11,9 @@ import 'channel_media_display_screen.dart';
import 'channel_page.dart'; import 'channel_page.dart';
import 'chat_info_screen.dart'; import 'chat_info_screen.dart';
import 'pinned_messages_screen.dart'; import 'pinned_messages_screen.dart';
import 'routes/routes.dart';
class GroupInfoScreen extends StatefulWidget { class GroupInfoScreen extends StatefulWidget {
final MessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
const GroupInfoScreen({ const GroupInfoScreen({
Key? key, Key? key,
@@ -41,16 +39,52 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
ValueNotifier<bool?> mutedBool = ValueNotifier(false); ValueNotifier<bool?> mutedBool = ValueNotifier(false);
late final channel = StreamChannel.of(context).channel;
late final userListController = StreamUserListController(
client: StreamChat.of(context).client,
limit: 25,
filter: Filter.and(
[
if (_searchController!.text.isNotEmpty)
Filter.autoComplete('name', _userNameQuery),
Filter.notIn('id', [
StreamChat.of(context).currentUser!.id,
...channel.state!.members
.map<String?>(((e) => e.userId))
.whereType<String>(),
]),
],
),
sort: [
SortOption(
'name',
direction: 1,
),
],
);
void _userNameListener() { void _userNameListener() {
if (_searchController!.text == _userNameQuery) { if (_searchController!.text == _userNameQuery) {
return; return;
} }
if (_debounce?.isActive ?? false) _debounce!.cancel(); if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () { _debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted && modalSetStateCallback != null) { if (mounted) {
modalSetStateCallback!(() { _userNameQuery = _searchController!.text;
_userNameQuery = _searchController!.text; userListController.filter = Filter.and(
}); [
if (_searchController!.text.isNotEmpty)
Filter.autoComplete('name', _userNameQuery),
Filter.notIn('id', [
StreamChat.of(context).currentUser!.id,
...channel.state!.members
.map<String?>(((e) => e.userId))
.whereType<String>(),
]),
],
);
userListController.doInitialLoad();
} }
}); });
} }
@@ -58,25 +92,28 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
var channel = StreamChannel.of(context);
_nameController = TextEditingController.fromValue( _nameController = TextEditingController.fromValue(
TextEditingValue( TextEditingValue(text: (channel.extraData['name'] as String?) ?? ''),
text: (channel.channel.extraData['name'] as String?) ?? ''),
); );
_searchController = TextEditingController()..addListener(_userNameListener); _searchController = TextEditingController()..addListener(_userNameListener);
_nameController!.addListener(() { _nameController!.addListener(() {
setState(() {}); setState(() {});
}); });
mutedBool = ValueNotifier(StreamChannel.of(context).channel.isMuted); mutedBool = ValueNotifier(channel.isMuted);
}
@override
void dispose() {
userListController.dispose();
super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var channel = StreamChannel.of(context);
return StreamBuilder<List<Member>>( return StreamBuilder<List<Member>>(
stream: channel.channel.state!.membersStream, stream: channel.state!.membersStream,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return Container( return Container(
@@ -85,11 +122,6 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
); );
} }
var userMember = snapshot.data!.firstWhereOrNull(
(e) => e.user!.id == StreamChat.of(context).currentUser!.id,
);
var isOwner = userMember?.role == 'owner';
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: AppBar( appBar: AppBar(
@@ -100,7 +132,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
title: Column( title: Column(
children: [ children: [
StreamBuilder<ChannelState>( StreamBuilder<ChannelState>(
stream: channel.channelStateStream, stream: channel.state?.channelStateStream,
builder: (context, state) { builder: (context, state) {
if (!state.hasData) { if (!state.hasData) {
return Text( return Text(
@@ -137,7 +169,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
height: 3.0, height: 3.0,
), ),
Text( Text(
'${channel.channel.memberCount} ${AppLocalizations.of(context).members}, ${snapshot.data?.where((e) => e.user!.online).length ?? 0} ${AppLocalizations.of(context).online}', '${channel.memberCount} ${AppLocalizations.of(context).members}, ${snapshot.data?.where((e) => e.user!.online).length ?? 0} ${AppLocalizations.of(context).online}',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
@@ -150,7 +182,8 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
), ),
centerTitle: true, centerTitle: true,
actions: [ actions: [
if (!channel.channel.isDistinct && isOwner) if (channel.ownCapabilities
.contains(PermissionType.updateChannelMembers))
StreamNeumorphicButton( StreamNeumorphicButton(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
@@ -174,7 +207,9 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
height: 8.0, height: 8.0,
color: StreamChatTheme.of(context).colorTheme.disabled, color: StreamChatTheme.of(context).colorTheme.disabled,
), ),
if (isOwner) _buildNameTile(), if (channel.ownCapabilities
.contains(PermissionType.updateChannel))
_buildNameTile(),
_buildOptionListTiles(), _buildOptionListTiles(),
], ],
), ),
@@ -224,7 +259,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
horizontal: 8.0, horizontal: 8.0,
vertical: 12.0, vertical: 12.0,
), ),
child: UserAvatar( child: StreamUserAvatar(
user: member.user!, user: member.user!,
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
height: 40.0, height: 40.0,
@@ -339,7 +374,6 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
} }
Widget _buildNameTile() { Widget _buildNameTile() {
var channel = StreamChannel.of(context).channel;
var channelName = (channel.extraData['name'] as String?) ?? ''; var channelName = (channel.extraData['name'] as String?) ?? '';
return Material( return Material(
@@ -365,6 +399,8 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
), ),
Expanded( Expanded(
child: TextField( child: TextField(
enabled: channel.ownCapabilities
.contains(PermissionType.updateChannel),
focusNode: _focusNode, focusNode: _focusNode,
controller: _nameController, controller: _nameController,
cursorColor: cursorColor:
@@ -413,7 +449,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
size: 24.0, size: 24.0,
), ),
onTap: () { onTap: () {
StreamChannel.of(context).channel.update({ channel.update({
'name': _nameController!.text.trim(), 'name': _nameController!.text.trim(),
}).catchError((err) { }).catchError((err) {
setState(() { setState(() {
@@ -433,8 +469,6 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
} }
Widget _buildOptionListTiles() { Widget _buildOptionListTiles() {
var channel = StreamChannel.of(context);
return Column( return Column(
children: [ children: [
// OptionListTile( // OptionListTile(
@@ -449,48 +483,50 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
// ), // ),
// onTap: () {}, // onTap: () {},
// ), // ),
StreamBuilder<bool>( if (channel.ownCapabilities.contains(PermissionType.muteChannel))
stream: StreamChannel.of(context).channel.isMutedStream, StreamBuilder<bool>(
builder: (context, snapshot) { stream: channel.isMutedStream,
mutedBool.value = snapshot.data; builder: (context, snapshot) {
mutedBool.value = snapshot.data;
return OptionListTile( return StreamOptionListTile(
tileColor: StreamChatTheme.of(context).colorTheme.appBg, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
separatorColor: StreamChatTheme.of(context).colorTheme.disabled, separatorColor:
title: AppLocalizations.of(context).muteGroup, StreamChatTheme.of(context).colorTheme.disabled,
titleTextStyle: StreamChatTheme.of(context).textTheme.body, title: AppLocalizations.of(context).muteGroup,
leading: Padding( titleTextStyle: StreamChatTheme.of(context).textTheme.body,
padding: const EdgeInsets.symmetric(horizontal: 16.0), leading: Padding(
child: StreamSvgIcon.mute( padding: const EdgeInsets.symmetric(horizontal: 16.0),
size: 24.0, child: StreamSvgIcon.mute(
color: StreamChatTheme.of(context) size: 24.0,
.colorTheme color: StreamChatTheme.of(context)
.textHighEmphasis .colorTheme
.withOpacity(0.5), .textHighEmphasis
.withOpacity(0.5),
),
), ),
), trailing: snapshot.data == null
trailing: snapshot.data == null ? CircularProgressIndicator()
? CircularProgressIndicator() : ValueListenableBuilder<bool?>(
: ValueListenableBuilder<bool?>( valueListenable: mutedBool,
valueListenable: mutedBool, builder: (context, value, _) {
builder: (context, value, _) { return CupertinoSwitch(
return CupertinoSwitch( value: value!,
value: value!, onChanged: (val) {
onChanged: (val) { mutedBool.value = val;
mutedBool.value = val;
if (snapshot.data!) { if (snapshot.data!) {
channel.channel.unmute(); channel.unmute();
} else { } else {
channel.channel.mute(); channel.mute();
} }
}, },
); );
}), }),
onTap: () {}, onTap: () {},
); );
}), }),
OptionListTile( StreamOptionListTile(
title: AppLocalizations.of(context).pinnedMessages, title: AppLocalizations.of(context).pinnedMessages,
tileColor: StreamChatTheme.of(context).colorTheme.appBg, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
@@ -515,43 +551,13 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageSearchBloc( child: PinnedMessagesScreen(),
child: PinnedMessagesScreen(
messageTheme: widget.messageTheme,
sortOptions: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
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.pushNamedAndRemoveUntil(
context,
Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.CHANNEL_LIST_PAGE),
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
);
},
),
),
), ),
), ),
); );
}, },
), ),
OptionListTile( StreamOptionListTile(
tileColor: StreamChatTheme.of(context).colorTheme.appBg, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
separatorColor: StreamChatTheme.of(context).colorTheme.disabled, separatorColor: StreamChatTheme.of(context).colorTheme.disabled,
title: AppLocalizations.of(context).photosAndVideos, title: AppLocalizations.of(context).photosAndVideos,
@@ -577,43 +583,15 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageSearchBloc( child: ChannelMediaDisplayScreen(
child: ChannelMediaDisplayScreen( messageTheme: widget.messageTheme,
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();
}
await Navigator.pushNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
);
},
),
), ),
), ),
), ),
); );
}, },
), ),
OptionListTile( StreamOptionListTile(
tileColor: StreamChatTheme.of(context).colorTheme.appBg, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
separatorColor: StreamChatTheme.of(context).colorTheme.disabled, separatorColor: StreamChatTheme.of(context).colorTheme.disabled,
title: AppLocalizations.of(context).files, title: AppLocalizations.of(context).files,
@@ -639,24 +617,17 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageSearchBloc( child: ChannelFileDisplayScreen(
child: ChannelFileDisplayScreen( messageTheme: widget.messageTheme,
sortOptions: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
paginationParams: PaginationParams(limit: 20),
),
), ),
), ),
), ),
); );
}, },
), ),
if (!channel.channel.isDistinct) if (!channel.isDistinct &&
OptionListTile( channel.ownCapabilities.contains(PermissionType.leaveChannel))
StreamOptionListTile(
tileColor: StreamChatTheme.of(context).colorTheme.appBg, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
separatorColor: StreamChatTheme.of(context).colorTheme.disabled, separatorColor: StreamChatTheme.of(context).colorTheme.disabled,
title: AppLocalizations.of(context).leaveGroup, title: AppLocalizations.of(context).leaveGroup,
@@ -700,106 +671,81 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
} }
void _buildAddUserModal(context) { void _buildAddUserModal(context) {
var channel = StreamChannel.of(context).channel;
showDialog( showDialog(
useRootNavigator: false, useRootNavigator: false,
context: context, context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay, barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
builder: (context) { builder: (context) {
return StatefulBuilder(builder: (context, modalSetState) { return Padding(
modalSetStateCallback = modalSetState; padding: EdgeInsets.only(top: 16.0, left: 8.0, right: 8.0),
return Padding( child: Material(
padding: EdgeInsets.only(top: 16.0, left: 8.0, right: 8.0), borderRadius: BorderRadius.only(
child: Material( topLeft: Radius.circular(16.0),
borderRadius: BorderRadius.only( topRight: Radius.circular(16.0),
topLeft: Radius.circular(16.0), ),
topRight: Radius.circular(16.0), clipBehavior: Clip.antiAlias,
), child: Scaffold(
clipBehavior: Clip.antiAlias, body: Column(
child: Scaffold( children: [
body: UsersBloc( Padding(
child: Column( padding: const EdgeInsets.all(16),
children: [ child: _buildTextInputSection(),
Padding( ),
padding: const EdgeInsets.all(16), Expanded(
child: _buildTextInputSection(modalSetState), child: StreamUserGridView(
), controller: userListController,
Expanded( onUserTap: (user) async {
child: UserListView( _searchController!.clear();
selectedUsers: {},
onUserTap: (user, _) async {
_searchController!.clear();
await channel.addMembers([user.id]); await channel.addMembers([user.id]);
Navigator.pop(context); Navigator.pop(context);
setState(() {}); setState(() {});
}, },
crossAxisCount: 4, emptyBuilder: (_) {
limit: 25, return LayoutBuilder(
filter: Filter.and( builder: (context, viewportConstraints) {
[ return SingleChildScrollView(
if (_searchController!.text.isNotEmpty) physics: AlwaysScrollableScrollPhysics(),
Filter.autoComplete('name', _userNameQuery), child: ConstrainedBox(
Filter.notIn('id', [ constraints: BoxConstraints(
StreamChat.of(context).currentUser!.id, minHeight: viewportConstraints.maxHeight,
...channel.state!.members ),
.map<String?>(((e) => e.userId)) child: Center(
.whereType<String>(), child: Column(
]), children: [
], Padding(
), padding: const EdgeInsets.all(24),
sort: [ child: StreamSvgIcon.search(
SortOption( size: 96,
'name', color: StreamChatTheme.of(context)
direction: 1, .colorTheme
), .textLowEmphasis,
], ),
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: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
Text(AppLocalizations.of(context)
.noUserMatchesTheseKeywords),
],
), ),
), Text(AppLocalizations.of(context)
.noUserMatchesTheseKeywords),
],
), ),
); ),
}, ),
); );
}, },
), );
), },
], ),
), ),
), ],
), ),
), ),
); ),
}); );
}, },
); ).whenComplete(() {
_searchController?.clear();
});
} }
Widget _buildTextInputSection(modalSetState) { Widget _buildTextInputSection() {
final theme = StreamChatTheme.of(context); final theme = StreamChatTheme.of(context);
return Column( return Column(
children: [ children: [
@@ -859,7 +805,6 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
} }
void _showUserInfoModal(User? user, bool isUserAdmin) { void _showUserInfoModal(User? user, bool isUserAdmin) {
var channel = StreamChannel.of(context).channel;
final color = StreamChatTheme.of(context).colorTheme.barsBg; final color = StreamChatTheme.of(context).colorTheme.barsBg;
showModalBottomSheet( showModalBottomSheet(
@@ -896,7 +841,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: UserAvatar( child: StreamUserAvatar(
user: user, user: user,
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
height: 64.0, height: 64.0,
@@ -4,7 +4,6 @@ import 'package:example/channel_page.dart';
import 'package:example/notifications_service.dart'; import 'package:example/notifications_service.dart';
import 'package:example/routes/app_routes.dart'; import 'package:example/routes/app_routes.dart';
import 'package:example/routes/routes.dart'; import 'package:example/routes/routes.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -19,6 +19,20 @@ class _NewChatScreenState extends State<NewChatScreen> {
late TextEditingController _controller; late TextEditingController _controller;
late final userListController = StreamUserListController(
client: StreamChat.of(context).client,
limit: 25,
filter: Filter.and([
Filter.notEqual('id', StreamChat.of(context).currentUser!.id),
]),
sort: [
SortOption(
'name',
direction: 1,
),
],
);
ChipInputTextFieldState? get _chipInputTextFieldState => ChipInputTextFieldState? get _chipInputTextFieldState =>
_chipInputTextFieldStateKey.currentState; _chipInputTextFieldStateKey.currentState;
@@ -45,6 +59,12 @@ class _NewChatScreenState extends State<NewChatScreen> {
_userNameQuery = _controller.text; _userNameQuery = _controller.text;
_isSearchActive = _userNameQuery.isNotEmpty; _isSearchActive = _userNameQuery.isNotEmpty;
}); });
userListController.filter = Filter.and([
if (_userNameQuery.isNotEmpty)
Filter.autoComplete('name', _userNameQuery),
Filter.notEqual('id', StreamChat.of(context).currentUser!.id),
]);
userListController.doInitialLoad();
}); });
} }
@@ -112,6 +132,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
_controller.clear(); _controller.clear();
_controller.removeListener(_userNameListener); _controller.removeListener(_userNameListener);
_controller.dispose(); _controller.dispose();
userListController.dispose();
super.dispose(); super.dispose();
} }
@@ -130,7 +151,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
), ),
centerTitle: true, centerTitle: true,
), ),
body: ConnectionStatusBuilder( body: StreamConnectionStatusBuilder(
statusBuilder: (context, status) { statusBuilder: (context, status) {
String statusString = ''; String statusString = '';
bool showStatus = true; bool showStatus = true;
@@ -147,7 +168,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
statusString = AppLocalizations.of(context).disconnected; statusString = AppLocalizations.of(context).disconnected;
break; break;
} }
return InfoTile( return StreamInfoTile(
showMessage: showStatus, showMessage: showStatus,
tileAnchor: Alignment.topCenter, tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter,
@@ -200,7 +221,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
.overlay, .overlay,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: UserAvatar( child: StreamUserAvatar(
showOnlineStatus: false, showOnlineStatus: false,
user: user, user: user,
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -287,86 +308,81 @@ class _NewChatScreenState extends State<NewChatScreen> {
? GestureDetector( ? GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(), onPanDown: (_) => FocusScope.of(context).unfocus(),
child: UsersBloc( child: StreamUserListView(
child: UserListView( controller: userListController,
selectedUsers: _selectedUsers, // groupAlphabetically:
groupAlphabetically: // _isSearchActive ? false : true,
_isSearchActive ? false : true, onUserTap: (user) {
onUserTap: (user, _) { _controller.clear();
_controller.clear(); if (!_selectedUsers.contains(user)) {
if (!_selectedUsers.contains(user)) { _chipInputTextFieldState
_chipInputTextFieldState ?..addItem(user)
?..addItem(user) ..pauseItemAddition();
..pauseItemAddition(); } else {
} else { _chipInputTextFieldState!.removeItem(user);
_chipInputTextFieldState!.removeItem(user); }
} },
}, itemBuilder: (
limit: 25, context,
filter: Filter.and([ users,
if (_userNameQuery.isNotEmpty) index,
Filter.autoComplete('name', _userNameQuery), defaultWidget,
Filter.notEqual('id', ) {
StreamChat.of(context).currentUser!.id), return defaultWidget.copyWith(
]), selected:
sort: [ _selectedUsers.contains(users[index]),
SortOption( );
'name', },
direction: 1, emptyBuilder: (_) {
), return LayoutBuilder(
], builder: (context, viewportConstraints) {
emptyBuilder: (_) { return SingleChildScrollView(
return LayoutBuilder( physics: AlwaysScrollableScrollPhysics(),
builder: (context, viewportConstraints) { child: ConstrainedBox(
return SingleChildScrollView( constraints: BoxConstraints(
physics: minHeight:
AlwaysScrollableScrollPhysics(), viewportConstraints.maxHeight,
child: ConstrainedBox( ),
constraints: BoxConstraints( child: Center(
minHeight: child: Column(
viewportConstraints.maxHeight, children: [
), Padding(
child: Center( padding:
child: Column( const EdgeInsets.all(24),
children: [ child: StreamSvgIcon.search(
Padding( size: 96,
padding: color: Colors.grey,
const EdgeInsets.all(24),
child: StreamSvgIcon.search(
size: 96,
color: Colors.grey,
),
), ),
Text( ),
AppLocalizations.of(context) Text(
.noUserMatchesTheseKeywords, AppLocalizations.of(context)
style: StreamChatTheme.of( .noUserMatchesTheseKeywords,
context) style: StreamChatTheme.of(
.textTheme context)
.footnote .textTheme
.copyWith( .footnote
color: StreamChatTheme .copyWith(
.of(context) color: StreamChatTheme
.colorTheme .of(context)
.textHighEmphasis .colorTheme
.withOpacity(.5)), .textHighEmphasis
), .withOpacity(.5)),
], ),
), ],
), ),
), ),
); ),
}, );
); },
}, );
), },
), ),
) )
: FutureBuilder<bool>( : FutureBuilder<bool>(
future: channel!.initialized, future: channel!.initialized,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.data == true) { if (snapshot.data == true) {
return MessageListView(); return StreamMessageListView();
} }
return Center( return Center(
@@ -384,7 +400,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
}, },
), ),
), ),
MessageInput( StreamMessageInput(
focusNode: _messageInputFocusNode, focusNode: _messageInputFocusNode,
preMessageSending: (message) async { preMessageSending: (message) async {
await channel!.watch(); await channel!.watch();
@@ -23,6 +23,20 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
Timer? _debounce; Timer? _debounce;
late final userListController = StreamUserListController(
client: StreamChat.of(context).client,
sort: [
SortOption(
'name',
direction: 1,
),
],
limit: 25,
filter: Filter.and([
Filter.notEqual('id', StreamChat.of(context).currentUser!.id),
]),
);
void _userNameListener() { void _userNameListener() {
if (_debounce?.isActive ?? false) _debounce!.cancel(); if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () { _debounce = Timer(const Duration(milliseconds: 350), () {
@@ -31,6 +45,12 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
_userNameQuery = _controller!.text; _userNameQuery = _controller!.text;
_isSearchActive = _userNameQuery.isNotEmpty; _isSearchActive = _userNameQuery.isNotEmpty;
}); });
userListController.filter = Filter.and([
if (_userNameQuery.isNotEmpty)
Filter.autoComplete('name', _userNameQuery),
Filter.notEqual('id', StreamChat.of(context).currentUser!.id),
]);
userListController.doInitialLoad();
} }
}); });
} }
@@ -46,6 +66,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
_controller?.clear(); _controller?.clear();
_controller?.removeListener(_userNameListener); _controller?.removeListener(_userNameListener);
_controller?.dispose(); _controller?.dispose();
userListController.dispose();
super.dispose(); super.dispose();
} }
@@ -88,7 +109,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
) )
], ],
), ),
body: ConnectionStatusBuilder( body: StreamConnectionStatusBuilder(
statusBuilder: (context, status) { statusBuilder: (context, status) {
String statusString = ''; String statusString = '';
bool showStatus = true; bool showStatus = true;
@@ -105,7 +126,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
statusString = AppLocalizations.of(context).disconnected; statusString = AppLocalizations.of(context).disconnected;
break; break;
} }
return InfoTile( return StreamInfoTile(
showMessage: showStatus, showMessage: showStatus,
tileAnchor: Alignment.topCenter, tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter,
@@ -136,7 +157,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
children: [ children: [
Stack( Stack(
children: [ children: [
UserAvatar( StreamUserAvatar(
onlineIndicatorAlignment: onlineIndicatorAlignment:
Alignment(0.9, 0.9), Alignment(0.9, 0.9),
user: user, user: user,
@@ -228,77 +249,66 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
body: GestureDetector( body: GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(), onPanDown: (_) => FocusScope.of(context).unfocus(),
child: UsersBloc( child: StreamUserListView(
child: UserListView( controller: userListController,
selectedUsers: _selectedUsers, // groupAlphabetically: _isSearchActive ? false : true,
pullToRefresh: false, itemBuilder: (context, items, index, defaultWidget) {
groupAlphabetically: _isSearchActive ? false : true, return defaultWidget.copyWith(
onUserTap: (user, _) { selected: _selectedUsers.contains(items[index]),
if (!_selectedUsers.contains(user)) { );
setState(() { },
_selectedUsers.add(user); onUserTap: (user) {
}); if (!_selectedUsers.contains(user)) {
} else { setState(() {
setState(() { _selectedUsers.add(user);
_selectedUsers.remove(user); });
}); } else {
} setState(() {
}, _selectedUsers.remove(user);
limit: 25, });
filter: Filter.and([ }
if (_userNameQuery.isNotEmpty) },
Filter.autoComplete('name', _userNameQuery), emptyBuilder: (_) {
Filter.notEqual( return LayoutBuilder(
'id', StreamChat.of(context).currentUser!.id), builder: (context, viewportConstraints) {
]), return SingleChildScrollView(
sort: [ physics: AlwaysScrollableScrollPhysics(),
SortOption( child: ConstrainedBox(
'name', constraints: BoxConstraints(
direction: 1, minHeight: viewportConstraints.maxHeight,
), ),
], child: Center(
emptyBuilder: (_) { child: Column(
return LayoutBuilder( children: [
builder: (context, viewportConstraints) { Padding(
return SingleChildScrollView( padding: const EdgeInsets.all(24),
physics: AlwaysScrollableScrollPhysics(), child: StreamSvgIcon.search(
child: ConstrainedBox( size: 96,
constraints: BoxConstraints( color: StreamChatTheme.of(context)
minHeight: viewportConstraints.maxHeight, .colorTheme
), .textLowEmphasis,
child: Center(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(24),
child: StreamSvgIcon.search(
size: 96,
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
), ),
Text( ),
AppLocalizations.of(context) Text(
.noUserMatchesTheseKeywords, AppLocalizations.of(context)
style: StreamChatTheme.of(context) .noUserMatchesTheseKeywords,
.textTheme style: StreamChatTheme.of(context)
.footnote .textTheme
.copyWith( .footnote
color: StreamChatTheme.of(context) .copyWith(
.colorTheme color: StreamChatTheme.of(context)
.textLowEmphasis, .colorTheme
), .textLowEmphasis,
), ),
], ),
), ],
), ),
), ),
); ),
}, );
); },
}, );
), },
), ),
), ),
), ),
@@ -1,60 +1,34 @@
import 'package:example/localizations.dart'; import 'package:example/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
import 'channel_page.dart';
class PinnedMessagesScreen extends StatefulWidget { 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<SortOption>? 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 MessageThemeData messageTheme;
const PinnedMessagesScreen({
required this.messageTheme,
this.sortOptions,
this.paginationParams = const PaginationParams(limit: 20),
this.emptyBuilder,
this.onShowMessage,
});
@override @override
_PinnedMessagesScreenState createState() => _PinnedMessagesScreenState(); State<PinnedMessagesScreen> createState() => _PinnedMessagesScreenState();
} }
class _PinnedMessagesScreenState extends State<PinnedMessagesScreen> { class _PinnedMessagesScreenState extends State<PinnedMessagesScreen> {
Map<String?, VideoPlayerController?> controllerCache = {}; late final controller = StreamMessageSearchListController(
client: StreamChat.of(context).client,
@override filter: Filter.in_(
void initState() { 'cid',
super.initState(); [StreamChannel.of(context).channel.cid!],
final messageSearchBloc = MessageSearchBloc.of(context); ),
messageSearchBloc.search( messageFilter: Filter.equal(
filter: Filter.in_( 'pinned',
'cid', true,
[StreamChannel.of(context).channel.cid!], ),
sort: [
SortOption(
'created_at',
direction: SortOption.ASC,
), ),
messageFilter: Filter.equal( ],
'pinned', limit: 20,
true, );
),
sort: widget.sortOptions,
pagination: widget.paginationParams,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -73,25 +47,9 @@ class _PinnedMessagesScreenState extends State<PinnedMessagesScreen> {
leading: StreamBackButton(), leading: StreamBackButton(),
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
), ),
body: _buildMediaGrid(), body: StreamMessageSearchListView(
); controller: controller,
} emptyBuilder: (_) {
Widget _buildMediaGrid() {
final messageSearchBloc = MessageSearchBloc.of(context);
return StreamBuilder<List<GetMessageResponse>>(
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( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -140,74 +98,33 @@ class _PinnedMessagesScreenState extends State<PinnedMessagesScreen> {
], ],
), ),
); );
} },
onMessageTap: (messageResponse) async {
var data = snapshot.data ?? []; final client = StreamChat.of(context).client;
final message = messageResponse.message;
return LazyLoadScrollView( final channel = client.channel(
onEndOfPage: () => messageSearchBloc.search( messageResponse.channel!.type,
filter: Filter.in_( id: messageResponse.channel!.id,
'cid', );
[StreamChannel.of(context).channel.cid!], if (channel.state == null) {
await channel.watch();
}
Navigator.pushNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
), ),
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.tightFor(
width: 40.0,
height: 40.0,
),
borderRadius: BorderRadius.circular(28),
),
title: Text(
user.name,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
fontWeight: FontWeight.bold),
),
subtitle: Text(
text != ''
? text
: (attachments.isNotEmpty
? '${attachments.length} ${attachments.length > 1 ? AppLocalizations.of(context).attachments : AppLocalizations.of(context).attachment}'
: ''),
),
onTap: () {
widget.onShowMessage?.call(data[position].message,
StreamChannel.of(context).channel);
},
);
},
itemCount: snapshot.data!.length,
),
);
},
stream: messageSearchBloc.messagesStream,
); );
} }
@override @override
void dispose() { void dispose() {
controller.dispose();
super.dispose(); super.dispose();
for (var c in controllerCache.values) {
c!.dispose();
}
} }
} }
@@ -1,4 +1,4 @@
import 'package:collection/src/iterable_extensions.dart'; import 'package:collection/collection.dart';
import 'package:example/channel_list_page.dart'; import 'package:example/channel_list_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:lottie/lottie.dart'; import 'package:lottie/lottie.dart';
mixin SplashScreenStateMixin<T extends StatefulWidget> on State<T> mixin SplashScreenStateMixin<T extends StatefulWidget> on State<T>
+14 -13
View File
@@ -18,8 +18,15 @@ class ThreadPage extends StatefulWidget {
} }
class _ThreadPageState extends State<ThreadPage> { class _ThreadPageState extends State<ThreadPage> {
Message? _quotedMessage;
FocusNode _focusNode = FocusNode(); FocusNode _focusNode = FocusNode();
late StreamMessageInputController _messageInputController;
@override
void initState() {
super.initState();
_messageInputController =
StreamMessageInputController(message: widget.parent);
}
@override @override
void dispose() { void dispose() {
@@ -28,7 +35,7 @@ class _ThreadPageState extends State<ThreadPage> {
} }
void _reply(Message message) { void _reply(Message message) {
setState(() => _quotedMessage = message); _messageInputController.quotedMessage = message;
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_focusNode.requestFocus(); _focusNode.requestFocus();
}); });
@@ -38,13 +45,13 @@ class _ThreadPageState extends State<ThreadPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: ThreadHeader( appBar: StreamThreadHeader(
parent: widget.parent, parent: widget.parent,
), ),
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: MessageListView( child: StreamMessageListView(
parentMessage: widget.parent, parentMessage: widget.parent,
initialScrollIndex: widget.initialScrollIndex, initialScrollIndex: widget.initialScrollIndex,
initialAlignment: widget.initialAlignment, initialAlignment: widget.initialAlignment,
@@ -56,22 +63,16 @@ class _ThreadPageState extends State<ThreadPage> {
return defaultMessage.copyWith( return defaultMessage.copyWith(
onReplyTap: _reply, onReplyTap: _reply,
deletedBottomRowBuilder: (context, message) { deletedBottomRowBuilder: (context, message) {
return const VisibleFootnote(); return const StreamVisibleFootnote();
}, },
); );
}, },
pinPermissions: ['owner', 'admin', 'member'],
), ),
), ),
if (widget.parent.type != 'deleted') if (widget.parent.type != 'deleted')
MessageInput( StreamMessageInput(
parentMessage: widget.parent,
focusNode: _focusNode, focusNode: _focusNode,
quotedMessage: _quotedMessage, messageInputController: _messageInputController,
onQuotedMessageCleared: () {
setState(() => _quotedMessage = null);
_focusNode.unfocus();
},
), ),
], ],
), ),
@@ -5,85 +5,94 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_page.dart'; import 'channel_page.dart';
class UserMentionsPage extends StatelessWidget { class UserMentionsPage extends StatefulWidget {
@override
State<UserMentionsPage> createState() => _UserMentionsPageState();
}
class _UserMentionsPageState extends State<UserMentionsPage> {
late final controller = StreamMessageSearchListController(
client: StreamChat.of(context).client,
filter: Filter.in_('members', [StreamChat.of(context).currentUser!.id]),
messageFilter: Filter.custom(
operator: r'$contains',
key: 'mentioned_users.id',
value: StreamChat.of(context).currentUser!.id,
),
sort: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
limit: 20,
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = StreamChat.of(context).currentUser!; return StreamMessageSearchListView(
return MessageSearchBloc( controller: controller,
child: MessageSearchListView( emptyBuilder: (_) {
filters: Filter.in_('members', [user.id]), return LayoutBuilder(
messageFilters: Filter.custom( builder: (context, viewportConstraints) {
operator: r'$contains', return SingleChildScrollView(
key: 'mentioned_users.id', physics: AlwaysScrollableScrollPhysics(),
value: user.id, child: ConstrainedBox(
), constraints: BoxConstraints(
sortOptions: [ minHeight: viewportConstraints.maxHeight,
SortOption( ),
'created_at', child: Center(
direction: SortOption.ASC, child: Column(
), children: [
], Padding(
limit: 20, padding: const EdgeInsets.all(24),
showResultCount: false, child: StreamSvgIcon.mentions(
emptyBuilder: (_) { size: 96,
return LayoutBuilder( color:
builder: (context, viewportConstraints) { StreamChatTheme.of(context).colorTheme.disabled,
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(24),
child: StreamSvgIcon.mentions(
size: 96,
color:
StreamChatTheme.of(context).colorTheme.disabled,
),
), ),
Text( ),
AppLocalizations.of(context).noMentionsExistYet, Text(
style: StreamChatTheme.of(context) AppLocalizations.of(context).noMentionsExistYet,
.textTheme style:
.body StreamChatTheme.of(context).textTheme.body.copyWith(
.copyWith( color: StreamChatTheme.of(context)
color: StreamChatTheme.of(context) .colorTheme
.colorTheme .textLowEmphasis,
.textLowEmphasis, ),
), ),
), ],
],
),
), ),
), ),
); ),
}, );
); },
}, );
onItemTap: (messageResponse) async { },
final client = StreamChat.of(context).client; onMessageTap: (messageResponse) async {
final message = messageResponse.message; final client = StreamChat.of(context).client;
final channel = client.channel( final message = messageResponse.message;
messageResponse.channel!.type, final channel = client.channel(
id: messageResponse.channel!.id, messageResponse.channel!.type,
); id: messageResponse.channel!.id,
if (channel.state == null) { );
await channel.watch(); if (channel.state == null) {
} await channel.watch();
Navigator.pushNamed( }
context, Navigator.pushNamed(
Routes.CHANNEL_PAGE, context,
arguments: ChannelPageArgs( Routes.CHANNEL_PAGE,
channel: channel, arguments: ChannelPageArgs(
initialMessage: message, channel: channel,
), initialMessage: message,
); ),
}, );
), },
); );
} }
@override
void dispose() {
controller.dispose();
super.dispose();
}
} }
+6 -6
View File
@@ -13,17 +13,17 @@ dependencies:
stream_chat_flutter: stream_chat_flutter:
git: git:
url: https://github.com/GetStream/stream-chat-flutter.git url: https://github.com/GetStream/stream-chat-flutter.git
ref: develop ref: v4
path: packages/stream_chat_flutter path: packages/stream_chat_flutter
stream_chat_persistence: stream_chat_persistence:
git: git:
url: https://github.com/GetStream/stream-chat-flutter.git url: https://github.com/GetStream/stream-chat-flutter.git
ref: develop ref: v4
path: packages/stream_chat_persistence path: packages/stream_chat_persistence
stream_chat_localizations: stream_chat_localizations:
git: git:
url: https://github.com/GetStream/stream-chat-flutter.git url: https://github.com/GetStream/stream-chat-flutter.git
ref: develop ref: v4
path: packages/stream_chat_localizations path: packages/stream_chat_localizations
flutter_local_notifications: ^9.0.0 flutter_local_notifications: ^9.0.0
flutter_svg: ^1.0.3 flutter_svg: ^1.0.3
@@ -43,17 +43,17 @@ dependency_overrides:
stream_chat: stream_chat:
git: git:
url: https://github.com/GetStream/stream-chat-flutter.git url: https://github.com/GetStream/stream-chat-flutter.git
ref: develop ref: v4
path: packages/stream_chat path: packages/stream_chat
stream_chat_flutter_core: stream_chat_flutter_core:
git: git:
url: https://github.com/GetStream/stream-chat-flutter.git url: https://github.com/GetStream/stream-chat-flutter.git
ref: develop ref: v4
path: packages/stream_chat_flutter_core path: packages/stream_chat_flutter_core
stream_chat_flutter: stream_chat_flutter:
git: git:
url: https://github.com/GetStream/stream-chat-flutter.git url: https://github.com/GetStream/stream-chat-flutter.git
ref: develop ref: v4
path: packages/stream_chat_flutter path: packages/stream_chat_flutter
flutter: flutter: