refactor: use gorouter for navigation

This commit is contained in:
Gordon Hayes
2022-10-17 15:01:03 +02:00
parent 852bb68f1b
commit bea73ec6a1
27 changed files with 922 additions and 915 deletions
-1
View File
@@ -32,7 +32,6 @@
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
@@ -251,6 +251,7 @@
"${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework",
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
"${BUILT_PRODUCTS_DIR}/file_selector_ios/file_selector_ios.framework",
"${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework",
"${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework",
"${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework",
@@ -282,6 +283,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_selector_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework",
+93 -74
View File
@@ -1,28 +1,31 @@
import 'dart:async';
import 'package:example/state/init_data.dart';
import 'package:example/pages/choose_user_page.dart';
import 'package:example/pages/home_page.dart';
import 'package:example/utils/localizations.dart';
import 'package:example/pages/splash_screen.dart';
import 'package:example/routes/app_routes.dart';
import 'package:example/routes/routes.dart';
import 'package:example/utils/app_config.dart';
import 'package:example/utils/local_notification_observer.dart';
import 'package:example/utils/localizations.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_localizations/stream_chat_localizations.dart';
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
import 'routes/app_routes.dart';
import 'routes/routes.dart';
final chatPersistentClient = StreamChatPersistenceClient(
logLevel: Level.SEVERE,
connectionMode: ConnectionMode.regular,
);
void sampleAppLogHandler(LogRecord record) async {
void _sampleAppLogHandler(LogRecord record) async {
if (kDebugMode) StreamChatClient.defaultLogHandler(record);
// report errors to setnry.io
@@ -34,14 +37,17 @@ void sampleAppLogHandler(LogRecord record) async {
}
}
StreamChatClient buildStreamChatClient(
String apiKey, {
Level logLevel = Level.INFO,
}) {
StreamChatClient buildStreamChatClient(String apiKey) {
late Level logLevel;
if (kDebugMode) {
logLevel = Level.INFO;
} else {
logLevel = Level.SEVERE;
}
return StreamChatClient(
apiKey,
logLevel: logLevel,
logHandlerFunction: sampleAppLogHandler,
logHandlerFunction: _sampleAppLogHandler,
)..chatPersistenceClient = chatPersistentClient;
}
@@ -54,7 +60,7 @@ class StreamChatSampleApp extends StatefulWidget {
class _StreamChatSampleAppState extends State<StreamChatSampleApp>
with SplashScreenStateMixin, TickerProviderStateMixin {
InitData? _initData;
final InitNotifier _initNotifier = InitNotifier();
Future<InitData> _initConnection() async {
String? apiKey, userId, token;
@@ -66,7 +72,7 @@ class _StreamChatSampleAppState extends State<StreamChatSampleApp>
token = await secureStorage.read(key: kStreamToken);
}
final client = buildStreamChatClient(apiKey ?? kStreamApiKey);
final client = buildStreamChatClient(apiKey ?? kDefaultStreamApiKey);
if (userId != null && token != null) {
await client.connectUser(
@@ -87,7 +93,7 @@ class _StreamChatSampleAppState extends State<StreamChatSampleApp>
_initConnection().then(
(initData) {
setState(() {
_initData = initData;
_initNotifier.initData = initData;
});
final now = DateTime.now().millisecondsSinceEpoch;
@@ -107,67 +113,87 @@ class _StreamChatSampleAppState extends State<StreamChatSampleApp>
super.initState();
}
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
LocalNotificationObserver? localNotificationObserver;
/// Conditionally sets up the router and adding an observer for the
/// current chat client.
GoRouter _setupRouter() {
if (localNotificationObserver != null) {
localNotificationObserver!.dispose();
}
localNotificationObserver = LocalNotificationObserver(
_initNotifier.initData!.client, _navigatorKey);
return GoRouter(
refreshListenable: _initNotifier,
initialLocation: Routes.CHANNEL_LIST_PAGE.path,
navigatorKey: _navigatorKey,
observers: [localNotificationObserver!],
redirect: (context, state) {
final loggedIn =
_initNotifier.initData?.client.state.currentUser != null;
final loggingIn = state.subloc == Routes.CHOOSE_USER.path ||
state.subloc == Routes.ADVANCED_OPTIONS.path;
if (!loggedIn) {
return loggingIn ? null : Routes.CHOOSE_USER.path;
}
// if the user is logged in but still on the login page, send them to
// the home page
if (loggedIn && state.subloc == Routes.CHOOSE_USER.path) {
return Routes.CHANNEL_LIST_PAGE.path;
}
return null;
},
routes: appRoutes,
);
}
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
if (_initData != null)
PreferenceBuilder<int>(
preference: _initData!.preferences.getInt(
'theme',
defaultValue: 0,
),
builder: (context, snapshot) => MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: {
-1: ThemeMode.dark,
0: ThemeMode.system,
1: ThemeMode.light,
}[snapshot],
supportedLocales: const [
Locale('en'),
Locale('it'),
],
localizationsDelegates: const [
AppLocalizationsDelegate(),
GlobalStreamChatLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
builder: (context, child) => StreamChatConfiguration(
data: StreamChatConfigurationData(),
child: StreamChatTheme(
data: StreamChatThemeData(
brightness: Theme.of(context).brightness,
if (_initNotifier.initData != null)
ChangeNotifierProvider.value(
value: _initNotifier,
builder: (context, child) => Builder(
builder: (context) {
context.watch<InitNotifier>(); // rebuild on change
return PreferenceBuilder<int>(
preference: _initNotifier.initData!.preferences.getInt(
'theme',
defaultValue: 0,
),
child: child!,
),
),
onGenerateRoute: AppRoutes.generateRoute,
onGenerateInitialRoutes: (initialRouteName) {
if (initialRouteName == Routes.HOME) {
return [
AppRoutes.generateRoute(
RouteSettings(
name: Routes.HOME,
arguments: HomePageArgs(_initData!.client),
),
)!
];
}
return [
AppRoutes.generateRoute(
const RouteSettings(
name: Routes.CHOOSE_USER,
builder: (context, snapshot) => MaterialApp.router(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: const {
-1: ThemeMode.dark,
0: ThemeMode.system,
1: ThemeMode.light,
}[snapshot],
supportedLocales: const [
Locale('en'),
Locale('it'),
],
localizationsDelegates: const [
AppLocalizationsDelegate(),
GlobalStreamChatLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
builder: (context, child) => StreamChat(
client: _initNotifier.initData!.client,
child: child,
),
)!
];
routerConfig: _setupRouter(),
),
);
},
initialRoute: _initData!.client.state.currentUser == null
? Routes.CHOOSE_USER
: Routes.HOME,
),
),
if (!animationCompleted) buildAnimation(),
@@ -175,10 +201,3 @@ class _StreamChatSampleAppState extends State<StreamChatSampleApp>
);
}
}
class InitData {
final StreamChatClient client;
final StreamingSharedPreferences preferences;
InitData(this.client, this.preferences);
}
@@ -1,13 +1,15 @@
import 'package:example/app.dart';
import 'package:example/pages/home_page.dart';
import 'package:example/state/init_data.dart';
import 'package:example/utils/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:example/widgets/stream_version.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'choose_user_page.dart';
import 'package:example/pages/choose_user_page.dart';
class AdvancedOptionsPage extends StatefulWidget {
const AdvancedOptionsPage({super.key});
@@ -41,6 +43,86 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
super.dispose();
}
Future<void> _login() async {
if (loading) {
return;
}
if (_formKey.currentState!.validate()) {
final apiKey = _apiKeyController.text;
final userId = _userIdController.text;
final userToken = _userTokenController.text;
final username = _usernameController.text;
loading = true;
showDialog(
barrierDismissible: false,
context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
builder: (context) => Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: StreamChatTheme.of(context).colorTheme.barsBg,
),
height: 100,
width: 100,
child: const Center(
child: CircularProgressIndicator(),
),
),
),
);
final client = buildStreamChatClient(apiKey);
final router = GoRouter.of(context);
final initNotifier = context.read<InitNotifier>();
try {
await client.connectUser(
User(
id: userId,
extraData: {
'name': username,
},
),
userToken,
);
const secureStorage = FlutterSecureStorage();
await Future.wait([
secureStorage.write(
key: kStreamApiKey,
value: apiKey,
),
secureStorage.write(
key: kStreamUserId,
value: userId,
),
secureStorage.write(
key: kStreamToken,
value: userToken,
),
]);
} catch (e) {
debugPrint(e.toString());
var errorText = AppLocalizations.of(context).errorConnecting;
if (e is Map) {
errorText = e['message'] ?? errorText;
}
Navigator.of(context).pop();
setState(() {
_apiKeyError = errorText.toUpperCase();
});
loading = false;
return;
}
loading = false;
initNotifier.initData = initNotifier.initData!.copyWith(client: client);
router.goNamed(Routes.CHOOSE_USER.name);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -260,6 +342,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
),
),
),
onPressed: _login,
child: Text(
AppLocalizations.of(context).login,
style: TextStyle(
@@ -271,84 +354,6 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
: Colors.white,
),
),
onPressed: () async {
if (loading) {
return;
}
if (_formKey.currentState!.validate()) {
final apiKey = _apiKeyController.text;
final userId = _userIdController.text;
final userToken = _userTokenController.text;
final username = _usernameController.text;
loading = true;
showDialog(
barrierDismissible: false,
context: context,
barrierColor:
StreamChatTheme.of(context).colorTheme.overlay,
builder: (context) => Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: StreamChatTheme.of(context)
.colorTheme
.barsBg,
),
height: 100,
width: 100,
child: const Center(
child: CircularProgressIndicator(),
),
),
),
);
final client = buildStreamChatClient(apiKey);
final navigator = Navigator.of(context);
try {
await client.connectUser(
User(id: userId, extraData: {
'name': username,
}),
userToken,
);
const secureStorage = FlutterSecureStorage();
secureStorage.write(
key: kStreamApiKey,
value: apiKey,
);
secureStorage.write(
key: kStreamUserId,
value: userId,
);
secureStorage.write(
key: kStreamToken,
value: userToken,
);
} catch (e) {
var errorText =
AppLocalizations.of(context).errorConnecting;
if (e is Map) {
errorText = e['message'] ?? errorText;
}
Navigator.pop(context);
setState(() {
_apiKeyError = errorText.toUpperCase();
});
loading = false;
return;
}
loading = false;
await navigator.pushNamedAndRemoveUntil(
Routes.HOME,
ModalRoute.withName(Routes.HOME),
arguments: HomePageArgs(client),
);
}
},
),
const StreamVersion(),
],
@@ -1,17 +1,21 @@
import 'dart:async';
import 'package:example/utils/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:example/app.dart';
import 'package:example/state/init_data.dart';
import 'package:example/pages/user_mentions_page.dart';
import 'package:example/routes/routes.dart';
import 'package:example/utils/app_config.dart';
import 'package:example/utils/localizations.dart';
import 'package:example/widgets/channel_list.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
import 'package:example/widgets/channel_list.dart';
class ChannelListPage extends StatefulWidget {
const ChannelListPage({
Key? key,
@@ -71,12 +75,10 @@ class _ChannelListPageState extends State<ChannelListPage> {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: StreamChannelListHeader(
onNewChatButtonTap: () {
Navigator.pushNamed(context, Routes.NEW_CHAT);
},
preNavigationCallback: () {
FocusScope.of(context).requestFocus(FocusNode());
},
onNewChatButtonTap: () =>
GoRouter.of(context).pushNamed(Routes.NEW_CHAT.name),
preNavigationCallback: () =>
FocusScope.of(context).requestFocus(FocusNode()),
),
drawer: LeftDrawer(
user: user,
@@ -188,10 +190,8 @@ class LeftDrawer extends StatelessWidget {
.withOpacity(.5),
),
onTap: () {
Navigator.popAndPushNamed(
context,
Routes.NEW_CHAT,
);
Navigator.of(context).pop();
GoRouter.of(context).pushNamed(Routes.NEW_CHAT.name);
},
title: Text(
AppLocalizations.of(context).newDirectMessage,
@@ -208,10 +208,8 @@ class LeftDrawer extends StatelessWidget {
.withOpacity(.5),
),
onTap: () {
Navigator.popAndPushNamed(
context,
Routes.NEW_GROUP_CHAT,
);
Navigator.of(context).pop();
GoRouter.of(context).pushNamed(Routes.NEW_GROUP_CHAT.name);
},
title: Text(
AppLocalizations.of(context).newGroup,
@@ -226,22 +224,21 @@ class LeftDrawer extends StatelessWidget {
child: ListTile(
onTap: () async {
final client = StreamChat.of(context).client;
final navigator =
Navigator.of(context, rootNavigator: true);
Navigator.pop(context);
final router = GoRouter.of(context);
final initNotifier = context.read<InitNotifier>();
if (!kIsWeb) {
const secureStorage = FlutterSecureStorage();
await secureStorage.deleteAll();
}
client.disconnectUser();
await client.disconnectUser(flushChatPersistence: true);
await client.dispose();
initNotifier.initData = initNotifier.initData!.copyWith(
client:
buildStreamChatClient(kDefaultStreamApiKey));
await navigator.pushNamedAndRemoveUntil(
Routes.CHOOSE_USER,
ModalRoute.withName(Routes.CHOOSE_USER),
);
router.goNamed(Routes.CHOOSE_USER.name);
},
leading: StreamSvgIcon.user(
color: StreamChatTheme.of(context)
@@ -1,10 +1,10 @@
import 'package:example/utils/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
import 'channel_page.dart';
import '../routes/routes.dart';
class ChannelMediaDisplayScreen extends StatefulWidget {
final StreamMessageThemeData messageTheme;
@@ -156,23 +156,16 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
startIndex: position,
userName: media[position].message.user!.name,
onShowMessage: (m, c) async {
final client =
StreamChat.of(context).client;
final navigator = Navigator.of(context);
final message = m;
final channel = client.channel(
c.type,
id: c.id,
);
final router = GoRouter.of(context);
if (channel.state == null) {
await channel.watch();
}
navigator.pushNamed(
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
router.pushNamed(
Routes.CHANNEL_PAGE.name,
params:
Routes.CHANNEL_PAGE.params(channel),
queryParams:
Routes.CHANNEL_PAGE.queryParams(m),
);
},
),
@@ -2,21 +2,9 @@ import 'package:collection/collection.dart';
import 'package:example/routes/routes.dart';
import 'package:example/pages/thread_page.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'chat_info_screen.dart';
import 'group_info_screen.dart';
class ChannelPageArgs {
final Channel? channel;
final Message? initialMessage;
const ChannelPageArgs({
this.channel,
this.initialMessage,
});
}
class ChannelPage extends StatefulWidget {
final int? initialScrollIndex;
final double? initialAlignment;
@@ -63,9 +51,10 @@ class _ChannelPageState extends State<ChannelPage> {
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: StreamChannelHeader(
showTypingIndicator: false,
onBackPressed: () => GoRouter.of(context).pop(),
onImageTap: () async {
final channel = StreamChannel.of(context).channel;
final navigator = Navigator.of(context);
final router = GoRouter.of(context);
if (channel.memberCount == 2 && channel.isDistinct) {
final currentUser = StreamChat.of(context).currentUser;
@@ -73,34 +62,16 @@ class _ChannelPageState extends State<ChannelPage> {
(element) => element.user!.id != currentUser!.id,
);
if (otherUser != null) {
final pop = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: ChatInfoScreen(
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
user: otherUser.user,
),
),
),
router.pushNamed(
Routes.CHAT_INFO_SCREEN.name,
params: Routes.CHAT_INFO_SCREEN.params(channel),
extra: otherUser.user,
);
if (pop == true) {
navigator.pop();
}
}
} else {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: GroupInfoScreen(
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
),
),
),
GoRouter.of(context).pushNamed(
Routes.GROUP_INFO_SCREEN.name,
params: Routes.GROUP_INFO_SCREEN.params(channel),
);
}
},
@@ -117,7 +88,7 @@ class _ChannelPageState extends State<ChannelPage> {
onMessageSwiped: _reply,
messageFilter: defaultFilter,
messageBuilder: (context, details, messages, defaultMessage) {
final navigator = Navigator.of(context);
final router = GoRouter.of(context);
return defaultMessage.copyWith(
onReplyTap: _reply,
onShowMessage: (m, c) async {
@@ -130,12 +101,10 @@ class _ChannelPageState extends State<ChannelPage> {
if (channel.state == null) {
await channel.watch();
}
navigator.pushReplacementNamed(
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
router.goNamed(
Routes.CHANNEL_PAGE.name,
params: Routes.CHANNEL_PAGE.params(channel),
queryParams: Routes.CHANNEL_PAGE.queryParams(message),
);
},
deletedBottomRowBuilder: (context, message) {
@@ -1,12 +1,13 @@
import 'package:example/app.dart';
import 'package:example/state/init_data.dart';
import 'package:example/utils/app_config.dart';
import 'package:example/pages/home_page.dart';
import 'package:example/utils/localizations.dart';
import 'package:example/widgets/stream_version.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../routes/routes.dart';
@@ -94,12 +95,10 @@ class ChooseUserPage extends StatelessWidget {
),
);
final client = StreamChatClient(
kDefaultStreamApiKey,
logLevel: Level.INFO,
)..chatPersistenceClient = chatPersistentClient;
final client =
context.read<InitNotifier>().initData!.client;
final navigator = Navigator.of(context);
final router = GoRouter.of(context);
await client.connectUser(
user,
@@ -121,11 +120,7 @@ class ChooseUserPage extends StatelessWidget {
value: token,
);
}
navigator.pushNamedAndRemoveUntil(
Routes.HOME,
ModalRoute.withName(Routes.HOME),
arguments: HomePageArgs(client),
);
router.replaceNamed(Routes.CHANNEL_LIST_PAGE.name);
},
leading: StreamUserAvatar(
user: user,
@@ -157,9 +152,8 @@ class ChooseUserPage extends StatelessWidget {
);
}),
ListTile(
onTap: () {
Navigator.pushNamed(context, Routes.ADVANCED_OPTIONS);
},
onTap: () => GoRouter.of(context)
.pushNamed(Routes.ADVANCED_OPTIONS.name),
leading: CircleAvatar(
backgroundColor:
StreamChatTheme.of(context).colorTheme.borders,
@@ -1,16 +1,17 @@
import 'package:example/state/new_group_chat_state.dart';
import 'package:example/utils/localizations.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_page.dart';
import '../routes/routes.dart';
class GroupChatDetailsScreen extends StatefulWidget {
final List<User>? selectedUsers;
final NewGroupChatState groupChatState;
const GroupChatDetailsScreen({
Key? key,
required this.selectedUsers,
required this.groupChatState,
}) : super(key: key);
@override
@@ -18,14 +19,12 @@ class GroupChatDetailsScreen extends StatefulWidget {
}
class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
late final _selectedUsers = <User>[...?widget.selectedUsers];
late final TextEditingController _groupNameController =
TextEditingController()..addListener(_groupNameListener);
bool _isGroupNameEmpty = true;
int get _totalUsers => _selectedUsers.length;
int get _totalUsers => widget.groupChatState.users.length;
void _groupNameListener() {
final name = _groupNameController.text;
@@ -48,7 +47,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
Navigator.pop(context, _selectedUsers);
GoRouter.of(context).pop();
return false;
},
child: Scaffold(
@@ -123,21 +122,21 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
try {
final groupName = _groupNameController.text;
final client = StreamChat.of(context).client;
final navigator = Navigator.of(context);
final router = GoRouter.of(context);
final channel = client.channel('messaging',
id: const Uuid().v4(),
extraData: {
'members': [
client.state.currentUser!.id,
..._selectedUsers.map((e) => e.id),
...widget.groupChatState.users
.map((e) => e.id),
],
'name': groupName,
});
await channel.watch();
navigator.pushNamedAndRemoveUntil(
Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.CHANNEL_LIST_PAGE),
arguments: ChannelPageArgs(channel: channel),
router.goNamed(
Routes.CHANNEL_PAGE.name,
params: Routes.CHANNEL_PAGE.params(channel),
);
} catch (err) {
_showErrorAlert();
@@ -192,67 +191,73 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
),
),
),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: ListView.separated(
itemCount: _selectedUsers.length + 1,
separatorBuilder: (_, __) => Container(
height: 1,
color: StreamChatTheme.of(context).colorTheme.borders,
),
itemBuilder: (_, index) {
if (index == _selectedUsers.length) {
return Container(
height: 1,
color: StreamChatTheme.of(context)
.colorTheme
.borders,
);
}
final user = _selectedUsers[index];
return ListTile(
key: ObjectKey(user),
leading: StreamUserAvatar(
user: user,
constraints: const BoxConstraints.tightFor(
width: 40,
height: 40,
),
),
title: Text(
user.name,
style:
const TextStyle(fontWeight: FontWeight.bold),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
trailing: IconButton(
icon: Icon(
Icons.clear_rounded,
AnimatedBuilder(
animation: widget.groupChatState,
builder: (context, child) {
return Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: ListView.separated(
itemCount: widget.groupChatState.users.length + 1,
separatorBuilder: (_, __) => Container(
height: 1,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
.borders,
),
padding: const EdgeInsets.all(0),
splashRadius: 24,
onPressed: () {
setState(() {
_selectedUsers.remove(user);
});
if (_selectedUsers.isEmpty) {
Navigator.pop(context, _selectedUsers);
itemBuilder: (_, index) {
if (index ==
widget.groupChatState.users.length) {
return Container(
height: 1,
color: StreamChatTheme.of(context)
.colorTheme
.borders,
);
}
final user = widget.groupChatState.users
.elementAt(index);
return ListTile(
key: ObjectKey(user),
leading: StreamUserAvatar(
user: user,
constraints: const BoxConstraints.tightFor(
width: 40,
height: 40,
),
),
title: Text(
user.name,
style: const TextStyle(
fontWeight: FontWeight.bold),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
trailing: IconButton(
icon: Icon(
Icons.clear_rounded,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
),
padding: const EdgeInsets.all(0),
splashRadius: 24,
onPressed: () {
widget.groupChatState.removeUser(user);
if (widget.groupChatState.users.isEmpty) {
GoRouter.of(context).pop();
}
},
),
);
},
),
);
},
),
),
),
),
);
}),
],
),
);
@@ -308,6 +313,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: GoRouter.of(context).pop,
child: Text(
AppLocalizations.of(context).ok,
style: StreamChatTheme.of(context)
@@ -318,9 +324,6 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
.colorTheme
.accentPrimary),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
@@ -2,14 +2,14 @@ import 'dart:async';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:example/pages/channel_file_display_screen.dart';
import 'package:example/routes/routes.dart';
import 'package:example/utils/localizations.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_media_display_screen.dart';
import 'channel_page.dart';
import 'chat_info_screen.dart';
import 'pinned_messages_screen.dart';
class GroupInfoScreen extends StatefulWidget {
@@ -647,7 +647,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
onTap: () async {
final streamChannel = StreamChannel.of(context);
final streamChat = StreamChat.of(context);
final navigator = Navigator.of(context);
final router = GoRouter.of(context);
final res = await showConfirmationBottomSheet(
context,
title: AppLocalizations.of(context).leaveConversation,
@@ -662,7 +662,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
if (res == true) {
final channel = streamChannel.channel;
await channel.removeMembers([streamChat.currentUser!.id]);
navigator.pop();
router.pop();
}
},
),
@@ -865,7 +865,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
AppLocalizations.of(context).viewInfo,
() async {
final client = StreamChat.of(context).client;
final navigator = Navigator.of(context);
final router = GoRouter.of(context);
final c = client.channel('messaging', extraData: {
'members': [
@@ -876,16 +876,10 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
await c.watch();
await navigator.push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: c,
child: ChatInfoScreen(
messageTheme: widget.messageTheme,
user: user,
),
),
),
router.pushNamed(
Routes.CHAT_INFO_SCREEN.name,
params: Routes.CHAT_INFO_SCREEN.params(c),
extra: user,
);
},
),
@@ -901,7 +895,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
AppLocalizations.of(context).message,
() async {
final client = StreamChat.of(context).client;
final navigator = Navigator.of(context);
final router = GoRouter.of(context);
final c = client.channel('messaging', extraData: {
'members': [
@@ -912,13 +906,9 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
await c.watch();
await navigator.push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: c,
child: const ChannelPage(),
),
),
router.pushNamed(
Routes.CHANNEL_PAGE.name,
params: Routes.CHANNEL_PAGE.params(c),
);
},
),
@@ -934,7 +924,7 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
size: 24.0,
),
AppLocalizations.of(context).removeFromGroup, () async {
final navigator = Navigator.of(context);
final router = GoRouter.of(context);
final res = await showConfirmationBottomSheet(
context,
title: AppLocalizations.of(context).removeMember,
@@ -949,21 +939,23 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
if (res == true) {
await channel.removeMembers([user.id]);
}
navigator.pop();
router.pop();
},
color:
StreamChatTheme.of(context).colorTheme.accentError),
_buildModalListTile(
context,
StreamSvgIcon.closeSmall(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
size: 24.0,
),
AppLocalizations.of(context).cancel, () {
Navigator.pop(context);
}),
context,
StreamSvgIcon.closeSmall(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
size: 24.0,
),
AppLocalizations.of(context).cancel,
() {
Navigator.pop(context);
},
),
],
),
),
@@ -1,115 +0,0 @@
import 'dart:async';
import 'package:example/pages/channel_page.dart';
import 'package:example/utils/notifications_service.dart';
import 'package:example/routes/app_routes.dart';
import 'package:example/routes/routes.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class MyObserver extends NavigatorObserver {
Route? currentRoute;
late final StreamSubscription _subscription;
MyObserver(
StreamChatClient client,
GlobalKey<NavigatorState> navigatorKey,
) {
_subscription = client
.on(
EventType.messageNew,
EventType.notificationMessageNew,
)
.listen((event) {
if (event.message?.user?.id == client.state.currentUser?.id) {
return;
}
final channelId = event.channelId;
if (currentRoute?.settings.name == Routes.CHANNEL_PAGE) {
final args = currentRoute?.settings.arguments as ChannelPageArgs;
if (args.channel?.id == channelId) {
return;
}
}
showLocalNotification(
event,
client.state.currentUser!.id,
navigatorKey.currentState!.context,
);
});
}
@override
void didPop(Route route, Route? previousRoute) {
currentRoute = route;
}
@override
void didPush(Route route, Route? previousRoute) {
currentRoute = route;
}
@override
void didRemove(Route route, Route? previousRoute) {
currentRoute = route;
}
@override
void didReplace({Route? newRoute, Route? oldRoute}) {
currentRoute = newRoute;
}
void dispose() {
_subscription.cancel();
}
}
class HomePageArgs {
final StreamChatClient chatClient;
HomePageArgs(this.chatClient);
}
class HomePage extends StatefulWidget {
const HomePage({
Key? key,
required this.chatClient,
}) : super(key: key);
final StreamChatClient chatClient;
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
MyObserver? _observer;
@override
Widget build(BuildContext context) {
return StreamChat(
client: widget.chatClient,
child: WillPopScope(
onWillPop: () async {
final canPop = await _navigatorKey.currentState?.maybePop() ?? false;
return !canPop;
},
child: Navigator(
key: _navigatorKey,
onGenerateRoute: AppRoutes.generateRoute,
initialRoute: Routes.CHANNEL_LIST_PAGE,
observers: [_observer!],
),
),
);
}
@override
void didChangeDependencies() {
_observer?.dispose();
_observer = MyObserver(widget.chatClient, _navigatorKey);
super.didChangeDependencies();
}
}
@@ -2,9 +2,9 @@ import 'dart:async';
import 'package:example/utils/localizations.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_page.dart';
import '../widgets/chips_input_text_field.dart';
import '../routes/routes.dart';
@@ -61,13 +61,14 @@ class _NewChatScreenState extends State<NewChatScreen> {
_userNameQuery = _controller.text;
_isSearchActive = _userNameQuery.isNotEmpty;
});
userListController.filter = Filter.and([
if (_userNameQuery.isNotEmpty)
Filter.autoComplete('name', _userNameQuery),
Filter.notEqual('id', StreamChat.of(context).currentUser!.id),
]);
userListController.doInitialLoad();
}
userListController.filter = Filter.and([
if (_userNameQuery.isNotEmpty)
Filter.autoComplete('name', _userNameQuery),
Filter.notEqual('id', StreamChat.of(context).currentUser!.id),
]);
userListController.doInitialLoad();
});
}
@@ -248,9 +249,8 @@ class _NewChatScreenState extends State<NewChatScreen> {
if (!_isSearchActive && !_selectedUsers.isNotEmpty)
InkWell(
onTap: () {
Navigator.pushNamed(
context,
Routes.NEW_GROUP_CHAT,
GoRouter.of(context).pushNamed(
Routes.NEW_GROUP_CHAT.name,
);
},
child: Padding(
@@ -311,8 +311,6 @@ class _NewChatScreenState extends State<NewChatScreen> {
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: StreamUserListView(
controller: userListController,
// groupAlphabetically:
// _isSearchActive ? false : true,
onUserTap: (user) {
_controller.clear();
if (!_selectedUsers.contains(user)) {
@@ -409,11 +407,9 @@ class _NewChatScreenState extends State<NewChatScreen> {
return message;
},
onMessageSent: (m) {
Navigator.pushNamedAndRemoveUntil(
context,
Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.CHANNEL_LIST_PAGE),
arguments: ChannelPageArgs(channel: channel),
GoRouter.of(context).goNamed(
Routes.CHANNEL_PAGE.name,
params: Routes.CHANNEL_PAGE.params(channel!),
);
},
),
@@ -1,7 +1,9 @@
import 'dart:async';
import 'package:example/state/new_group_chat_state.dart';
import 'package:example/utils/localizations.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../routes/routes.dart';
@@ -20,7 +22,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
String _userNameQuery = '';
final _selectedUsers = <User>{};
final groupChatState = NewGroupChatState();
bool _isSearchActive = false;
@@ -69,250 +71,240 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: AppBar(
elevation: 1,
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
leading: const StreamBackButton(),
title: Text(
AppLocalizations.of(context).addGroupMembers,
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16,
),
),
centerTitle: true,
actions: [
if (_selectedUsers.isNotEmpty)
IconButton(
icon: StreamSvgIcon.arrowRight(
color: StreamChatTheme.of(context).colorTheme.accentPrimary,
return AnimatedBuilder(
animation: groupChatState,
builder: (context, child) {
final state = groupChatState;
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: AppBar(
elevation: 1,
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
leading: const StreamBackButton(),
title: Text(
AppLocalizations.of(context).addGroupMembers,
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16,
),
onPressed: () async {
final updatedList = await Navigator.pushNamed(
context,
Routes.NEW_GROUP_CHAT_DETAILS,
arguments: _selectedUsers.toList(growable: false),
);
if (updatedList != null) {
setState(() {
_selectedUsers
..clear()
..addAll(updatedList as Iterable<User>);
});
}
},
)
],
),
body: StreamConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
switch (status) {
case ConnectionStatus.connected:
statusString = AppLocalizations.of(context).connected;
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = AppLocalizations.of(context).reconnecting;
break;
case ConnectionStatus.disconnected:
statusString = AppLocalizations.of(context).disconnected;
break;
}
return StreamInfoTile(
showMessage: showStatus,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverToBoxAdapter(
child: SearchTextField(
controller: _controller,
hintText: AppLocalizations.of(context).search,
),
),
centerTitle: true,
actions: [
if (state.users.isNotEmpty)
IconButton(
icon: StreamSvgIcon.arrowRight(
color: StreamChatTheme.of(context).colorTheme.accentPrimary,
),
if (_selectedUsers.isNotEmpty)
SliverToBoxAdapter(
child: SizedBox(
height: 104,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _selectedUsers.length,
padding: const EdgeInsets.all(8),
separatorBuilder: (_, __) =>
const SizedBox(width: 16),
itemBuilder: (_, index) {
final user = _selectedUsers.elementAt(index);
return Column(
children: [
Stack(
children: [
StreamUserAvatar(
onlineIndicatorAlignment:
const Alignment(0.9, 0.9),
user: user,
showOnlineStatus: true,
borderRadius: BorderRadius.circular(32),
constraints:
const BoxConstraints.tightFor(
height: 64,
width: 64,
),
),
Positioned(
top: -4,
right: -4,
child: GestureDetector(
onTap: () {
if (_selectedUsers.contains(user)) {
setState(() =>
_selectedUsers.remove(user));
}
},
child: Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context)
.colorTheme
.appBg,
shape: BoxShape.circle,
border: Border.all(
color: StreamChatTheme.of(context)
.colorTheme
.appBg,
),
),
child: StreamSvgIcon.close(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
size: 24,
),
),
),
)
],
),
const SizedBox(height: 4),
Text(
user.name.split(' ')[0],
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
);
},
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _HeaderDelegate(
height: 32,
child: Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
_isSearchActive
? '${AppLocalizations.of(context).matchesFor} "$_userNameQuery"'
: AppLocalizations.of(context).onThePlatorm,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
),
),
),
),
];
},
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: StreamUserListView(
controller: userListController,
itemBuilder: (context, items, index, defaultWidget) {
return defaultWidget.copyWith(
selected: _selectedUsers.contains(items[index]),
onPressed: () async {
GoRouter.of(context).pushNamed(
Routes.NEW_GROUP_CHAT_DETAILS.name,
extra: state,
);
},
onUserTap: (user) {
if (!_selectedUsers.contains(user)) {
setState(() {
_selectedUsers.add(user);
});
} else {
setState(() {
_selectedUsers.remove(user);
});
}
},
emptyBuilder: (_) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: const 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,
)
],
),
body: StreamConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
switch (status) {
case ConnectionStatus.connected:
statusString = AppLocalizations.of(context).connected;
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = AppLocalizations.of(context).reconnecting;
break;
case ConnectionStatus.disconnected:
statusString = AppLocalizations.of(context).disconnected;
break;
}
return StreamInfoTile(
showMessage: showStatus,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverToBoxAdapter(
child: SearchTextField(
controller: _controller,
hintText: AppLocalizations.of(context).search,
),
),
if (state.users.isNotEmpty)
SliverToBoxAdapter(
child: SizedBox(
height: 104,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: state.users.length,
padding: const EdgeInsets.all(8),
separatorBuilder: (_, __) =>
const SizedBox(width: 16),
itemBuilder: (_, index) {
final user = state.users.elementAt(index);
return Column(
children: [
Stack(
children: [
StreamUserAvatar(
onlineIndicatorAlignment:
const Alignment(0.9, 0.9),
user: user,
showOnlineStatus: true,
borderRadius:
BorderRadius.circular(32),
constraints:
const BoxConstraints.tightFor(
height: 64,
width: 64,
),
),
Positioned(
top: -4,
right: -4,
child: GestureDetector(
onTap: () {
groupChatState.removeUser(user);
},
child: Container(
decoration: BoxDecoration(
color:
StreamChatTheme.of(context)
.colorTheme
.appBg,
shape: BoxShape.circle,
border: Border.all(
color: StreamChatTheme.of(
context)
.colorTheme
.appBg,
),
),
child: StreamSvgIcon.close(
color:
StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
size: 24,
),
),
),
)
],
),
),
Text(
AppLocalizations.of(context)
.noUserMatchesTheseKeywords,
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
const SizedBox(height: 4),
Text(
user.name.split(' ')[0],
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
);
},
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _HeaderDelegate(
height: 32,
child: Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient: StreamChatTheme.of(context)
.colorTheme
.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
_isSearchActive
? '${AppLocalizations.of(context).matchesFor} "$_userNameQuery"'
: AppLocalizations.of(context).onThePlatorm,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
),
),
),
),
];
},
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: StreamUserListView(
controller: userListController,
itemBuilder: (context, items, index, defaultWidget) {
return defaultWidget.copyWith(
selected: state.users.contains(items[index]),
);
},
onUserTap: groupChatState.addOrRemoveUser,
emptyBuilder: (_) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: const 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,
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
],
),
],
),
),
),
),
);
},
);
},
);
},
),
),
),
),
),
);
},
),
);
},
),
);
},
);
}
}
@@ -1,10 +1,9 @@
import 'package:example/utils/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_page.dart';
class PinnedMessagesScreen extends StatefulWidget {
const PinnedMessagesScreen({super.key});
@@ -103,7 +102,7 @@ class _PinnedMessagesScreenState extends State<PinnedMessagesScreen> {
},
onMessageTap: (messageResponse) async {
final client = StreamChat.of(context).client;
final navigator = Navigator.of(context);
final router = GoRouter.of(context);
final message = messageResponse.message;
final channel = client.channel(
messageResponse.channel!.type,
@@ -112,12 +111,10 @@ class _PinnedMessagesScreenState extends State<PinnedMessagesScreen> {
if (channel.state == null) {
await channel.watch();
}
navigator.pushNamed(
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
router.pushNamed(
Routes.CHANNEL_PAGE.name,
params: Routes.CHANNEL_PAGE.params(channel),
queryParams: Routes.CHANNEL_PAGE.queryParams(message),
);
},
),
@@ -1,10 +1,9 @@
import 'package:example/utils/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_page.dart';
class UserMentionsPage extends StatefulWidget {
const UserMentionsPage({super.key});
@@ -72,7 +71,7 @@ class _UserMentionsPageState extends State<UserMentionsPage> {
},
onMessageTap: (messageResponse) async {
final client = StreamChat.of(context).client;
final navigator = Navigator.of(context);
final router = GoRouter.of(context);
final message = messageResponse.message;
final channel = client.channel(
messageResponse.channel!.type,
@@ -81,12 +80,10 @@ class _UserMentionsPageState extends State<UserMentionsPage> {
if (channel.state == null) {
await channel.watch();
}
navigator.pushNamed(
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
router.pushNamed(
Routes.CHANNEL_PAGE.name,
params: Routes.CHANNEL_PAGE.params(channel),
queryParams: Routes.CHANNEL_PAGE.queryParams(message),
);
},
);
+117 -125
View File
@@ -1,135 +1,127 @@
import 'package:collection/collection.dart';
import 'package:example/pages/advanced_options_page.dart';
import 'package:example/pages/channel_list_page.dart';
import 'package:example/pages/channel_page.dart';
import 'package:example/pages/chat_info_screen.dart';
import 'package:example/pages/group_chat_details_screen.dart';
import 'package:example/pages/group_info_screen.dart';
import 'package:example/pages/new_chat_screen.dart';
import 'package:example/pages/new_group_chat_screen.dart';
import 'package:example/pages/thread_page.dart';
import 'package:example/routes/routes.dart';
import 'package:example/state/new_group_chat_state.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../app.dart';
import '../pages/advanced_options_page.dart';
import '../pages/channel_page.dart';
import '../pages/chat_info_screen.dart';
import '../pages/choose_user_page.dart';
import '../pages/group_chat_details_screen.dart';
import '../pages/group_info_screen.dart';
import '../pages/home_page.dart';
import '../pages/new_chat_screen.dart';
import '../pages/new_group_chat_screen.dart';
import '../pages/thread_page.dart';
import 'routes.dart';
class AppRoutes {
/// Add entry for new route here
static Route<dynamic>? generateRoute(RouteSettings settings) {
final args = settings.arguments;
switch (settings.name) {
case Routes.APP:
return MaterialPageRoute(
settings: RouteSettings(arguments: args, name: Routes.APP),
builder: (_) {
return const StreamChatSampleApp();
});
case Routes.HOME:
return MaterialPageRoute(
settings: RouteSettings(arguments: args, name: Routes.HOME),
builder: (_) {
final homePageArgs = args as HomePageArgs;
return HomePage(
chatClient: homePageArgs.chatClient,
);
});
case Routes.CHOOSE_USER:
return MaterialPageRoute(
settings: RouteSettings(arguments: args, name: Routes.CHOOSE_USER),
builder: (_) {
return const ChooseUserPage();
});
case Routes.ADVANCED_OPTIONS:
return MaterialPageRoute(
settings:
RouteSettings(arguments: args, name: Routes.ADVANCED_OPTIONS),
builder: (_) => const AdvancedOptionsPage(),
);
case Routes.CHANNEL_PAGE:
return MaterialPageRoute(
settings: RouteSettings(arguments: args, name: Routes.CHANNEL_PAGE),
builder: (context) {
final channelPageArgs = args as ChannelPageArgs;
final initialMessage = channelPageArgs.initialMessage;
final appRoutes = [
GoRoute(
name: Routes.CHANNEL_LIST_PAGE.name,
path: Routes.CHANNEL_LIST_PAGE.path,
builder: (BuildContext context, GoRouterState state) =>
const ChannelListPage(),
routes: [
GoRoute(
name: Routes.CHANNEL_PAGE.name,
path: Routes.CHANNEL_PAGE.path,
builder: (context, state) {
final channel =
StreamChat.of(context).client.state.channels[state.params['cid']];
final messageId = state.queryParams['mid'];
final parentId = state.queryParams['pid'];
return StreamChannel(
channel: channelPageArgs.channel!,
initialMessageId: initialMessage?.id,
child: Builder(
builder: (context) {
final parentId = initialMessage?.parentId;
Message? parentMessage;
if (parentId != null) {
final channel = StreamChannel.of(context).channel;
parentMessage = channel.state!.messages
.firstWhereOrNull((it) => it.id == parentId);
}
if (parentMessage != null) {
return ThreadPage(parent: parentMessage);
}
return ChannelPage(
highlightInitialMessage:
channelPageArgs.initialMessage != null,
);
},
),
);
},
);
case Routes.NEW_CHAT:
return MaterialPageRoute(
settings: RouteSettings(arguments: args, name: Routes.NEW_CHAT),
builder: (_) {
return const NewChatScreen();
});
case Routes.NEW_GROUP_CHAT:
return MaterialPageRoute(
settings:
RouteSettings(arguments: args, name: Routes.NEW_GROUP_CHAT),
builder: (_) {
return const NewGroupChatScreen();
});
case Routes.NEW_GROUP_CHAT_DETAILS:
return MaterialPageRoute(
settings: RouteSettings(
arguments: args, name: Routes.NEW_GROUP_CHAT_DETAILS),
builder: (_) {
return GroupChatDetailsScreen(
selectedUsers: args as List<User>?,
Message? parentMessage;
if (parentId != null) {
parentMessage = channel?.state!.messages
.firstWhereOrNull((it) => it.id == parentId);
}
return StreamChannel(
channel: channel!,
initialMessageId: messageId,
child: Builder(
builder: (context) {
return (parentMessage != null)
? ThreadPage(parent: parentMessage)
: ChannelPage(
highlightInitialMessage: messageId != null,
);
},
),
);
},
routes: [
GoRoute(
name: Routes.CHAT_INFO_SCREEN.name,
path: Routes.CHAT_INFO_SCREEN.path,
builder: (BuildContext context, GoRouterState state) {
final channel = StreamChat.of(context)
.client
.state
.channels[state.params['cid']];
return StreamChannel(
channel: channel!,
child: ChatInfoScreen(
user: state.extra as User?,
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
),
);
});
case Routes.CHAT_INFO_SCREEN:
return MaterialPageRoute(
settings:
RouteSettings(arguments: args, name: Routes.CHAT_INFO_SCREEN),
builder: (context) {
return ChatInfoScreen(
user: args as User?,
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
},
),
GoRoute(
name: Routes.GROUP_INFO_SCREEN.name,
path: Routes.GROUP_INFO_SCREEN.path,
builder: (BuildContext context, GoRouterState state) {
final channel = StreamChat.of(context)
.client
.state
.channels[state.params['cid']];
return StreamChannel(
channel: channel!,
child: GroupInfoScreen(
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
),
);
});
case Routes.GROUP_INFO_SCREEN:
return MaterialPageRoute(
settings:
RouteSettings(arguments: args, name: Routes.GROUP_INFO_SCREEN),
builder: (context) {
return GroupInfoScreen(
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
);
});
case Routes.CHANNEL_LIST_PAGE:
return MaterialPageRoute(
settings:
RouteSettings(arguments: args, name: Routes.CHANNEL_LIST_PAGE),
builder: (context) {
return const ChannelListPage();
});
// Default case, should not reach here.
default:
return null;
}
}
}
},
),
],
),
],
),
GoRoute(
name: Routes.NEW_CHAT.name,
path: Routes.NEW_CHAT.path,
builder: (BuildContext context, GoRouterState state) {
return const NewChatScreen();
},
),
GoRoute(
name: Routes.NEW_GROUP_CHAT.name,
path: Routes.NEW_GROUP_CHAT.path,
builder: (BuildContext context, GoRouterState state) {
return const NewGroupChatScreen();
},
),
GoRoute(
name: Routes.NEW_GROUP_CHAT_DETAILS.name,
path: Routes.NEW_GROUP_CHAT_DETAILS.path,
builder: (BuildContext context, GoRouterState state) {
final groupChatState = state.extra as NewGroupChatState;
return GroupChatDetailsScreen(groupChatState: groupChatState);
},
),
GoRoute(
name: Routes.CHOOSE_USER.name,
path: Routes.CHOOSE_USER.path,
builder: (BuildContext context, GoRouterState state) =>
const ChooseUserPage(),
),
GoRoute(
name: Routes.ADVANCED_OPTIONS.name,
path: Routes.ADVANCED_OPTIONS.path,
builder: (BuildContext context, GoRouterState state) =>
const AdvancedOptionsPage(),
),
];
+38 -11
View File
@@ -1,16 +1,43 @@
// ignore_for_file: constant_identifier_names
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Application routes
abstract class Routes {
static const String APP = '/app';
static const String HOME = '/home';
static const String CHOOSE_USER = '/choose_user';
static const String ADVANCED_OPTIONS = '/advance_options';
static const String CHANNEL_PAGE = '/channel_page';
static const String NEW_CHAT = '/new_chat';
static const String NEW_GROUP_CHAT = '/new_group_chat';
static const String NEW_GROUP_CHAT_DETAILS = '/new_group_chat_details';
static const String CHAT_INFO_SCREEN = '/chat_info_screen';
static const String GROUP_INFO_SCREEN = '/group_info_screen';
static const String CHANNEL_LIST_PAGE = '/channel_list_page';
static const RouteConfig CHOOSE_USER =
RouteConfig(name: 'choose_user', path: '/users');
static const RouteConfig ADVANCED_OPTIONS =
RouteConfig(name: 'advanced_options', path: '/options');
static const ChannelRouteConfig CHANNEL_PAGE =
ChannelRouteConfig(name: 'channel_page', path: 'channel/:cid');
static const RouteConfig NEW_CHAT =
RouteConfig(name: 'new_chat', path: '/new_chat');
static const RouteConfig NEW_GROUP_CHAT =
RouteConfig(name: 'new_group_chat', path: '/new_group_chat');
static const RouteConfig NEW_GROUP_CHAT_DETAILS = RouteConfig(
name: 'new_group_chat_details', path: '/new_group_chat_details');
static const ChannelRouteConfig CHAT_INFO_SCREEN =
ChannelRouteConfig(name: 'chat_info_screen', path: 'chat_info_screen');
static const ChannelRouteConfig GROUP_INFO_SCREEN =
ChannelRouteConfig(name: 'group_info_screen', path: 'group_info_screen');
static const RouteConfig CHANNEL_LIST_PAGE =
RouteConfig(name: 'channel_list_page', path: '/channels');
}
class RouteConfig {
final String name;
final String path;
const RouteConfig({required this.name, required this.path});
}
class ChannelRouteConfig extends RouteConfig {
const ChannelRouteConfig({required super.name, required super.path});
Map<String, String> params(Channel channel) => {'cid': channel.cid!};
Map<String, String> queryParams(Message message) => {
'mid': message.id,
if (message.parentId != null) 'pid': message.parentId!
};
}
@@ -0,0 +1,36 @@
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
/// {@template init_notifier}
/// [ChangeNotifier] to store [InitData] and notify listeners on change.
/// {@endtemplate}
class InitNotifier extends ChangeNotifier {
/// {@macro init_notifier}
InitNotifier();
InitData? _initData;
set initData(InitData? data) {
_initData = data;
notifyListeners();
}
InitData? get initData => _initData;
}
/// {@template init_data}
/// Manages the initialization data for the sample application.
///
/// Stores a reference to the current [StreamChatClient].
/// {@endtemplate}
class InitData {
/// {@macro init_data}
InitData(this.client, this.preferences);
final StreamChatClient client;
final StreamingSharedPreferences preferences;
InitData copyWith({required StreamChatClient client}) =>
InitData(client, preferences);
}
@@ -0,0 +1,29 @@
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class NewGroupChatState extends ChangeNotifier {
final users = <User>{};
void addUser(User user) {
if (!users.contains(user)) {
users.add(user);
notifyListeners();
}
}
void removeUser(User user) {
if (users.contains(user)) {
users.remove(user);
notifyListeners();
}
}
void addOrRemoveUser(User user) {
if (users.contains(user)) {
users.remove(user);
} else {
users.add(user);
}
notifyListeners();
}
}
@@ -0,0 +1,69 @@
import 'dart:async';
import 'package:example/routes/routes.dart';
import 'package:example/utils/notifications_service.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class LocalNotificationObserver extends NavigatorObserver {
Route? currentRoute;
late final StreamSubscription _subscription;
LocalNotificationObserver(
StreamChatClient client,
GlobalKey<NavigatorState> navigatorKey,
) {
_subscription = client
.on(
EventType.messageNew,
EventType.notificationMessageNew,
)
.listen((event) {
_handleEvent(event, client, navigatorKey);
});
}
void _handleEvent(Event event, StreamChatClient client,
GlobalKey<NavigatorState> navigatorKey) {
if (event.message?.user?.id == client.state.currentUser?.id) {
return;
}
final channelId = event.cid;
if (currentRoute?.settings.name == Routes.CHANNEL_PAGE.name) {
final args = currentRoute?.settings.arguments as Map<String, String>;
if (args['cid'] == channelId) {
return;
}
}
showLocalNotification(
event,
client.state.currentUser!.id,
navigatorKey.currentState!.context,
);
}
@override
void didPop(Route route, Route? previousRoute) {
currentRoute = route;
}
@override
void didPush(Route route, Route? previousRoute) {
currentRoute = route;
}
@override
void didRemove(Route route, Route? previousRoute) {
currentRoute = route;
}
@override
void didReplace({Route? newRoute, Route? oldRoute}) {
currentRoute = newRoute;
}
void dispose() {
_subscription.cancel();
}
}
@@ -1,9 +1,9 @@
import 'package:example/pages/channel_page.dart';
import 'package:example/utils/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'
hide Message;
import 'package:go_router/go_router.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
void showLocalNotification(
@@ -35,7 +35,7 @@ void showLocalNotification(
onSelectNotification: (channelCid) async {
if (channelCid != null) {
final client = StreamChat.of(context).client;
final navigator = Navigator.of(context);
final router = GoRouter.of(context);
var channel = client.state.channels[channelCid];
@@ -50,11 +50,9 @@ void showLocalNotification(
await channel.watch();
}
navigator.pushNamed(
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
),
router.pushNamed(
Routes.CHANNEL_PAGE.name,
params: Routes.CHANNEL_PAGE.params(channel),
);
}
},
@@ -5,10 +5,10 @@ import 'package:example/routes/routes.dart';
import 'package:example/widgets/search_text_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:go_router/go_router.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import '../pages/channel_page.dart';
import '../pages/chat_info_screen.dart';
import '../pages/group_info_screen.dart';
@@ -150,7 +150,7 @@ class _ChannelList extends State<ChannelList> {
final messageResponse = messageResponses[index];
FocusScope.of(context).requestFocus(FocusNode());
final client = StreamChat.of(context).client;
final navigator = Navigator.of(context);
final router = GoRouter.of(context);
final message = messageResponse.message;
final channel = client.channel(
messageResponse.channel!.type,
@@ -159,12 +159,10 @@ class _ChannelList extends State<ChannelList> {
if (channel.state == null) {
await channel.watch();
}
navigator.pushNamed(
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
router.pushNamed(
Routes.CHANNEL_PAGE.name,
params: Routes.CHANNEL_PAGE.params(channel),
queryParams: Routes.CHANNEL_PAGE.queryParams(message),
);
},
);
@@ -265,12 +263,9 @@ class _ChannelList extends State<ChannelList> {
);
},
onChannelTap: (channel) {
Navigator.pushNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
),
GoRouter.of(context).pushNamed(
Routes.CHANNEL_PAGE.name,
params: Routes.CHANNEL_PAGE.params(channel),
);
},
emptyBuilder: (_) {
@@ -286,10 +281,8 @@ class _ChannelList extends State<ChannelList> {
),
emptyTitle: TextButton(
onPressed: () {
Navigator.pushNamed(
context,
Routes.NEW_CHAT,
);
GoRouter.of(context)
.pushNamed(Routes.NEW_CHAT.name);
},
child: Text(
'Start a chat',
@@ -72,6 +72,7 @@
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
929AC42CE2CDA27FE9235234 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
CDF4BDDD28F9A6920059DB87 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = "<group>"; };
D4F4A1B883B395E86A7B745A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -142,6 +143,7 @@
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
CDF4BDDD28F9A6920059DB87 /* RunnerDebug.entitlements */,
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
@@ -159,7 +161,6 @@
929AC42CE2CDA27FE9235234 /* Pods-Runner.release.xcconfig */,
D4F4A1B883B395E86A7B745A /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
@@ -209,7 +210,6 @@
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
@@ -419,6 +419,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
@@ -544,9 +545,11 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -565,8 +568,10 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -6,11 +6,11 @@
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
@@ -4,9 +4,9 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
+3 -1
View File
@@ -27,7 +27,7 @@ dependencies:
path: packages/stream_chat_localizations
flutter_local_notifications: ^9.0.0
flutter_svg: ^1.0.3
flutter_secure_storage: ^5.0.2
flutter_secure_storage: ^6.0.0
yaml: ^3.1.0
uuid: ^3.0.5
streaming_shared_preferences: ^2.0.0
@@ -35,6 +35,8 @@ dependencies:
collection: ^1.15.0
sentry_flutter: ^6.5.0
flutter_slidable: ^2.0.0
go_router: ^5.0.5
provider: ^6.0.3
dev_dependencies:
flutter_launcher_icons: ^0.9.2