checkpoint

This commit is contained in:
Christien Rioux 2023-08-07 23:03:26 -07:00
parent ee80dbf3a5
commit d965f674fc
13 changed files with 422 additions and 208 deletions

View File

@ -7,6 +7,7 @@ import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
import 'router/router.dart'; import 'router/router.dart';
import 'tick.dart';
class VeilidChatApp extends ConsumerWidget { class VeilidChatApp extends ConsumerWidget {
const VeilidChatApp({ const VeilidChatApp({
@ -23,24 +24,25 @@ class VeilidChatApp extends ConsumerWidget {
final localizationDelegate = LocalizedApp.of(context).delegate; final localizationDelegate = LocalizedApp.of(context).delegate;
return ThemeProvider( return ThemeProvider(
initTheme: theme, initTheme: theme,
builder: (_, theme) => LocalizationProvider( builder: (_, theme) => LocalizationProvider(
state: LocalizationProvider.of(context).state, state: LocalizationProvider.of(context).state,
child: MaterialApp.router( child: BackgroundTicker(
debugShowCheckedModeBanner: false, builder: (context) => MaterialApp.router(
routerConfig: router, debugShowCheckedModeBanner: false,
title: translate('app.title'), routerConfig: router,
theme: theme, title: translate('app.title'),
localizationsDelegates: [ theme: theme,
GlobalMaterialLocalizations.delegate, localizationsDelegates: [
GlobalWidgetsLocalizations.delegate, GlobalMaterialLocalizations.delegate,
FormBuilderLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
localizationDelegate FormBuilderLocalizations.delegate,
], localizationDelegate
supportedLocales: localizationDelegate.supportedLocales, ],
locale: localizationDelegate.currentLocale, supportedLocales: localizationDelegate.supportedLocales,
)), locale: localizationDelegate.currentLocale,
); )),
));
} }
@override @override

View File

@ -1,5 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart'; 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/proto.dart' as proto;
import '../entities/identity.dart'; import '../entities/identity.dart';
import '../providers/account.dart'; import '../providers/account.dart';
import '../providers/chat.dart';
import '../providers/conversation.dart'; import '../providers/conversation.dart';
import '../tools/theme_service.dart'; import '../tools/theme_service.dart';
import '../veilid_support/veilid_support.dart'; import '../veilid_support/veilid_support.dart';
@ -59,6 +62,8 @@ class ChatComponentState extends ConsumerState<ChatComponent> {
super.dispose(); super.dispose();
} }
externalize messages so they auto refresh and fix speed.
Future<void> _loadMessages() async { Future<void> _loadMessages() async {
final localConversationRecordKey = proto.TypedKeyProto.fromProto( final localConversationRecordKey = proto.TypedKeyProto.fromProto(
widget.activeChatContact.localConversationRecordKey); widget.activeChatContact.localConversationRecordKey);
@ -88,8 +93,8 @@ class ChatComponentState extends ConsumerState<ChatComponent> {
final textMessage = types.TextMessage( final textMessage = types.TextMessage(
author: isLocal ? _localUser : _remoteUser, author: isLocal ? _localUser : _remoteUser,
createdAt: DateTime.now().millisecondsSinceEpoch, createdAt: (message.timestamp ~/ 1000).toInt(),
id: const Uuid().v4(), id: message.timestamp.toString(),
text: message.text, text: message.text,
); );
return textMessage; return textMessage;
@ -157,16 +162,23 @@ class ChatComponentState extends ConsumerState<ChatComponent> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: scale.primaryScale.subtleBackground, color: scale.primaryScale.subtleBackground,
), ),
child: Align( child: Row(children: [
alignment: AlignmentDirectional.centerStart, Align(
child: Padding( alignment: AlignmentDirectional.centerStart,
padding: child: Padding(
const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 0), padding: const EdgeInsetsDirectional.fromSTEB(
child: Text(contactName, 16, 0, 16, 0),
textAlign: TextAlign.start, child: Text(contactName,
style: textTheme.titleMedium), textAlign: TextAlign.start,
), style: textTheme.titleMedium),
), )),
Spacer(),
IconButton(
icon: Icon(Icons.close),
onPressed: () async {
activeChatState.add(null);
}).paddingLTRB(16, 0, 16, 0)
]),
), ),
Expanded( Expanded(
child: Container( child: Container(

View File

@ -42,6 +42,7 @@ class ChatSingleContactListWidget extends ConsumerWidget {
child: (chatList.isEmpty) child: (chatList.isEmpty)
? const EmptyChatListWidget() ? const EmptyChatListWidget()
: SearchableList<proto.Chat>( : SearchableList<proto.Chat>(
autoFocusOnSearch: false,
initialList: chatList.toList(), initialList: chatList.toList(),
builder: (c) { builder: (c) {
final contact = contactMap[c.remoteConversationKey]; final contact = contactMap[c.remoteConversationKey];

View File

@ -48,6 +48,7 @@ class ContactListWidget extends ConsumerWidget {
child: (contactList.isEmpty) child: (contactList.isEmpty)
? const EmptyContactListWidget().toCenter() ? const EmptyContactListWidget().toCenter()
: SearchableList<proto.Contact>( : SearchableList<proto.Contact>(
autoFocusOnSearch: false,
initialList: contactList.toList(), initialList: contactList.toList(),
builder: (contact) => ContactItemWidget(contact: contact), builder: (contact) => ContactItemWidget(contact: contact),
filter: (value) { filter: (value) {

View File

@ -137,17 +137,17 @@ extension IdentityMasterExtension on IdentityMaster {
// Make empty contact list // Make empty contact list
final contactList = final contactList =
await (await DHTShortArray.create(parent: accountRec.key)) await (await DHTShortArray.create(parent: accountRec.key))
.scope((r) => r.record.ownedDHTRecordPointer); .scope((r) async => r.record.ownedDHTRecordPointer);
// Make empty contact invitation record list // Make empty contact invitation record list
final contactInvitationRecords = final contactInvitationRecords =
await (await DHTShortArray.create(parent: accountRec.key)) await (await DHTShortArray.create(parent: accountRec.key))
.scope((r) => r.record.ownedDHTRecordPointer); .scope((r) async => r.record.ownedDHTRecordPointer);
// Make empty chat record list // Make empty chat record list
final chatRecords = final chatRecords =
await (await DHTShortArray.create(parent: accountRec.key)) await (await DHTShortArray.create(parent: accountRec.key))
.scope((r) => r.record.ownedDHTRecordPointer); .scope((r) async => r.record.ownedDHTRecordPointer);
// Make account object // Make account object
final account = proto.Account() final account = proto.Account()

View 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),
));
}
}

View File

@ -25,169 +25,8 @@ class HomePage extends ConsumerStatefulWidget {
@override @override
HomePageState createState() => HomePageState(); HomePageState createState() => HomePageState();
}
// XXX Eliminate this when we have ValueChanged static Widget buildChatComponent(BuildContext context, WidgetRef ref) {
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) {
final contactList = ref.watch(fetchContactListProvider).asData?.value ?? final contactList = ref.watch(fetchContactListProvider).asData?.value ??
const IListConst([]); const IListConst([]);
@ -218,6 +57,47 @@ class HomePageState extends ConsumerState<HomePage>
activeChat: activeChat, activeChat: activeChat,
activeChatContact: activeChatContact); 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 // ignore: prefer_expression_function_bodies
Widget buildTablet(BuildContext context) { Widget buildTablet(BuildContext context) {

View File

@ -34,8 +34,8 @@ class MainPagerState extends ConsumerState<MainPager>
final _unfocusNode = FocusNode(); final _unfocusNode = FocusNode();
final pageController = PageController();
var _currentPage = 0; var _currentPage = 0;
final pageController = PageController();
final _selectedIconList = <IconData>[Icons.person, Icons.chat]; final _selectedIconList = <IconData>[Icons.person, Icons.chat];
// final _unselectedIconList = <IconData>[ // final _unselectedIconList = <IconData>[
@ -235,6 +235,11 @@ class MainPagerState extends ConsumerState<MainPager>
onNotification: onScrollNotification, onNotification: onScrollNotification,
child: PageView( child: PageView(
controller: pageController, controller: pageController,
onPageChanged: (index) {
setState(() {
_currentPage = index;
});
},
//physics: const NeverScrollableScrollPhysics(), //physics: const NeverScrollableScrollPhysics(),
children: List.generate( children: List.generate(
_bottomBarPages.length, (index) => _bottomBarPages[index]), _bottomBarPages.length, (index) => _bottomBarPages[index]),
@ -271,9 +276,6 @@ class MainPagerState extends ConsumerState<MainPager>
onTap: (index) async { onTap: (index) async {
await pageController.animateToPage(index, await pageController.animateToPage(index,
duration: 250.ms, curve: Curves.easeInOut); duration: 250.ms, curve: Curves.easeInOut);
setState(() {
_currentPage = index;
});
}, },
), ),

View File

@ -7,6 +7,7 @@ import '../entities/identity.dart';
import '../entities/proto.dart' as proto; import '../entities/proto.dart' as proto;
import '../entities/proto.dart' show Conversation; import '../entities/proto.dart' show Conversation;
import '../log/loggy.dart';
import '../veilid_support/veilid_support.dart'; import '../veilid_support/veilid_support.dart';
import 'account.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({ Future<IList<proto.Message>?> getLocalConversationMessages({
required ActiveAccountInfo activeAccountInfo, required ActiveAccountInfo activeAccountInfo,
required TypedKey localConversationRecordKey, required TypedKey localConversationRecordKey,

View File

@ -2,10 +2,13 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../pages/chat_only_page.dart';
import '../pages/home.dart'; import '../pages/home.dart';
import '../pages/index.dart'; import '../pages/index.dart';
import '../pages/new_account.dart'; import '../pages/new_account.dart';
import '../providers/chat.dart';
import '../providers/local_accounts.dart'; import '../providers/local_accounts.dart';
import '../tools/responsive.dart';
part 'router_notifier.g.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? /// Do we need to make or import an account immediately?
bool hasAnyAccount = false; bool hasAnyAccount = false;
bool hasActiveChat = false;
/// AsyncNotifier build /// AsyncNotifier build
@override @override
@ -23,6 +27,7 @@ class RouterNotifier extends _$RouterNotifier implements Listenable {
hasAnyAccount = await ref.watch( hasAnyAccount = await ref.watch(
localAccountsProvider.selectAsync((data) => data.isNotEmpty), localAccountsProvider.selectAsync((data) => data.isNotEmpty),
); );
hasActiveChat = ref.watch(activeChatStateProvider).asData?.value != null;
// When this notifier's state changes, inform GoRouter // When this notifier's state changes, inform GoRouter
ref.listenSelf((_, __) { ref.listenSelf((_, __) {
@ -46,6 +51,36 @@ class RouterNotifier extends _$RouterNotifier implements Listenable {
return hasAnyAccount ? HomePage.path : NewAccountPage.path; return hasAnyAccount ? HomePage.path : NewAccountPage.path;
case NewAccountPage.path: case NewAccountPage.path:
return hasAnyAccount ? HomePage.path : null; 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: default:
return hasAnyAccount ? null : NewAccountPage.path; return hasAnyAccount ? null : NewAccountPage.path;
} }
@ -65,6 +100,10 @@ class RouterNotifier extends _$RouterNotifier implements Listenable {
path: NewAccountPage.path, path: NewAccountPage.path,
builder: (context, state) => const NewAccountPage(), builder: (context, state) => const NewAccountPage(),
), ),
GoRoute(
path: ChatOnlyPage.path,
builder: (context, state) => const ChatOnlyPage(),
),
]; ];
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////

165
lib/tick.dart Normal file
View 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);
}
}
}

View File

@ -66,7 +66,7 @@ class DHTRecord {
_valid = false; _valid = false;
} }
Future<T> scope<T>(FutureOr<T> Function(DHTRecord) scopeFunction) async { Future<T> scope<T>(Future<T> Function(DHTRecord) scopeFunction) async {
try { try {
return await scopeFunction(this); return await scopeFunction(this);
} finally { } finally {
@ -76,8 +76,7 @@ class DHTRecord {
} }
} }
Future<T> deleteScope<T>( Future<T> deleteScope<T>(Future<T> Function(DHTRecord) scopeFunction) async {
FutureOr<T> Function(DHTRecord) scopeFunction) async {
try { try {
final out = await scopeFunction(this); final out = await scopeFunction(this);
if (_valid && _open) { if (_valid && _open) {

View File

@ -42,10 +42,10 @@ class DHTShortArray {
} }
stride = oCnt - 1; stride = oCnt - 1;
case DHTSchemaSMPL(oCnt: final oCnt, members: final members): 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'); throw StateError('Invalid SMPL schema in DHTShortArray');
} }
stride = members[1].mCnt - 1; stride = members[0].mCnt - 1;
} }
assert(stride <= maxElements, 'stride too long'); assert(stride <= maxElements, 'stride too long');
_stride = stride; _stride = stride;
@ -117,7 +117,7 @@ class DHTShortArray {
parent: parent, routingContext: routingContext, crypto: crypto); parent: parent, routingContext: routingContext, crypto: crypto);
try { try {
final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); final dhtShortArray = DHTShortArray._(headRecord: dhtRecord);
await dhtShortArray._refreshHead(); await dhtShortArray._refreshHead(forceRefresh: true);
return dhtShortArray; return dhtShortArray;
} on Exception catch (_) { } on Exception catch (_) {
await dhtRecord.close(); await dhtRecord.close();
@ -137,7 +137,7 @@ class DHTShortArray {
parent: parent, routingContext: routingContext, crypto: crypto); parent: parent, routingContext: routingContext, crypto: crypto);
try { try {
final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); final dhtShortArray = DHTShortArray._(headRecord: dhtRecord);
await dhtShortArray._refreshHead(); await dhtShortArray._refreshHead(forceRefresh: true);
return dhtShortArray; return dhtShortArray;
} on Exception catch (_) { } on Exception catch (_) {
await dhtRecord.close(); await dhtRecord.close();
@ -315,7 +315,7 @@ class DHTShortArray {
await Future.wait(futures); 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 { try {
return await scopeFunction(this); return await scopeFunction(this);
} finally { } finally {
@ -324,7 +324,7 @@ class DHTShortArray {
} }
Future<T> deleteScope<T>( Future<T> deleteScope<T>(
FutureOr<T> Function(DHTShortArray) scopeFunction) async { Future<T> Function(DHTShortArray) scopeFunction) async {
try { try {
final out = await scopeFunction(this); final out = await scopeFunction(this);
await close(); await close();