clean up windows and loading state

This commit is contained in:
Christien Rioux 2024-02-28 20:32:37 -05:00
parent ad9a77d68f
commit d00722433d
13 changed files with 130 additions and 175 deletions

View File

@ -29,7 +29,6 @@ class NewAccountPageState extends State<NewAccountPage> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
setState(() {});
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.portraitOnly);
});

View File

@ -2,7 +2,6 @@ import 'package:bloc_tools/bloc_tools.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:veilid_support/veilid_support.dart';
class ActiveChatCubit extends Cubit<TypedKey?> with BlocTools {
ActiveChatCubit(super.initialState);

View File

@ -57,7 +57,7 @@ class ChatComponent extends StatelessWidget {
AsyncValue<ActiveConversationState>?>(
(x) => x.state[remoteConversationRecordKey]);
if (avconversation == null) {
return debugPage('should always have an active conversation here');
return waitingPage();
}
final conversation = avconversation.data?.value;
if (conversation == null) {

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../chat/chat.dart';
import '../../proto/proto.dart' as proto;
//////////////////////////////////////////////////
@ -14,6 +15,7 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
ChatListCubit({
required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
required this.activeChatCubit,
}) : super(
open: () => _open(activeAccountInfo, account),
decodeElement: proto.Chat.fromBuffer);
@ -35,18 +37,35 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
Future<void> getOrCreateChatSingleContact({
required TypedKey remoteConversationRecordKey,
}) async {
// Add Chat to account's list
// if this fails, don't keep retrying, user can try again later
await operate((shortArray) async {
final remoteConversationRecordKeyProto =
remoteConversationRecordKey.toProto();
// See if we have added this chat already
for (var i = 0; i < shortArray.length; i++) {
final cbuf = await shortArray.getItem(i);
if (cbuf == null) {
throw Exception('Failed to get chat');
}
final c = proto.Chat.fromBuffer(cbuf);
if (c.remoteConversationKey == remoteConversationRecordKeyProto) {
// Nothing to do here
return;
}
}
// Create conversation type Chat
final chat = proto.Chat()
..type = proto.ChatType.SINGLE_CONTACT
..remoteConversationKey = remoteConversationRecordKey.toProto();
..remoteConversationKey = remoteConversationRecordKeyProto;
// Add Chat to account's list
// if this fails, don't keep retrying, user can try again later
final added = await operate(
(shortArray) => shortArray.tryAddItem(chat.writeToBuffer()));
// Add chat
final added = await shortArray.tryAddItem(chat.writeToBuffer());
if (!added) {
throw Exception('Failed to add chat');
}
});
}
/// Delete a chat
@ -58,6 +77,9 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
// Remove Chat from account's list
// if this fails, don't keep retrying, user can try again later
await operate((shortArray) async {
if (activeChatCubit.state == remoteConversationRecordKey) {
activeChatCubit.setActiveChat(null);
}
for (var i = 0; i < shortArray.length; i++) {
final cbuf = await shortArray.getItem(i);
if (cbuf == null) {
@ -71,4 +93,6 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
}
});
}
final ActiveChatCubit activeChatCubit;
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../chat/chat.dart';
import '../../../tools/tools.dart';
class HomeAccountReadyChat extends StatefulWidget {
const HomeAccountReadyChat({super.key});
@ -16,6 +17,11 @@ class HomeAccountReadyChatState extends State<HomeAccountReadyChat> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
});
}
@override

View File

@ -10,9 +10,24 @@ import '../../../theme/theme.dart';
import '../../../tools/tools.dart';
import 'main_pager/main_pager.dart';
class HomeAccountReadyMain extends StatelessWidget {
class HomeAccountReadyMain extends StatefulWidget {
const HomeAccountReadyMain({super.key});
@override
State<HomeAccountReadyMain> createState() => _HomeAccountReadyMainState();
}
class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
});
}
Widget buildUserPanel() => Builder(builder: (context) {
final account = context.watch<AccountRecordCubit>().state;
final theme = Theme.of(context);

View File

@ -135,9 +135,15 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
create: (context) => ContactListCubit(
activeAccountInfo: widget.activeAccountInfo,
account: account)),
BlocProvider(
create: (context) => ActiveChatCubit(null)
..withStateListen((event) {
widget.routerCubit.setHasActiveChat(event != null);
})),
BlocProvider(
create: (context) => ChatListCubit(
activeAccountInfo: widget.activeAccountInfo,
activeChatCubit: context.read<ActiveChatCubit>(),
account: account)),
BlocProvider(
create: (context) => ActiveConversationsBlocMapCubit(
@ -150,11 +156,6 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
activeAccountInfo: widget.activeAccountInfo,
)..followBloc(
context.read<ActiveConversationsBlocMapCubit>())),
BlocProvider(
create: (context) => ActiveChatCubit(null)
..withStateListen((event) {
widget.routerCubit.setHasActiveChat(event != null);
})),
BlocProvider(
create: (context) => WaitingInvitationsBlocMapCubit(
activeAccountInfo: widget.activeAccountInfo,

View File

@ -2,9 +2,26 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:radix_colors/radix_colors.dart';
class IndexPage extends StatelessWidget {
import '../tools/tools.dart';
class IndexPage extends StatefulWidget {
const IndexPage({super.key});
@override
State<IndexPage> createState() => _IndexPageState();
}
class _IndexPageState extends State<IndexPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await changeWindowSetup(
TitleBarStyle.hidden, OrientationCapability.normal);
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);

View File

@ -25,6 +25,11 @@ class SettingsPageState extends State<SettingsPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
});
}
@override

View File

@ -7,9 +7,6 @@ import 'package:veilid_support/veilid_support.dart';
import 'init.dart';
import 'veilid_processor/veilid_processor.dart';
const int ticksPerContactInvitationCheck = 5;
const int ticksPerNewMessageCheck = 5;
class BackgroundTicker extends StatefulWidget {
const BackgroundTicker({required this.builder, super.key});
@ -28,11 +25,6 @@ class BackgroundTicker extends StatefulWidget {
class BackgroundTickerState extends State<BackgroundTicker> {
Timer? _tickTimer;
bool _inTick = false;
bool _inDoContactInvitationCheck = false;
bool _inDoNewMessageCheck = false;
int _contactInvitationCheckTick = 0;
int _newMessageCheckTick = 0;
@override
void initState() {
@ -73,145 +65,9 @@ class BackgroundTickerState extends State<BackgroundTicker> {
_inTick = true;
try {
// Tick DHT record pool
if (!DHTRecordPool.instance.inTick) {
unawaited(DHTRecordPool.instance.tick());
}
// Check extant contact invitations once every N seconds
_contactInvitationCheckTick += 1;
if (_contactInvitationCheckTick >= ticksPerContactInvitationCheck) {
_contactInvitationCheckTick = 0;
if (!_inDoContactInvitationCheck) {
unawaited(_doContactInvitationCheck());
}
}
// Check new messages once every N seconds
_newMessageCheckTick += 1;
if (_newMessageCheckTick >= ticksPerNewMessageCheck) {
_newMessageCheckTick = 0;
if (!_inDoNewMessageCheck) {
unawaited(_doNewMessageCheck());
}
}
} finally {
_inTick = false;
}
}
Future<void> _doContactInvitationCheck() async {
if (_inDoContactInvitationCheck) {
return;
}
_inDoContactInvitationCheck = true;
if (!ProcessorRepository
.instance.processorConnectionState.isPublicInternetReady) {
return;
}
// final contactInvitationRecords =
// await ref.read(fetchContactInvitationRecordsProvider.future);
// if (contactInvitationRecords == null) {
// return;
// }
try {
// final activeAccountInfo =
// await ref.read(fetchActiveAccountProvider.future);
// if (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);
} finally {
_inDoContactInvitationCheck = true;
}
}
Future<void> _doNewMessageCheck() async {
if (_inDoNewMessageCheck) {
return;
}
_inDoNewMessageCheck = true;
try {
if (!ProcessorRepository
.instance.processorConnectionState.isPublicInternetReady) {
return;
}
// final activeChat = ref.read(activeChatStateProvider);
// 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 && newMessages.isNotEmpty) {
// final changed = await mergeLocalConversationMessages(
// activeAccountInfo: activeAccountInfo,
// localConversationRecordKey: localConversationRecordKey,
// remoteIdentityPublicKey: remoteIdentityPublicKey,
// newMessages: newMessages);
// if (changed) {
// ref.invalidate(activeConversationMessagesProvider);
// }
// }
} finally {
_inDoNewMessageCheck = false;
}
}
}

View File

@ -29,10 +29,10 @@ class DeveloperPage extends StatefulWidget {
const DeveloperPage({super.key});
@override
DeveloperPageState createState() => DeveloperPageState();
State<DeveloperPage> createState() => _DeveloperPageState();
}
class DeveloperPageState extends State<DeveloperPage> {
class _DeveloperPageState extends State<DeveloperPage> {
final _terminalController = TerminalController();
final _debugCommandController = TextEditingController();
final _logLevelController = DropdownController(duration: 250.ms);
@ -43,6 +43,12 @@ class DeveloperPageState extends State<DeveloperPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
});
_terminalController.addListener(() {
setState(() {});
});

View File

@ -57,7 +57,7 @@ abstract mixin class StateFollower<S extends Object, K, V> {
}
late IMap<K, V> _lastInputStateMap;
late final StreamSubscription<S> _subscription;
final SingleStateProcessor<IMap<K, V>> _singleStateProcessor =
SingleStateProcessor();
late final StreamSubscription<S> _subscription;
}

View File

@ -15,6 +15,9 @@ part 'dht_record_pool.g.dart';
part 'dht_record.dart';
const int watchBackoffMultiplier = 2;
const int watchBackoffMax = 30;
/// Record pool that managed DHTRecords and allows for tagged deletion
@freezed
class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations {
@ -109,7 +112,11 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
// Convenience accessor
final Veilid _veilid;
// If tick is already running or not
bool inTick = false;
bool _inTick = false;
// Tick counter for backoff
int _tickCount = 0;
// Backoff timer
int _watchBackoffTimer = 1;
static DHTRecordPool? _singleton;
@ -578,13 +585,19 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
/// Ticker to check watch state change requests
Future<void> tick() async {
if (inTick) {
if (_tickCount < _watchBackoffTimer) {
_tickCount++;
return;
}
inTick = true;
if (_inTick) {
return;
}
_inTick = true;
_tickCount = 0;
try {
// See if any opened records need watch state changes
final unord = <Future<void> Function()>[];
final unord = <Future<bool> Function()>[];
await _mutex.protect(() async {
for (final kv in _opened.entries) {
@ -600,16 +613,19 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
if (watchState == null) {
unord.add(() async {
// Record needs watch cancel
var success = false;
try {
await dhtctx.cancelDHTWatch(openedRecordKey);
success = await dhtctx.cancelDHTWatch(openedRecordKey);
openedRecordInfo.shared.needsWatchStateUpdate = false;
} on VeilidAPIException {
// Failed to cancel DHT watch, try again next tick
}
return success;
});
} else {
unord.add(() async {
// Record needs new watch
var success = false;
try {
final realExpiration = await dhtctx.watchDHTValues(
openedRecordKey,
@ -622,10 +638,12 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
openedRecordInfo.shared.needsWatchStateUpdate = false;
_updateWatchExpirations(
openedRecordInfo.records, realExpiration);
success = true;
}
} on VeilidAPIException {
// Failed to cancel DHT watch, try again next tick
}
return success;
});
}
}
@ -633,9 +651,18 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
});
// Process all watch changes
await unord.map((f) => f()).wait;
// If any watched did not success, back off the attempts to
// update the watches for a bit
final allSuccess =
(await unord.map((f) => f()).wait).reduce((a, b) => a && b);
if (!allSuccess) {
_watchBackoffTimer *= watchBackoffMultiplier;
_watchBackoffTimer = min(_watchBackoffTimer, watchBackoffMax);
} else {
_watchBackoffTimer = 1;
}
} finally {
inTick = false;
_inTick = false;
}
}
}