This commit is contained in:
Christien Rioux 2024-01-26 21:02:11 -05:00
parent b35b618a4d
commit 7cf44ef192
21 changed files with 338 additions and 82 deletions

View file

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import '../../../chat/chat.dart';
import '../../../tools/tools.dart';
class ChatOnlyPage extends StatefulWidget {
const ChatOnlyPage({super.key});
@override
ChatOnlyPageState createState() => ChatOnlyPageState();
}
class ChatOnlyPageState extends State<ChatOnlyPage>
with TickerProviderStateMixin {
final _unfocusNode = FocusNode();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
setState(() {});
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
});
}
@override
void dispose() {
_unfocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => SafeArea(
child: GestureDetector(
onTap: () => FocusScope.of(context).requestFocus(_unfocusNode),
child: buildChatComponent(),
));
}

View file

@ -0,0 +1,235 @@
import 'dart:async';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../account_manager/account_manager.dart';
import '../../../contact_invitation/contact_invitation.dart';
import '../../../proto/proto.dart' as proto;
import '../../../theme/theme.dart';
import '../../../tools/tools.dart';
class HomeAccountReady extends StatefulWidget {
const HomeAccountReady(
{required IList<LocalAccount> localAccounts,
required TypedKey activeUserLogin,
required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
super.key})
: _localAccounts = localAccounts,
_activeUserLogin = activeUserLogin,
_activeAccountInfo = activeAccountInfo,
_account = account;
final IList<LocalAccount> _localAccounts;
final TypedKey _activeUserLogin;
final ActiveAccountInfo _activeAccountInfo;
final proto.Account _account;
@override
HomeAccountReadyState createState() => HomeAccountReadyState();
}
class HomeAccountReadyState extends State<HomeAccountReady>
with TickerProviderStateMixin {
//
ContactInvitationRepository? _contactInvitationRepository;
//
@override
void initState() {
super.initState();
// Async initialize repositories for the active user
// xxx: this should not be necessary
// xxx: but RepositoryProvider doesn't call dispose()
Future.delayed(Duration.zero, () async {
//
final cir = await ContactInvitationRepository.open(
widget._activeAccountInfo, widget._account);
setState(() {
_contactInvitationRepository = cir;
});
});
}
@override
void dispose() {
super.dispose();
_contactInvitationRepository?.dispose();
}
// ignore: prefer_expression_function_bodies
Widget buildAccountList() {
return const Column(children: [
Center(child: Text('Small Profile')),
Center(child: Text('Contact invitations')),
Center(child: Text('Contacts'))
]);
}
Widget buildUnlockAccount(
BuildContext context,
IList<LocalAccount> localAccounts,
// ignore: prefer_expression_function_bodies
) {
return const Center(child: Text('unlock account'));
}
/// We have an active, unlocked, user login
Widget buildReadyAccount(
BuildContext context,
IList<LocalAccount> localAccounts,
TypedKey activeUserLogin,
DHTRecord accountRecord) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
xxx get rid of the cubit here and
return BlocProvider(
create: (context) => AccountRecordCubit(record: accountRecord),
child: Column(children: <Widget>[
Row(children: [
IconButton(
icon: const Icon(Icons.settings),
color: scale.secondaryScale.text,
constraints: const BoxConstraints.expand(height: 64, width: 64),
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(scale.secondaryScale.border),
shape: MaterialStateProperty.all(
const RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(16))))),
tooltip: translate('app_bar.settings_tooltip'),
onPressed: () async {
context.go('/home/settings');
}).paddingLTRB(0, 0, 8, 0),
context
.watch<AccountRecordCubit>()
.state
.builder((context, account) => ProfileWidget(
name: account.profile.name,
pronouns: account.profile.pronouns,
))
.expanded(),
]).paddingAll(8),
context
.watch<AccountRecordCubit>()
.state
.builder((context, account) => MainPager(
localAccounts: localAccounts,
activeUserLogin: activeUserLogin,
account: account))
.expanded()
]));
}
xxx get rid of this whole function
Widget buildUserPanel() => Builder(builder: (context) {
final activeUserLogin = context.watch<ActiveUserLoginCubit>().state;
final localAccounts = context.watch<LocalAccountsCubit>().state;
if (activeUserLogin == null) {
// If no logged in user is active, show the loading panel
return waitingPage(context);
}
final account = AccountRepository.instance
.getAccountInfo(accountMasterRecordKey: activeUserLogin)!;
switch (account.status) {
case AccountInfoStatus.noAccount:
Future.delayed(0.ms, () async {
await showErrorModal(
context,
translate('home.missing_account_title'),
translate('home.missing_account_text'));
// Delete account
await AccountRepository.instance
.deleteLocalAccount(activeUserLogin);
// Switch to no active user login
await AccountRepository.instance.switchToAccount(null);
});
return waitingPage(context);
case AccountInfoStatus.accountInvalid:
Future.delayed(0.ms, () async {
await showErrorModal(
context,
translate('home.invalid_account_title'),
translate('home.invalid_account_text'));
// Delete account
await AccountRepository.instance
.deleteLocalAccount(activeUserLogin);
// Switch to no active user login
await AccountRepository.instance.switchToAccount(null);
});
return waitingPage(context);
case AccountInfoStatus.accountLocked:
// Show unlock widget
return buildUnlockAccount(context, localAccounts);
case AccountInfoStatus.accountReady:
return buildReadyAccount(
context,
localAccounts,
activeUserLogin,
account.activeAccountInfo!.accountRecord,
);
}
});
Widget buildPhone() =>
Material(color: Colors.transparent, child: buildUserPanel());
Widget buildTabletLeftPane() =>
Material(color: Colors.transparent, child: buildUserPanel());
Widget buildTabletRightPane() => buildChatComponent();
// ignore: prefer_expression_function_bodies
Widget buildTablet() => Builder(builder: (context) {
final w = MediaQuery.of(context).size.width;
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final children = [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 300, maxWidth: 300),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: w / 2),
child: buildTabletLeftPane())),
SizedBox(
width: 2,
height: double.infinity,
child: ColoredBox(color: scale.primaryScale.hoverBorder)),
Expanded(child: buildTabletRightPane()),
];
return Row(
children: children,
);
});
@override
Widget build(BuildContext context) {
if (_contactInvitationRepository == null) {
return waitingPage(context);
}
return responsiveVisibility(
context: context,
phone: false,
)
? buildTablet()
: buildPhone();
}
}

View file

@ -0,0 +1,95 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../../proto/proto.dart' as proto;
import '../../../account_manager/account_manager.dart';
import '../../../contact_invitation/contact_invitation.dart';
import '../../../contacts/contacts.dart';
import '../../../theme/theme.dart';
class AccountPage extends StatefulWidget {
const AccountPage({
required this.localAccounts,
required this.activeUserLogin,
required this.account,
super.key,
});
final IList<LocalAccount> localAccounts;
final TypedKey activeUserLogin;
final proto.Account account;
@override
AccountPageState createState() => AccountPageState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(IterableProperty<LocalAccount>('localAccounts', localAccounts))
..add(DiagnosticsProperty<TypedKey>('activeUserLogin', activeUserLogin))
..add(DiagnosticsProperty<proto.Account>('account', account));
}
}
class AccountPageState extends State<AccountPage> {
final _unfocusNode = FocusNode();
@override
void initState() {
super.initState();
}
@override
void dispose() {
_unfocusNode.dispose();
super.dispose();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
final records = widget.account.contactInvitationRecords;
final contactInvitationRecordList =
ref.watch(fetchContactInvitationRecordsProvider).asData?.value ??
const IListConst([]);
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
const IListConst([]);
return SizedBox(
child: Column(children: <Widget>[
if (contactInvitationRecordList.isNotEmpty)
ExpansionTile(
tilePadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
backgroundColor: scale.primaryScale.border,
collapsedBackgroundColor: scale.primaryScale.border,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
collapsedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Text(
translate('account_page.contact_invitations'),
textAlign: TextAlign.center,
style: textTheme.titleMedium!
.copyWith(color: scale.primaryScale.subtleText),
),
initiallyExpanded: true,
children: [
ContactInvitationListWidget(
contactInvitationRecordList: contactInvitationRecordList)
],
).paddingLTRB(8, 0, 8, 8),
ContactListWidget(contactList: contactList).expanded(),
]));
}
}

View file

@ -0,0 +1,67 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class BottomSheetActionButton extends StatefulWidget {
const BottomSheetActionButton(
{required this.bottomSheetBuilder,
required this.builder,
this.foregroundColor,
this.backgroundColor,
this.shape,
super.key});
final Color? foregroundColor;
final Color? backgroundColor;
final ShapeBorder? shape;
final Widget Function(BuildContext) builder;
final Widget Function(BuildContext) bottomSheetBuilder;
@override
BottomSheetActionButtonState createState() => BottomSheetActionButtonState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(ObjectFlagProperty<Widget Function(BuildContext p1)>.has(
'bottomSheetBuilder', bottomSheetBuilder))
..add(ColorProperty('foregroundColor', foregroundColor))
..add(ColorProperty('backgroundColor', backgroundColor))
..add(DiagnosticsProperty<ShapeBorder?>('shape', shape))
..add(ObjectFlagProperty<Widget? Function(BuildContext p1)>.has(
'builder', builder));
}
}
class BottomSheetActionButtonState extends State<BottomSheetActionButton> {
bool _showFab = true;
@override
void initState() {
super.initState();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
//
return _showFab
? FloatingActionButton(
elevation: 0,
hoverElevation: 0,
shape: widget.shape,
foregroundColor: widget.foregroundColor,
backgroundColor: widget.backgroundColor,
child: widget.builder(context),
onPressed: () async {
await showModalBottomSheet<void>(
context: context, builder: widget.bottomSheetBuilder);
},
)
: Container();
}
void showFloatingActionButton(bool value) {
setState(() {
_showFab = value;
});
}
}

View file

@ -0,0 +1,91 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import '../../../../proto/proto.dart' as proto;
import '../../../account_manager/account_manager.dart';
import '../../../tools/tools.dart';
class ChatsPage extends StatefulWidget {
const ChatsPage({super.key});
@override
ChatsPageState createState() => ChatsPageState();
}
class ChatsPageState extends State<ChatsPage> {
final _unfocusNode = FocusNode();
@override
void initState() {
super.initState();
}
@override
void dispose() {
_unfocusNode.dispose();
super.dispose();
}
/// We have an active, unlocked, user login
Widget buildChatList(
BuildContext context,
IList<LocalAccount> localAccounts,
TypedKey activeUserLogin,
proto.Account account,
// ignore: prefer_expression_function_bodies
) {
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
const IListConst([]);
final chatList =
ref.watch(fetchChatListProvider).asData?.value ?? const IListConst([]);
return Column(children: <Widget>[
if (chatList.isNotEmpty)
ChatSingleContactListWidget(
contactList: contactList, chatList: chatList)
.expanded(),
if (chatList.isEmpty) const EmptyChatListWidget().expanded(),
]);
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final localAccountsV = ref.watch(localAccountsProvider);
final loginsV = ref.watch(loginsProvider);
if (!localAccountsV.hasValue || !loginsV.hasValue) {
return waitingPage(context);
}
final localAccounts = localAccountsV.requireValue;
final logins = loginsV.requireValue;
final activeUserLogin = logins.activeUserLogin;
if (activeUserLogin == null) {
// If no logged in user is active show a placeholder
return waitingPage(context);
}
final accountV = ref
.watch(fetchAccountProvider(accountMasterRecordKey: activeUserLogin));
if (!accountV.hasValue) {
return waitingPage(context);
}
final account = accountV.requireValue;
switch (account.status) {
case AccountInfoStatus.noAccount:
return waitingPage(context);
case AccountInfoStatus.accountInvalid:
return waitingPage(context);
case AccountInfoStatus.accountLocked:
return waitingPage(context);
case AccountInfoStatus.accountReady:
return buildChatList(
context,
localAccounts,
activeUserLogin,
account.account!,
);
}
}
}

View file

@ -0,0 +1,252 @@
import 'dart:async';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:preload_page_view/preload_page_view.dart';
import 'package:stylish_bottom_bar/model/bar_items.dart';
import 'package:stylish_bottom_bar/stylish_bottom_bar.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../../proto/proto.dart' as proto;
import '../../../../tools/tools.dart';
import '../../../account_manager/account_manager.dart';
import '../../../contact_invitation/contact_invitation.dart';
import '../../../theme/theme.dart';
import 'account_page.dart';
import 'bottom_sheet_action_button.dart';
import 'chats_page.dart';
class MainPager extends StatefulWidget {
const MainPager(
{required this.localAccounts,
required this.activeUserLogin,
required this.account,
super.key});
final IList<LocalAccount> localAccounts;
final TypedKey activeUserLogin;
final proto.Account account;
@override
MainPagerState createState() => MainPagerState();
static MainPagerState? of(BuildContext context) =>
context.findAncestorStateOfType<MainPagerState>();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(IterableProperty<LocalAccount>('localAccounts', localAccounts))
..add(DiagnosticsProperty<TypedKey>('activeUserLogin', activeUserLogin))
..add(DiagnosticsProperty<proto.Account>('account', account));
}
}
class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
//////////////////////////////////////////////////////////////////
final _unfocusNode = FocusNode();
var _currentPage = 0;
final pageController = PreloadPageController();
final _selectedIconList = <IconData>[Icons.person, Icons.chat];
// final _unselectedIconList = <IconData>[
// Icons.chat_outlined,
// Icons.person_outlined
// ];
final _fabIconList = <IconData>[
Icons.person_add_sharp,
Icons.add_comment_sharp,
];
final _bottomLabelList = <String>[
translate('pager.account'),
translate('pager.chats'),
];
//////////////////////////////////////////////////////////////////
@override
void initState() {
super.initState();
}
@override
void dispose() {
_unfocusNode.dispose();
pageController.dispose();
super.dispose();
}
bool onScrollNotification(ScrollNotification notification) {
if (notification is UserScrollNotification &&
notification.metrics.axis == Axis.vertical) {
switch (notification.direction) {
case ScrollDirection.forward:
// _hideBottomBarAnimationController.reverse();
// _fabAnimationController.forward(from: 0);
break;
case ScrollDirection.reverse:
// _hideBottomBarAnimationController.forward();
// _fabAnimationController.reverse(from: 1);
break;
case ScrollDirection.idle:
break;
}
}
return false;
}
BottomBarItem buildBottomBarItem(int index) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return BottomBarItem(
title: Text(_bottomLabelList[index]),
icon: Icon(_selectedIconList[index], color: scale.primaryScale.text),
selectedIcon:
Icon(_selectedIconList[index], color: scale.primaryScale.text),
backgroundColor: scale.primaryScale.text,
//unSelectedColor: theme.colorScheme.primaryContainer,
//selectedColor: theme.colorScheme.primary,
//badge: const Text('9+'),
//showBadge: true,
);
}
List<BottomBarItem> _buildBottomBarItems() {
final bottomBarItems = List<BottomBarItem>.empty(growable: true);
for (var index = 0; index < _bottomLabelList.length; index++) {
final item = buildBottomBarItem(index);
bottomBarItems.add(item);
}
return bottomBarItems;
}
Future<void> scanContactInvitationDialog(BuildContext context) async {
await showDialog<void>(
context: context,
// ignore: prefer_expression_function_bodies
builder: (context) {
return const AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
contentPadding: EdgeInsets.only(
top: 10,
),
title: Text(
'Scan Contact Invite',
style: TextStyle(fontSize: 24),
),
content: ScanInviteDialog());
});
}
// ignore: prefer_expression_function_bodies
Widget _onNewChatBottomSheetBuilder(BuildContext context) {
return const SizedBox(
height: 200,
child: Center(
child: Text(
'Group and custom chat functionality is not available yet')));
}
Widget _bottomSheetBuilder(BuildContext context) {
if (_currentPage == 0) {
// New contact invitation
return newContactInvitationBottomSheetBuilder(context);
} else if (_currentPage == 1) {
// New chat
return _onNewChatBottomSheetBuilder(context);
} else {
// Unknown error
return waitingPage(context);
}
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return Scaffold(
//extendBody: true,
backgroundColor: Colors.transparent,
body: NotificationListener<ScrollNotification>(
onNotification: onScrollNotification,
child: PreloadPageView(
controller: pageController,
preloadPagesCount: 2,
onPageChanged: (index) {
setState(() {
_currentPage = index;
});
},
children: [
AccountPage(
localAccounts: widget.localAccounts,
activeUserLogin: widget.activeUserLogin,
account: widget.account),
const ChatsPage(),
])),
// appBar: AppBar(
// toolbarHeight: 24,
// title: Text(
// 'C',
// style: Theme.of(context).textTheme.headlineSmall,
// ),
// ),
bottomNavigationBar: StylishBottomBar(
backgroundColor: scale.primaryScale.hoverBorder,
// gradient: LinearGradient(
// begin: Alignment.topCenter,
// end: Alignment.bottomCenter,
// colors: <Color>[
// theme.colorScheme.primary,
// theme.colorScheme.primaryContainer,
// ]),
//borderRadius: BorderRadius.all(Radius.circular(16)),
option: AnimatedBarOptions(
// iconSize: 32,
//barAnimation: BarAnimation.fade,
iconStyle: IconStyle.animated,
inkEffect: true,
inkColor: scale.primaryScale.hoverBackground,
//opacity: 0.3,
),
items: _buildBottomBarItems(),
hasNotch: true,
fabLocation: StylishBarFabLocation.end,
currentIndex: _currentPage,
onTap: (index) async {
await pageController.animateToPage(index,
duration: 250.ms, curve: Curves.easeInOut);
},
),
floatingActionButton: BottomSheetActionButton(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(14))),
foregroundColor: scale.secondaryScale.text,
backgroundColor: scale.secondaryScale.hoverBorder,
builder: (context) => Icon(
_fabIconList[_currentPage],
color: scale.secondaryScale.text,
),
bottomSheetBuilder: _bottomSheetBuilder),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<PreloadPageController>(
'pageController', pageController));
}
}