mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-12-24 23:29:32 -05:00
checkpoint
This commit is contained in:
parent
ee80dbf3a5
commit
d965f674fc
38
lib/app.dart
38
lib/app.dart
@ -7,6 +7,7 @@ import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
import 'router/router.dart';
|
||||
import 'tick.dart';
|
||||
|
||||
class VeilidChatApp extends ConsumerWidget {
|
||||
const VeilidChatApp({
|
||||
@ -23,24 +24,25 @@ class VeilidChatApp extends ConsumerWidget {
|
||||
final localizationDelegate = LocalizedApp.of(context).delegate;
|
||||
|
||||
return ThemeProvider(
|
||||
initTheme: theme,
|
||||
builder: (_, theme) => LocalizationProvider(
|
||||
state: LocalizationProvider.of(context).state,
|
||||
child: MaterialApp.router(
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerConfig: router,
|
||||
title: translate('app.title'),
|
||||
theme: theme,
|
||||
localizationsDelegates: [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
FormBuilderLocalizations.delegate,
|
||||
localizationDelegate
|
||||
],
|
||||
supportedLocales: localizationDelegate.supportedLocales,
|
||||
locale: localizationDelegate.currentLocale,
|
||||
)),
|
||||
);
|
||||
initTheme: theme,
|
||||
builder: (_, theme) => LocalizationProvider(
|
||||
state: LocalizationProvider.of(context).state,
|
||||
child: BackgroundTicker(
|
||||
builder: (context) => MaterialApp.router(
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerConfig: router,
|
||||
title: translate('app.title'),
|
||||
theme: theme,
|
||||
localizationsDelegates: [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
FormBuilderLocalizations.delegate,
|
||||
localizationDelegate
|
||||
],
|
||||
supportedLocales: localizationDelegate.supportedLocales,
|
||||
locale: localizationDelegate.currentLocale,
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||
@ -9,6 +11,7 @@ import 'package:uuid/uuid.dart';
|
||||
import '../../entities/proto.dart' as proto;
|
||||
import '../entities/identity.dart';
|
||||
import '../providers/account.dart';
|
||||
import '../providers/chat.dart';
|
||||
import '../providers/conversation.dart';
|
||||
import '../tools/theme_service.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
@ -59,6 +62,8 @@ class ChatComponentState extends ConsumerState<ChatComponent> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
externalize messages so they auto refresh and fix speed.
|
||||
|
||||
Future<void> _loadMessages() async {
|
||||
final localConversationRecordKey = proto.TypedKeyProto.fromProto(
|
||||
widget.activeChatContact.localConversationRecordKey);
|
||||
@ -88,8 +93,8 @@ class ChatComponentState extends ConsumerState<ChatComponent> {
|
||||
|
||||
final textMessage = types.TextMessage(
|
||||
author: isLocal ? _localUser : _remoteUser,
|
||||
createdAt: DateTime.now().millisecondsSinceEpoch,
|
||||
id: const Uuid().v4(),
|
||||
createdAt: (message.timestamp ~/ 1000).toInt(),
|
||||
id: message.timestamp.toString(),
|
||||
text: message.text,
|
||||
);
|
||||
return textMessage;
|
||||
@ -157,16 +162,23 @@ class ChatComponentState extends ConsumerState<ChatComponent> {
|
||||
decoration: BoxDecoration(
|
||||
color: scale.primaryScale.subtleBackground,
|
||||
),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 0),
|
||||
child: Text(contactName,
|
||||
textAlign: TextAlign.start,
|
||||
style: textTheme.titleMedium),
|
||||
),
|
||||
),
|
||||
child: Row(children: [
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.fromSTEB(
|
||||
16, 0, 16, 0),
|
||||
child: Text(contactName,
|
||||
textAlign: TextAlign.start,
|
||||
style: textTheme.titleMedium),
|
||||
)),
|
||||
Spacer(),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: () async {
|
||||
activeChatState.add(null);
|
||||
}).paddingLTRB(16, 0, 16, 0)
|
||||
]),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
|
@ -42,6 +42,7 @@ class ChatSingleContactListWidget extends ConsumerWidget {
|
||||
child: (chatList.isEmpty)
|
||||
? const EmptyChatListWidget()
|
||||
: SearchableList<proto.Chat>(
|
||||
autoFocusOnSearch: false,
|
||||
initialList: chatList.toList(),
|
||||
builder: (c) {
|
||||
final contact = contactMap[c.remoteConversationKey];
|
||||
|
@ -48,6 +48,7 @@ class ContactListWidget extends ConsumerWidget {
|
||||
child: (contactList.isEmpty)
|
||||
? const EmptyContactListWidget().toCenter()
|
||||
: SearchableList<proto.Contact>(
|
||||
autoFocusOnSearch: false,
|
||||
initialList: contactList.toList(),
|
||||
builder: (contact) => ContactItemWidget(contact: contact),
|
||||
filter: (value) {
|
||||
|
@ -137,17 +137,17 @@ extension IdentityMasterExtension on IdentityMaster {
|
||||
// Make empty contact list
|
||||
final contactList =
|
||||
await (await DHTShortArray.create(parent: accountRec.key))
|
||||
.scope((r) => r.record.ownedDHTRecordPointer);
|
||||
.scope((r) async => r.record.ownedDHTRecordPointer);
|
||||
|
||||
// Make empty contact invitation record list
|
||||
final contactInvitationRecords =
|
||||
await (await DHTShortArray.create(parent: accountRec.key))
|
||||
.scope((r) => r.record.ownedDHTRecordPointer);
|
||||
.scope((r) async => r.record.ownedDHTRecordPointer);
|
||||
|
||||
// Make empty chat record list
|
||||
final chatRecords =
|
||||
await (await DHTShortArray.create(parent: accountRec.key))
|
||||
.scope((r) => r.record.ownedDHTRecordPointer);
|
||||
.scope((r) async => r.record.ownedDHTRecordPointer);
|
||||
|
||||
// Make account object
|
||||
final account = proto.Account()
|
||||
|
47
lib/pages/chat_only_page.dart
Normal file
47
lib/pages/chat_only_page.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../providers/window_control.dart';
|
||||
import '../tools/tools.dart';
|
||||
import 'home.dart';
|
||||
|
||||
class ChatOnlyPage extends ConsumerStatefulWidget {
|
||||
const ChatOnlyPage({super.key});
|
||||
static const path = '/chat';
|
||||
|
||||
@override
|
||||
ChatOnlyPageState createState() => ChatOnlyPageState();
|
||||
}
|
||||
|
||||
class ChatOnlyPageState extends ConsumerState<ChatOnlyPage>
|
||||
with TickerProviderStateMixin {
|
||||
final _unfocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
setState(() {});
|
||||
await ref.read(windowControlProvider.notifier).changeWindowSetup(
|
||||
TitleBarStyle.normal, OrientationCapability.normal);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unfocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.watch(windowControlProvider);
|
||||
|
||||
return SafeArea(
|
||||
child: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).requestFocus(_unfocusNode),
|
||||
child: HomePage.buildChatComponent(context, ref),
|
||||
));
|
||||
}
|
||||
}
|
@ -25,169 +25,8 @@ class HomePage extends ConsumerStatefulWidget {
|
||||
|
||||
@override
|
||||
HomePageState createState() => HomePageState();
|
||||
}
|
||||
|
||||
// XXX Eliminate this when we have ValueChanged
|
||||
const int ticksPerContactInvitationCheck = 5;
|
||||
const int ticksPerNewMessageCheck = 5;
|
||||
|
||||
class HomePageState extends ConsumerState<HomePage>
|
||||
with TickerProviderStateMixin {
|
||||
final _unfocusNode = FocusNode();
|
||||
|
||||
Timer? _homeTickTimer;
|
||||
bool _inHomeTick = false;
|
||||
int _contactInvitationCheckTick = 0;
|
||||
int _newMessageCheckTick = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
setState(() {});
|
||||
await ref.read(windowControlProvider.notifier).changeWindowSetup(
|
||||
TitleBarStyle.normal, OrientationCapability.normal);
|
||||
|
||||
_homeTickTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (!_inHomeTick) {
|
||||
unawaited(_onHomeTick());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final homeTickTimer = _homeTickTimer;
|
||||
if (homeTickTimer != null) {
|
||||
homeTickTimer.cancel();
|
||||
}
|
||||
_unfocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _onHomeTick() async {
|
||||
_inHomeTick = true;
|
||||
try {
|
||||
final unord = <Future<void>>[];
|
||||
// Check extant contact invitations once every N seconds
|
||||
_contactInvitationCheckTick += 1;
|
||||
if (_contactInvitationCheckTick >= ticksPerContactInvitationCheck) {
|
||||
_contactInvitationCheckTick = 0;
|
||||
unord.add(_doContactInvitationCheck());
|
||||
}
|
||||
|
||||
// Check new messages once every N seconds
|
||||
_newMessageCheckTick += 1;
|
||||
if (_newMessageCheckTick >= ticksPerNewMessageCheck) {
|
||||
_newMessageCheckTick = 0;
|
||||
unord.add(_doNewMessageCheck());
|
||||
}
|
||||
if (unord.isNotEmpty) {
|
||||
await Future.wait(unord);
|
||||
}
|
||||
} finally {
|
||||
_inHomeTick = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _doContactInvitationCheck() async {
|
||||
final contactInvitationRecords =
|
||||
await ref.read(fetchContactInvitationRecordsProvider.future);
|
||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
||||
if (contactInvitationRecords == null || activeAccountInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final allChecks = <Future<void>>[];
|
||||
for (final contactInvitationRecord in contactInvitationRecords) {
|
||||
allChecks.add(() async {
|
||||
final acceptReject = await checkAcceptRejectContact(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
contactInvitationRecord: contactInvitationRecord);
|
||||
if (acceptReject != null) {
|
||||
final acceptedContact = acceptReject.acceptedContact;
|
||||
if (acceptedContact != null) {
|
||||
// Accept
|
||||
await createContact(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
profile: acceptedContact.profile,
|
||||
remoteIdentity: acceptedContact.remoteIdentity,
|
||||
remoteConversationRecordKey:
|
||||
acceptedContact.remoteConversationRecordKey,
|
||||
localConversationRecordKey:
|
||||
acceptedContact.localConversationRecordKey,
|
||||
);
|
||||
ref
|
||||
..invalidate(fetchContactInvitationRecordsProvider)
|
||||
..invalidate(fetchContactListProvider);
|
||||
} else {
|
||||
// Reject
|
||||
ref.invalidate(fetchContactInvitationRecordsProvider);
|
||||
}
|
||||
}
|
||||
}());
|
||||
}
|
||||
await Future.wait(allChecks);
|
||||
}
|
||||
|
||||
Future<void> _doNewMessageCheck() async {
|
||||
final activeChat = activeChatState.currentState;
|
||||
if (activeChat == null) {
|
||||
return;
|
||||
}
|
||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final contactList = ref.read(fetchContactListProvider).asData?.value ??
|
||||
const IListConst([]);
|
||||
|
||||
final activeChatContactIdx = contactList.indexWhere(
|
||||
(c) =>
|
||||
proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) ==
|
||||
activeChat,
|
||||
);
|
||||
if (activeChatContactIdx == -1) {
|
||||
return;
|
||||
}
|
||||
final activeChatContact = contactList[activeChatContactIdx];
|
||||
final remoteIdentityPublicKey =
|
||||
proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey);
|
||||
final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(
|
||||
activeChatContact.remoteConversationRecordKey);
|
||||
|
||||
await getRemoteConversationMessages(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
remoteIdentityPublicKey: remoteIdentityPublicKey,
|
||||
remoteConversationRecordKey: remoteConversationRecordKey);
|
||||
|
||||
// xxx add messages
|
||||
}
|
||||
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget buildPhone(BuildContext context) {
|
||||
//
|
||||
return Material(
|
||||
color: Colors.transparent, elevation: 4, child: MainPager());
|
||||
}
|
||||
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget buildTabletLeftPane(BuildContext context) {
|
||||
//
|
||||
return Material(
|
||||
color: Colors.transparent, elevation: 4, child: MainPager());
|
||||
}
|
||||
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget buildTabletRightPane(BuildContext context) {
|
||||
//
|
||||
return buildChatComponent(context);
|
||||
}
|
||||
|
||||
Widget buildChatComponent(BuildContext context) {
|
||||
static Widget buildChatComponent(BuildContext context, WidgetRef ref) {
|
||||
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
|
||||
const IListConst([]);
|
||||
|
||||
@ -218,6 +57,47 @@ class HomePageState extends ConsumerState<HomePage>
|
||||
activeChat: activeChat,
|
||||
activeChatContact: activeChatContact);
|
||||
}
|
||||
}
|
||||
|
||||
class HomePageState extends ConsumerState<HomePage>
|
||||
with TickerProviderStateMixin {
|
||||
final _unfocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
setState(() {});
|
||||
await ref.read(windowControlProvider.notifier).changeWindowSetup(
|
||||
TitleBarStyle.normal, OrientationCapability.normal);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unfocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget buildPhone(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent, elevation: 4, child: MainPager());
|
||||
}
|
||||
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget buildTabletLeftPane(BuildContext context) {
|
||||
//
|
||||
return Material(
|
||||
color: Colors.transparent, elevation: 4, child: MainPager());
|
||||
}
|
||||
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget buildTabletRightPane(BuildContext context) {
|
||||
//
|
||||
return HomePage.buildChatComponent(context, ref);
|
||||
}
|
||||
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget buildTablet(BuildContext context) {
|
||||
|
@ -34,8 +34,8 @@ class MainPagerState extends ConsumerState<MainPager>
|
||||
|
||||
final _unfocusNode = FocusNode();
|
||||
|
||||
final pageController = PageController();
|
||||
var _currentPage = 0;
|
||||
final pageController = PageController();
|
||||
|
||||
final _selectedIconList = <IconData>[Icons.person, Icons.chat];
|
||||
// final _unselectedIconList = <IconData>[
|
||||
@ -235,6 +235,11 @@ class MainPagerState extends ConsumerState<MainPager>
|
||||
onNotification: onScrollNotification,
|
||||
child: PageView(
|
||||
controller: pageController,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
_currentPage = index;
|
||||
});
|
||||
},
|
||||
//physics: const NeverScrollableScrollPhysics(),
|
||||
children: List.generate(
|
||||
_bottomBarPages.length, (index) => _bottomBarPages[index]),
|
||||
@ -271,9 +276,6 @@ class MainPagerState extends ConsumerState<MainPager>
|
||||
onTap: (index) async {
|
||||
await pageController.animateToPage(index,
|
||||
duration: 250.ms, curve: Curves.easeInOut);
|
||||
setState(() {
|
||||
_currentPage = index;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
|
@ -7,6 +7,7 @@ import '../entities/identity.dart';
|
||||
import '../entities/proto.dart' as proto;
|
||||
import '../entities/proto.dart' show Conversation;
|
||||
|
||||
import '../log/loggy.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
import 'account.dart';
|
||||
|
||||
@ -194,6 +195,71 @@ Future<void> addLocalConversationMessage(
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> mergeLocalConversationMessages(
|
||||
{required ActiveAccountInfo activeAccountInfo,
|
||||
required TypedKey localConversationRecordKey,
|
||||
required TypedKey remoteIdentityPublicKey,
|
||||
required IList<proto.Message> newMessages}) async {
|
||||
final conversation = await readLocalConversation(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
localConversationRecordKey: localConversationRecordKey,
|
||||
remoteIdentityPublicKey: remoteIdentityPublicKey);
|
||||
if (conversation == null) {
|
||||
return false;
|
||||
}
|
||||
bool changed = false;
|
||||
final messagesRecordKey =
|
||||
proto.TypedKeyProto.fromProto(conversation.messages);
|
||||
final crypto = await getConversationCrypto(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
remoteIdentityPublicKey: remoteIdentityPublicKey);
|
||||
final writer = getConversationWriter(activeAccountInfo: activeAccountInfo);
|
||||
|
||||
newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp)
|
||||
.compareTo(Timestamp.fromInt64(b.timestamp)));
|
||||
|
||||
await (await DHTShortArray.openWrite(messagesRecordKey, writer,
|
||||
parent: localConversationRecordKey, crypto: crypto))
|
||||
.scope((messages) async {
|
||||
// Ensure newMessages is sorted by timestamp
|
||||
newMessages =
|
||||
newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
// Existing messages will always be sorted by timestamp so merging is easy
|
||||
var pos = 0;
|
||||
outer:
|
||||
for (final newMessage in newMessages) {
|
||||
var skip = false;
|
||||
while (pos < messages.length) {
|
||||
final m = await messages.getItemProtobuf(proto.Message.fromBuffer, pos);
|
||||
if (m == null) {
|
||||
log.error('unable to get message #$pos');
|
||||
break outer;
|
||||
}
|
||||
|
||||
// If timestamp to insert is less than
|
||||
// the current position, insert it here
|
||||
final newTs = Timestamp.fromInt64(newMessage.timestamp);
|
||||
final curTs = Timestamp.fromInt64(m.timestamp);
|
||||
final cmp = newTs.compareTo(curTs);
|
||||
if (cmp < 0) {
|
||||
break;
|
||||
} else if (cmp == 0) {
|
||||
skip = true;
|
||||
break;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
// Insert at this position
|
||||
if (!skip) {
|
||||
await messages.tryInsertItem(pos, newMessage.writeToBuffer());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
return changed;
|
||||
}
|
||||
|
||||
Future<IList<proto.Message>?> getLocalConversationMessages({
|
||||
required ActiveAccountInfo activeAccountInfo,
|
||||
required TypedKey localConversationRecordKey,
|
||||
|
@ -2,10 +2,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../pages/chat_only_page.dart';
|
||||
import '../pages/home.dart';
|
||||
import '../pages/index.dart';
|
||||
import '../pages/new_account.dart';
|
||||
import '../providers/chat.dart';
|
||||
import '../providers/local_accounts.dart';
|
||||
import '../tools/responsive.dart';
|
||||
|
||||
part 'router_notifier.g.dart';
|
||||
|
||||
@ -16,6 +19,7 @@ class RouterNotifier extends _$RouterNotifier implements Listenable {
|
||||
|
||||
/// Do we need to make or import an account immediately?
|
||||
bool hasAnyAccount = false;
|
||||
bool hasActiveChat = false;
|
||||
|
||||
/// AsyncNotifier build
|
||||
@override
|
||||
@ -23,6 +27,7 @@ class RouterNotifier extends _$RouterNotifier implements Listenable {
|
||||
hasAnyAccount = await ref.watch(
|
||||
localAccountsProvider.selectAsync((data) => data.isNotEmpty),
|
||||
);
|
||||
hasActiveChat = ref.watch(activeChatStateProvider).asData?.value != null;
|
||||
|
||||
// When this notifier's state changes, inform GoRouter
|
||||
ref.listenSelf((_, __) {
|
||||
@ -46,6 +51,36 @@ class RouterNotifier extends _$RouterNotifier implements Listenable {
|
||||
return hasAnyAccount ? HomePage.path : NewAccountPage.path;
|
||||
case NewAccountPage.path:
|
||||
return hasAnyAccount ? HomePage.path : null;
|
||||
case HomePage.path:
|
||||
if (!hasAnyAccount) {
|
||||
return NewAccountPage.path;
|
||||
}
|
||||
if (responsiveVisibility(
|
||||
context: context,
|
||||
tablet: false,
|
||||
tabletLandscape: false,
|
||||
desktop: false)) {
|
||||
if (hasActiveChat) {
|
||||
return ChatOnlyPage.path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
case ChatOnlyPage.path:
|
||||
if (!hasAnyAccount) {
|
||||
return NewAccountPage.path;
|
||||
}
|
||||
if (responsiveVisibility(
|
||||
context: context,
|
||||
tablet: false,
|
||||
tabletLandscape: false,
|
||||
desktop: false)) {
|
||||
if (!hasActiveChat) {
|
||||
return HomePage.path;
|
||||
}
|
||||
} else {
|
||||
return HomePage.path;
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
return hasAnyAccount ? null : NewAccountPage.path;
|
||||
}
|
||||
@ -65,6 +100,10 @@ class RouterNotifier extends _$RouterNotifier implements Listenable {
|
||||
path: NewAccountPage.path,
|
||||
builder: (context, state) => const NewAccountPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: ChatOnlyPage.path,
|
||||
builder: (context, state) => const ChatOnlyPage(),
|
||||
),
|
||||
];
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
165
lib/tick.dart
Normal file
165
lib/tick.dart
Normal file
@ -0,0 +1,165 @@
|
||||
// XXX Eliminate this when we have ValueChanged
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../entities/proto.dart' as proto;
|
||||
import 'providers/account.dart';
|
||||
import 'providers/chat.dart';
|
||||
import 'providers/contact.dart';
|
||||
import 'providers/contact_invite.dart';
|
||||
import 'providers/conversation.dart';
|
||||
|
||||
const int ticksPerContactInvitationCheck = 5;
|
||||
const int ticksPerNewMessageCheck = 5;
|
||||
|
||||
class BackgroundTicker extends ConsumerStatefulWidget {
|
||||
const BackgroundTicker({required this.builder, super.key});
|
||||
|
||||
final Widget Function(BuildContext) builder;
|
||||
|
||||
@override
|
||||
BackgroundTickerState createState() => BackgroundTickerState();
|
||||
}
|
||||
|
||||
class BackgroundTickerState extends ConsumerState<BackgroundTicker> {
|
||||
Timer? _tickTimer;
|
||||
bool _inTick = false;
|
||||
int _contactInvitationCheckTick = 0;
|
||||
int _newMessageCheckTick = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tickTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (!_inTick) {
|
||||
unawaited(_onTick());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final tickTimer = _tickTimer;
|
||||
if (tickTimer != null) {
|
||||
tickTimer.cancel();
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return widget.builder(context);
|
||||
}
|
||||
|
||||
Future<void> _onTick() async {
|
||||
_inTick = true;
|
||||
try {
|
||||
final unord = <Future<void>>[];
|
||||
// Check extant contact invitations once every N seconds
|
||||
_contactInvitationCheckTick += 1;
|
||||
if (_contactInvitationCheckTick >= ticksPerContactInvitationCheck) {
|
||||
_contactInvitationCheckTick = 0;
|
||||
unord.add(_doContactInvitationCheck());
|
||||
}
|
||||
|
||||
// Check new messages once every N seconds
|
||||
_newMessageCheckTick += 1;
|
||||
if (_newMessageCheckTick >= ticksPerNewMessageCheck) {
|
||||
_newMessageCheckTick = 0;
|
||||
unord.add(_doNewMessageCheck());
|
||||
}
|
||||
if (unord.isNotEmpty) {
|
||||
await Future.wait(unord);
|
||||
}
|
||||
} finally {
|
||||
_inTick = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _doContactInvitationCheck() async {
|
||||
final contactInvitationRecords =
|
||||
await ref.read(fetchContactInvitationRecordsProvider.future);
|
||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
||||
if (contactInvitationRecords == null || activeAccountInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final allChecks = <Future<void>>[];
|
||||
for (final contactInvitationRecord in contactInvitationRecords) {
|
||||
allChecks.add(() async {
|
||||
final acceptReject = await checkAcceptRejectContact(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
contactInvitationRecord: contactInvitationRecord);
|
||||
if (acceptReject != null) {
|
||||
final acceptedContact = acceptReject.acceptedContact;
|
||||
if (acceptedContact != null) {
|
||||
// Accept
|
||||
await createContact(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
profile: acceptedContact.profile,
|
||||
remoteIdentity: acceptedContact.remoteIdentity,
|
||||
remoteConversationRecordKey:
|
||||
acceptedContact.remoteConversationRecordKey,
|
||||
localConversationRecordKey:
|
||||
acceptedContact.localConversationRecordKey,
|
||||
);
|
||||
ref
|
||||
..invalidate(fetchContactInvitationRecordsProvider)
|
||||
..invalidate(fetchContactListProvider);
|
||||
} else {
|
||||
// Reject
|
||||
ref.invalidate(fetchContactInvitationRecordsProvider);
|
||||
}
|
||||
}
|
||||
}());
|
||||
}
|
||||
await Future.wait(allChecks);
|
||||
}
|
||||
|
||||
Future<void> _doNewMessageCheck() async {
|
||||
final activeChat = activeChatState.currentState;
|
||||
if (activeChat == null) {
|
||||
return;
|
||||
}
|
||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
||||
if (activeAccountInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final contactList = ref.read(fetchContactListProvider).asData?.value ??
|
||||
const IListConst([]);
|
||||
|
||||
final activeChatContactIdx = contactList.indexWhere(
|
||||
(c) =>
|
||||
proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) ==
|
||||
activeChat,
|
||||
);
|
||||
if (activeChatContactIdx == -1) {
|
||||
return;
|
||||
}
|
||||
final activeChatContact = contactList[activeChatContactIdx];
|
||||
final remoteIdentityPublicKey =
|
||||
proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey);
|
||||
final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(
|
||||
activeChatContact.remoteConversationRecordKey);
|
||||
final localConversationRecordKey = proto.TypedKeyProto.fromProto(
|
||||
activeChatContact.localConversationRecordKey);
|
||||
|
||||
final newMessages = await getRemoteConversationMessages(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
remoteIdentityPublicKey: remoteIdentityPublicKey,
|
||||
remoteConversationRecordKey: remoteConversationRecordKey);
|
||||
if (newMessages != null) {
|
||||
await mergeLocalConversationMessages(
|
||||
activeAccountInfo: activeAccountInfo,
|
||||
localConversationRecordKey: localConversationRecordKey,
|
||||
remoteIdentityPublicKey: remoteIdentityPublicKey,
|
||||
newMessages: newMessages);
|
||||
}
|
||||
}
|
||||
}
|
@ -66,7 +66,7 @@ class DHTRecord {
|
||||
_valid = false;
|
||||
}
|
||||
|
||||
Future<T> scope<T>(FutureOr<T> Function(DHTRecord) scopeFunction) async {
|
||||
Future<T> scope<T>(Future<T> Function(DHTRecord) scopeFunction) async {
|
||||
try {
|
||||
return await scopeFunction(this);
|
||||
} finally {
|
||||
@ -76,8 +76,7 @@ class DHTRecord {
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> deleteScope<T>(
|
||||
FutureOr<T> Function(DHTRecord) scopeFunction) async {
|
||||
Future<T> deleteScope<T>(Future<T> Function(DHTRecord) scopeFunction) async {
|
||||
try {
|
||||
final out = await scopeFunction(this);
|
||||
if (_valid && _open) {
|
||||
|
@ -42,10 +42,10 @@ class DHTShortArray {
|
||||
}
|
||||
stride = oCnt - 1;
|
||||
case DHTSchemaSMPL(oCnt: final oCnt, members: final members):
|
||||
if (oCnt != 0 || members.length != 1 || members[1].mCnt <= 1) {
|
||||
if (oCnt != 0 || members.length != 1 || members[0].mCnt <= 1) {
|
||||
throw StateError('Invalid SMPL schema in DHTShortArray');
|
||||
}
|
||||
stride = members[1].mCnt - 1;
|
||||
stride = members[0].mCnt - 1;
|
||||
}
|
||||
assert(stride <= maxElements, 'stride too long');
|
||||
_stride = stride;
|
||||
@ -117,7 +117,7 @@ class DHTShortArray {
|
||||
parent: parent, routingContext: routingContext, crypto: crypto);
|
||||
try {
|
||||
final dhtShortArray = DHTShortArray._(headRecord: dhtRecord);
|
||||
await dhtShortArray._refreshHead();
|
||||
await dhtShortArray._refreshHead(forceRefresh: true);
|
||||
return dhtShortArray;
|
||||
} on Exception catch (_) {
|
||||
await dhtRecord.close();
|
||||
@ -137,7 +137,7 @@ class DHTShortArray {
|
||||
parent: parent, routingContext: routingContext, crypto: crypto);
|
||||
try {
|
||||
final dhtShortArray = DHTShortArray._(headRecord: dhtRecord);
|
||||
await dhtShortArray._refreshHead();
|
||||
await dhtShortArray._refreshHead(forceRefresh: true);
|
||||
return dhtShortArray;
|
||||
} on Exception catch (_) {
|
||||
await dhtRecord.close();
|
||||
@ -315,7 +315,7 @@ class DHTShortArray {
|
||||
await Future.wait(futures);
|
||||
}
|
||||
|
||||
Future<T> scope<T>(FutureOr<T> Function(DHTShortArray) scopeFunction) async {
|
||||
Future<T> scope<T>(Future<T> Function(DHTShortArray) scopeFunction) async {
|
||||
try {
|
||||
return await scopeFunction(this);
|
||||
} finally {
|
||||
@ -324,7 +324,7 @@ class DHTShortArray {
|
||||
}
|
||||
|
||||
Future<T> deleteScope<T>(
|
||||
FutureOr<T> Function(DHTShortArray) scopeFunction) async {
|
||||
Future<T> Function(DHTShortArray) scopeFunction) async {
|
||||
try {
|
||||
final out = await scopeFunction(this);
|
||||
await close();
|
||||
|
Loading…
Reference in New Issue
Block a user